面试题-6 2020-07-08 前端,Javascript 暂无评论 211 次阅读 ### 55. 手写call、apply及bind函数 **call 函数的实现步骤:** - 1.判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 - 2.判断传入上下文对象是否存在,如果不存在,则设置为 window 。 - 3.处理传入的参数,截取第一个参数后的所有参数。 - 4.将函数作为上下文对象的一个属性。 - 5.使用上下文对象来调用这个方法,并保存返回结果。 - 6.删除刚才新增的属性。 - 7.返回结果。 ```js // 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 函数的实现步骤:** - 1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 - 2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 - 3. 将函数作为上下文对象的一个属性。 - 4. 判断参数值是否传入 - 4. 使用上下文对象来调用这个方法,并保存返回结果。 - 5. 删除刚才新增的属性 - 6. 返回结果 ```js // 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 函数的实现步骤:** - 1.判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 - 2.保存当前函数的引用,获取其余传入参数值。 - 3.创建一个函数返回 - 4.函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。 ```js // 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) ); }; }; ``` 参考文章: [《手写 call、apply 及 bind 函数》](https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5bdd0d8e6fb9a04a044073fe) [《JavaScript 深入之 call 和 apply 的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) ### 56. 函数柯里化的实现 ```js // 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。 function curry(fn, args) { // 获取函数需要的参数长度 let length = fn.length; args = args || []; return function() { let subArgs = args.slice(0); // 拼接得到现有的所有参数 for (let i = 0; i < arguments.length; i++) { subArgs.push(arguments[i]); } // 判断参数的长度是否已经满足函数所需参数的长度 if (subArgs.length >= length) { // 如果满足,执行函数 return fn.apply(this, subArgs); } else { // 如果不满足,递归返回科里化的函数,等待参数的传入 return curry.call(this, fn, subArgs); } }; } // es6 实现 function curry(fn, ...args) { return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); } ``` 参考文章: [《JavaScript 专题之函数柯里化》](https://github.com/mqyqingfeng/Blog/issues/42) ### 57. js模拟new操作符的实现 这个问题如果你在掘金上搜,你可能会搜索到类似下面的回答:  说实话,看第一遍,我是不理解的,我需要去理一遍原型及原型链的知识才能理解。所以我觉得[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)对new的解释更容易理解: `new` 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。`new` 关键字会进行如下的操作: 1. 创建一个空的简单JavaScript对象(即{}); 2. 链接该对象(即设置该对象的构造函数)到另一个对象 ; 3. 将步骤1新创建的对象作为this的上下文 ; 4. 如果该函数没有返回对象,则返回this。 接下来我们看实现: ```js function Dog(name, color, age) { this.name = name; this.color = color; this.age = age; } Dog.prototype={ getName: function() { return this.name } } var dog = new Dog('大黄', 'yellow', 3) ``` 上面的代码相信不用解释,大家都懂。我们来看最后一行带`new`关键字的代码,按照上述的1,2,3,4步来解析`new`背后的操作。 第一步:创建一个简单空对象 ```js var obj = {} ``` 第二步:链接该对象到另一个对象(原型链) ```js // 设置原型链 obj.__proto__ = Dog.prototype ``` 第三步:将步骤1新创建的对象作为 `this` 的上下文 ```js // this指向obj对象 Dog.apply(obj, ['大黄', 'yellow', 3]) ``` 第四步:如果该函数没有返回对象,则返回this ```js // 因为 Dog() 没有返回值,所以返回obj var dog = obj dog.getName() // '大黄' ``` 需要注意的是如果 Dog() 有 return 则返回 return的值 ```js var rtnObj = {} function Dog(name, color, age) { // ... //返回一个对象 return rtnObj } var dog = new Dog('大黄', 'yellow', 3) console.log(dog === rtnObj) // true ``` 接下来我们将以上步骤封装成一个对象实例化方法,即模拟new的操作: ```js function objectFactory(){ var obj = {}; //取得该方法的第一个参数(并删除第一个参数),该参数是构造函数 var Constructor = [].shift.apply(arguments); //将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法 obj.__proto__ = Constructor.prototype; //取得构造函数的返回值 var ret = Constructor.apply(obj, arguments); //如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象 return typeof ret === "object" ? ret : obj; } ``` ### 58. 什么是回调函数?回调函数有什么缺点 **回调函数**是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。 在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。 ```js const btnAdd = document.getElementById('btnAdd'); btnAdd.addEventListener('click', function clickCallback(e) { // do something useless }); ``` 在本例中,我们等待id为`btnAdd`的元素中的`click`事件,如果它被单击,则执行`clickCallback`函数。回调函数向某些数据或事件添加一些功能。 回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性: ```js setTimeout(() => { console.log(1) setTimeout(() => { console.log(2) setTimeout(() => { console.log(3) },3000) },2000) },1000) ``` 这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。接下来的两条就是来解决这些问题的,咱们往下看。 ### 59. Promise是什么,可以手写实现一下吗? Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)的相关说明: Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。 一个 Promise有以下几种状态: * pending: 初始状态,既不是成功,也不是失败状态。 * fulfilled: 意味着操作成功完成。 * rejected: 意味着操作失败。 这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。 可能光看概念大家不理解Promise,我们举个简单的栗子; 假如我有个女朋友,下周一是她生日,我答应她生日给她一个惊喜,那么从现在开始这个承诺就进入等待状态,等待下周一的到来,然后状态改变。如果下周一我如约给了女朋友惊喜,那么这个承诺的状态就会由pending切换为fulfilled,表示承诺成功兑现,一旦是这个结果了,就不会再有其他结果,即状态不会在发生改变;反之如果当天我因为工作太忙加班,把这事给忘了,说好的惊喜没有兑现,状态就会由pending切换为rejected,时间不可倒流,所以状态也不能再发生变化。 上一条我们说过Promise可以解决回调地狱的问题,没错,pending 状态的 Promise 对象会触发 fulfilled/rejected 状态,一旦状态改变,Promise 对象的 then 方法就会被调用;否则就会触发 catch。我们将上一条回调地狱的代码改写一下: ```js new Promise((resolve,reject) => { setTimeout(() => { console.log(1) resolve() },1000) }).then((res) => { setTimeout(() => { console.log(2) },2000) }).then((res) => { setTimeout(() => { console.log(3) },3000) }).catch((err) => { console.log(err) }) ``` 其实Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。 **promise手写实现,面试够用版:** ```js function myPromise(constructor){ let self=this; self.status="pending" //定义状态改变前的初始状态 self.value=undefined;//定义状态为resolved的时候的状态 self.reason=undefined;//定义状态为rejected的时候的状态 function resolve(value){ //两个==="pending",保证了状态的改变是不可逆的 if(self.status==="pending"){ self.value=value; self.status="resolved"; } } function reject(reason){ //两个==="pending",保证了状态的改变是不可逆的 if(self.status==="pending"){ self.reason=reason; self.status="rejected"; } } //捕获构造异常 try{ constructor(resolve,reject); }catch(e){ reject(e); } } // 定义链式调用的then方法 myPromise.prototype.then=function(onFullfilled,onRejected){ let self=this; switch(self.status){ case "resolved": onFullfilled(self.value); break; case "rejected": onRejected(self.reason); break; default: } } ``` 关于Promise还有其他的知识,比如Promise.all()、Promise.race()等的运用,由于篇幅原因就不再做展开,想要深入了解的可看下面的文章。 相关资料: [「硬核JS」深入了解异步解决方案](https://juejin.im/post/5e4613b36fb9a07ccc45e339#heading-69) [【翻译】Promises/A+规范](https://www.ituring.com.cn/article/66566#) ### 60. `Iterator`是什么,有什么作用? `Iterator`是理解第24条的先决知识,也许是我IQ不够,`Iterator和Generator`看了很多遍还是一知半解,即使当时理解了,过一阵又忘得一干二净。。。 Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 **Iterator语法:** ```js const obj = { [Symbol.iterator]:function(){} } ``` `[Symbol.iterator] `属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。 迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据 每一次的 next 都会返回一个对象,该对象有两个属性 * value 代表想要获取的数据 * done 布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束 **Iterator 的作用有三个:** 1. 为各种数据结构,提供一个统一的、简便的访问接口; 2. 使得数据结构的成员能够按某种次序排列; 3. ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。 **遍历过程:** 1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。 3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。 4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。 每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。 ```js let arr = [{num:1},2,3] let it = arr[Symbol.iterator]() // 获取数组中的迭代器 console.log(it.next()) // { value: Object { num: 1 }, done: false } console.log(it.next()) // { value: 2, done: false } console.log(it.next()) // { value: 3, done: false } console.log(it.next()) // { value: undefined, done: true } ``` ### 61. `Generator`函数是什么,有什么作用? Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。 ```js function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true} ``` 上面这个示例就是一个Generator函数,我们来分析其执行过程: * `首先 Generator 函数调用时它会返回一个迭代器` * `当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6` * `当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8` * `当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42` `Generator` 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,我们可以通过 `Generator` 函数解决回调地狱的问题。 ### 62. 什么是 `async/await` 及其如何工作,有什么优缺点? `async/await`是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。 `async` 是异步的意思,而 `await` 是 `async wait`的简写,即异步等待。 所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。 一个函数如果加上 async ,那么该函数就会返回一个 Promise ``` async function test() { return "1" } console.log(test()) // -> Promise {: "1"} ``` 可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 `PromIse.resolve() `封装成Promise对象返回。 相比于 `Promise`,`async/await`能更好地处理 then 链 ```js function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); }); } function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n); } function step2(n) { console.log(`step2 with ${n}`); return takeLongTime(n); } function step3(n) { console.log(`step3 with ${n}`); return takeLongTime(n); } ``` 现在分别用 `Promise` 和`async/await`来实现这三个步骤的处理。 **使用Promise** ```js function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); }); } doIt(); // step1 with 300 // step2 with 500 // step3 with 700 // result is 900 ``` **使用`async/await`** ``` async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); } doIt(); ``` 结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。 ```! await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。 ``` **优缺点:** `async/await`的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。 参考文章: [「硬核JS」深入了解异步解决方案](https://juejin.im/post/5e4613b36fb9a07ccc45e339#heading-69) `以上21~25条就是JavaScript中主要的异步解决方案了,难度是有的,需要好好揣摩并加以练习。` 标签: js面试题 本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭