多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] ### 手写 instanceof 方法 instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。 实现步骤: 1. 首先获取类型的原型 2. 然后获得对象的原型 3. 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 `null`,因为原型链最终为 `null` 具体实现: ``` function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象 // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } } ``` ### 手写 new 操作符 在调用 `new` 的过程中会发生以上四件事情: 1. 首先创建了一个新的空对象 2. 设置原型,将对象的原型设置为函数的 prototype 对象。 3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性) 4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。 ``` function objectFactory() { let newObject = null; let constructor = Array.prototype.shift.call(arguments); let result = null; // 判断参数是否是一个函数 if (typeof constructor !== "function") { console.error("type error"); return; } // 新建一个空对象,对象的原型为构造函数的 prototype 对象 newObject = Object.create(constructor.prototype); // 将 this 指向新建对象,并执行函数 result = constructor.apply(newObject, arguments); // 判断返回对象 let flag = result && (typeof result === "object" || typeof result === "function"); // 判断返回结果 return flag ? result : newObject; } // 使用方法 objectFactory(构造函数, 初始化参数); ``` ### 手写Promise ``` const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; function MyPromise(fn) { // 保存初始化状态 var self = this; // 初始化状态 this.state = PENDING; // 用于保存 resolve 或者 rejected 传入的值 this.value = null; // 用于保存 resolve 的回调函数 this.resolvedCallbacks = []; // 用于保存 reject 的回调函数 this.rejectedCallbacks = []; // 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); } // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED; // 设置传入的值 self.value = value; // 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); } // 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED; // 设置传入的值 self.value = value; // 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); } // 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); } } MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; }; onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; }; // 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); } // 如果状态已经凝固,则直接执行对应状态的函数 if (this.state === RESOLVED) { onResolved(this.value); } if (this.state === REJECTED) { onRejected(this.value); } }; ``` ### 手写 Promise.then `then` 方法返回一个新的 `promise` 实例,为了在 `promise` 状态发生变化时(`resolve` / `reject` 被调用时)再执行 `then` 里的函数,我们使用一个 `callbacks` 数组先把传给then的函数暂存起来,等状态改变时再调用。 **那么,怎么保证后一个** `**then**` **里的方法在前一个** `**then**`**(可能是异步)结束之后再执行呢?** 我们可以将传给 `then` 的函数和新 `promise` 的 `resolve` 一起 `push` 到前一个 `promise` 的 `callbacks` 数组中,达到承前启后的效果: * 承前:当前一个 `promise` 完成后,调用其 `resolve` 变更状态,在这个 `resolve` 里会依次调用 `callbacks` 里的回调,这样就执行了 `then` 里的方法了 * 启后:上一步中,当 `then` 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 `promise` 的 `resolve`,让其状态变更,这又会依次调用新 `promise` 的 `callbacks` 数组里的方法,循环往复。。如果返回的结果是个 `promise`,则需要等它完成之后再触发新 `promise` 的 `resolve`,所以可以在其结果的 `then` 里调用新 `promise` 的 `resolve`。 ``` then(onFulfilled, onReject){ // 保存前一个promise的this const self = this; return new MyPromise((resolve, reject) => { // 封装前一个promise成功时执行的函数 let fulfilled = () => { try{ const result = onFulfilled(self.value); // 承前 return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后 }catch(err){ reject(err) } } // 封装前一个promise失败时执行的函数 let rejected = () => { try{ const result = onReject(self.reason); return result instanceof MyPromise? result.then(resolve, reject) : reject(result); }catch(err){ reject(err) } } switch(self.status){ case PENDING: self.onFulfilledCallbacks.push(fulfilled); self.onRejectedCallbacks.push(rejected); break; case FULFILLED: fulfilled(); break; case REJECT: rejected(); break; } }) } ``` **注意:** * 连续多个 `then` 里的回调方法是同步注册的,但注册到了不同的 `callbacks` 数组中,因为每次 `then` 都返回新的 `promise` 实例(参考上面的例子和图) * 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 `callbacks` 数组中提前注册的回调 ### 手写 Promise.all **核心思路** 1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数 2. 这个方法返回一个新的 promise 对象, 3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象 4. 参数所有回调成功才是成功,返回值数组与参数顺序一致 5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。 **实现代码** 一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了。 ``` function promiseAll(promises) { return new Promise(function(resolve, reject) { if(!Array.isArray(promises)){ throw new TypeError(`argument must be a array`) } var resolvedCounter = 0; var promiseNum = promises.length; var resolvedResult = []; for (let i = 0; i < promiseNum; i++) { Promise.resolve(promises[i]).then(value=>{ resolvedCounter++; resolvedResult[i] = value; if (resolvedCounter == promiseNum) { return resolve(resolvedResult) } },error=>{ return reject(error) }) } }) } // test let p1 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(1) }, 1000) }) let p2 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(2) }, 2000) }) let p3 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(3) }, 3000) }) promiseAll([p3, p1, p2]).then(res => { console.log(res) // [3, 1, 2] }) ``` ### 手写 Promise.race 该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态**只能改变一次**, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可。 ``` Promise.race = function (args) { return new Promise((resolve, reject) => { for (let i = 0, len = args.length; i < len; i++) { args[i].then(resolve, reject) } }) } ``` ### 手写防抖函数 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。也可以[点击](https://www.kancloud.cn/vvmily_king/vvmily/2331774)查看以往。 ``` // 函数防抖的实现 function debounce(fn, wait) { let timer = null; return function() { let context = this, args = arguments; // 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; } ``` ### 手写节流函数 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。也可以[点击](https://www.kancloud.cn/vvmily_king/vvmily/2331774)查看以往。 ``` // 函数节流的实现; function throttle(fn, delay) { let curTime = Date.now(); return function() { let context = this, args = arguments, nowTime = Date.now(); // 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } }; } ``` ### 手写 call 函数 call 函数的实现步骤: 1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 3. 处理传入的参数,截取第一个参数后的所有参数。 4. 将函数作为上下文对象的一个属性。 5. 使用上下文对象来调用这个方法,并保存返回结果。 6. 删除刚才新增的属性。 7. 返回结果。 ``` // call函数实现 Function.prototype.myCall = function(context) { // 判断调用对象 if (typeof this !== "function") { console.error("type error"); } // 获取参数 let args = [...arguments].slice(1), result = null; // 判断 context 是否传入,如果未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(...args); // 将属性删除 delete context.fn; return result; }; ``` ### 手写 apply 函数 apply 函数的实现步骤: 1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 3. 将函数作为上下文对象的一个属性。 4. 判断参数值是否传入 5. 使用上下文对象来调用这个方法,并保存返回结果。 6. 删除刚才新增的属性 7. 返回结果 ``` // apply 函数实现 Function.prototype.myApply = function(context) { // 判断调用对象是否为函数 if (typeof this !== "function") { throw new TypeError("Error"); } let result = null; // 判断 context 是否存在,如果未传入则为 window context = context || window; // 将函数设为对象的方法 context.fn = this; // 调用方法 if (arguments[1]) { result = context.fn(...arguments[1]); } else { result = context.fn(); } // 将属性删除 delete context.fn; return result; }; ``` ### 手写 bind 函数 bind 函数的实现步骤: 1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 2. 保存当前函数的引用,获取其余传入参数值。 3. 创建一个函数返回 4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。 ``` // bind 函数实现 Function.prototype.myBind = function(context) { // 判断调用对象是否为函数 if (typeof this !== "function") { throw new TypeError("Error"); } // 获取参数 var args = [...arguments].slice(1), fn = this; return function Fn() { // 根据调用方式,传入不同绑定值 return fn.apply( this instanceof Fn ? this : context, args.concat(...arguments) ); }; }; ``` ### 实现AJAX请求 AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 创建AJAX请求的步骤: * **创建一个 XMLHttpRequest 对象。** * 在这个对象上**使用 open 方法创建一个 HTTP 请求**,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。 * 在发起请求前,可以为这个对象**添加一些信息和监听函数**。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。 * 当对象的属性和监听函数设置完成后,最后调**用 sent 方法来向服务器发起请求**,可以传入参数作为发送的数据体。 ``` const SERVER_URL = "/server"; let xhr = new XMLHttpRequest(); // 创建 Http 请求 xhr.open("GET", SERVER_URL, true); // 设置状态监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); } }; // 设置请求失败时的监听函数 xhr.onerror = function() { console.error(this.statusText); }; // 设置请求头信息 xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); // 发送 Http 请求 xhr.send(null); ``` ### 实现浅拷贝 浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。 1. `Object.assign()`:是ES6中对象的拷贝方法,接受的第一个参数是目标对象,其余参数是源对象,用法:`Object.assign(target, source_1, ···)`,该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。 2. 扩展运算符:使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:`let cloneObj = { ...obj };`。 3. **Array.prototype.slice**:`arr.slice()`。 4. **Array.prototype.concat**:`arr.concat()`。 5. 手写浅拷贝: ``` // 浅拷贝的实现; function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; } ``` ### 实现深拷贝 * **浅拷贝:** 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用  Object.assign 和展开运算符来实现。 * **深拷贝:** 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败。 1. JSON.stringify() * `JSON.parse(JSON.stringify(obj))`是目前比较常用的深拷贝方法之一,它的原理就是利用`JSON.stringify` 将`js`对象序列化(JSON字符串),再使用`JSON.parse`来反序列化(还原)`js`对象。 * 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过`JSON.stringify()`进行处理之后,都会消失。 ``` let obj1 = { a: 0, b: { c: 0 } }; let obj2 = JSON.parse(JSON.stringify(obj1)); obj1.a = 1; obj1.b.c = 1; console.log(obj1); // {a: 1, b: {c: 1}} console.log(obj2); // {a: 0, b: {c: 0}} ``` 2. 手写实现深拷贝函数 ``` // 深拷贝的实现 function deepCopy(object) { if (!object || typeof object !== "object") return; let newObject = Array.isArray(object) ? [] : {}; for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } } return newObject; } ``` ### 实现数组的扁平化 **递归实现** * 普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接: ``` let arr = [1, [2, [3, 4, 5]]]; function flatten(arr) { let result = []; for(let i = 0; i < arr.length; i++) { if(Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])); } else { result.push(arr[i]); } } return result; } flatten(arr); // [1, 2, 3, 4,5] ``` **reduce 函数迭代** * 从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示: ``` let arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } console.log(flatten(arr));// [1, 2, 3, 4,5] ``` **扩展运算符实现** * 这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的: ``` let arr = [1, [2, [3, 4]]]; function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flatten(arr)); // [1, 2, 3, 4,5] ``` ### 实现数组去重 给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。(方法很多,就不一一列举了) * ES6方法(使用数据结构集合): ``` const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8] ``` * ES5方法:使用map存储不重复的数字 ``` const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; uniqueArray(array); // [1, 2, 3, 5, 9, 8] function uniqueArray(array) { let map = {}; let res = []; for(var i = 0; i < array.length; i++) { if(!map.hasOwnProperty([array[i]])) { map[array[i]] = 1; res.push(array[i]); } } return res; } ``` ### 大数相加 如果想要对一个超大的整数(`> Number.MAX_SAFE_INTEGER`)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 `Number.MAX_SAFE_INTEGER` 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。 实现一个算法进行大数的相加: ``` function sumBigNumber(a, b) { let res = ''; let temp = 0; a = a.split(''); b = b.split(''); while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, ''); } ``` 其主要的思路如下: * 首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化 * 初始化res,temp来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算 * 将两个数组的对应的位进行相加,两个数相加的结果可能大于10,所以可能要仅为,对10进行取余操作,将结果保存在当前位 * 判断当前位是否大于9,也就是是否会进位,若是则将temp赋值为true,因为在加法运算中,true会自动隐式转化为1,以便于下一次相加 * 重复上述操作,直至计算结束 ### 大数相乘 ``` function multiplyBigNum(num1, num2) { //判断输入是不是数字 if (isNaN(num1) || isNaN(num2)) return ""; num1 = num1 + "" num2 = num2 + "" let len1 = num1.length, len2 = num2.length; let pos = []; //j放外面,先固定被乘数的一位,分别去乘乘数的每一位,更符合竖式演算法 for (let j = len2 - 1; j >= 0; j--) { for (let i = len1 - 1; i >= 0; i--) { //两个个位数相乘,最多产生两位数,index1代表十位,index2代表个位 let index1 = i + j, index2 = i + j + 1; //两个个位数乘积加上当前位置个位已累积的数字,会产生进位,比如08 + 7 = 15,产生了进位1 let mul = num1[i] * num2[j] + (pos[index2] || 0); //mul包含新计算的十位,加上原有的十位就是最新的十位 pos[index1] = Math.floor(mul / 10) + (pos[index1] || 0); //mul的个位就是最新的个位 pos[index2] = mul % 10; } } //去掉前置0 let result = pos.join("").replace(/^0+/, ""); return result - 0 || '0'; } ``` ### 实现 add(1)(2)(3) 函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。 ``` function add (a) { return function (b) { return function (c) { return a + b + c; } } } console.log(add(1)(2)(3)); // 6 ``` ### 实现类数组转化为数组 类数组转换为数组的方法有这样几种: * 通过 call 调用数组的 slice 方法来实现转换 ``` Array.prototype.slice.call(arrayLike); ``` * 通过 call 调用数组的 splice 方法来实现转换 ``` Array.prototype.splice.call(arrayLike, 0); ``` * 通过 apply 调用数组的 concat 方法来实现转换 ``` Array.prototype.concat.apply([], arrayLike); ``` * 通过 Array.from 方法来实现转换 ``` Array.from(arrayLike); ``` ### 将js对象转化为树形结构 ``` // 转换前: source = [{ id: 1, pid: 0, name: 'body' }, { id: 2, pid: 1, name: 'title' }, { id: 3, pid: 2, name: 'div' }] // 转换为: tree = [{ id: 1, pid: 0, name: 'body', children: [{ id: 2, pid: 1, name: 'title', children: [{ id: 3, pid: 1, name: 'div' }] } }] ``` 代码实现: ``` function jsonToTree(data) { // 初始化结果数组,并判断输入数据的格式 let result = [] if(!Array.isArray(data)) { return result } // 使用map,将当前对象的id与当前对象对应存储起来 let map = {}; data.forEach(item => { map[item.id] = item; }); // data.forEach(item => { let parent = map[item.pid]; if(parent) { (parent.children || (parent.children = [])).push(item); } else { result.push(item); } }); return result; } ``` ### 实现prototype继承 所谓的原型链继承就是让新实例的原型等于父类的实例: ``` //父方法 function SupperFunction(flag1){ this.flag1 = flag1; } //子方法 function SubFunction(flag2){ this.flag2 = flag2; } //父实例 var superInstance = new SupperFunction(true); //子继承父 SubFunction.prototype = superInstance; //子实例 var subInstance = new SubFunction(false); //子调用自己和父的属性 subInstance.flag1; // true subInstance.flag2; // false ``` ### 实现双向数据绑定 ``` let obj = {} let input = document.getElementById('input') let span = document.getElementById('span') // 数据劫持 Object.defineProperty(obj, 'text', { configurable: true, enumerable: true, get() { console.log('获取数据了') }, set(newVal) { console.log('数据更新了') input.value = newVal span.innerHTML = newVal } }) // 输入监听 input.addEventListener('keyup', function(e) { obj.text = e.target.value }) ``` ### 实现斐波那契数列 ``` // 递归 function fn (n){ if(n==0) return 0 if(n==1) return 1 return fn(n-2)+fn(n-1) } // 优化 function fibonacci2(n) { const arr = [1, 1, 2]; const arrLen = arr.length; if (n <= arrLen) { return arr[n]; } for (let i = arrLen; i < n; i++) { arr.push(arr[i - 1] + arr[ i - 2]); } return arr[arr.length - 1]; } // 非递归 function fn(n) { let pre1 = 1; let pre2 = 1; let current = 2; if (n <= 2) { return current; } for (let i = 2; i < n; i++) { pre1 = pre2; pre2 = current; current = pre1 + pre2; } return current; } ``` ### 使用 setTimeout 实现 setInterval setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。 针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。 实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果 ``` function mySetInterval(fn, timeout) { // 控制器,控制定时器是否继续执行 var timer = { flag: true }; // 设置递归函数,模拟定时器执行。 function interval() { if (timer.flag) { fn(); setTimeout(interval, timeout); } } // 启动定时器 setTimeout(interval, timeout); // 返回控制器 return timer; } ```