javascript
面试官问:能否模拟实现JS的bind方法(高频考点)
可以點(diǎn)擊上方的話題JS基礎(chǔ)系列,查看往期文章
寫于2018年11月21日,發(fā)布在掘金閱讀量1.3w+
前言
這是面試官問系列的第二篇,旨在幫助讀者提升JS基礎(chǔ)知識,包含new、call、apply、this、繼承相關(guān)知識。
面試官問系列文章如下:感興趣的讀者可以點(diǎn)擊閱讀。
1.面試官問:能否模擬實(shí)現(xiàn)JS的new操作符
2.面試官問:能否模擬實(shí)現(xiàn)JS的bind方法(本文)
3.面試官問:能否模擬實(shí)現(xiàn)JS的call和apply方法
4.面試官問:JS的this指向
5.面試官問:JS的繼承
用過React的同學(xué)都知道,經(jīng)常會使用bind來綁定this。
import?React,?{?Component?}?from?'react'; class?TodoItem?extends?Component{constructor(props){super(props);this.handleClick?=?this.handleClick.bind(this);}handleClick(){console.log('handleClick');}render(){return??(<div?onClick={this.handleClick}>點(diǎn)擊</div>);}; } export?default?TodoItem;那么面試官可能會問是否想過bind到底做了什么,怎么模擬實(shí)現(xiàn)呢。
附上之前寫文章寫過的一段話:已經(jīng)有很多模擬實(shí)現(xiàn)bind的文章,為什么自己還要寫一遍呢。學(xué)習(xí)就好比是座大山,人們沿著不同的路登山,分享著自己看到的風(fēng)景。你不一定能看到別人看到的風(fēng)景,體會到別人的心情。只有自己去登山,才能看到不一樣的風(fēng)景,體會才更加深刻。
先看一下bind是什么。從上面的React代碼中,可以看出bind執(zhí)行后是函數(shù),并且每個(gè)函數(shù)都可以執(zhí)行調(diào)用它。眼見為實(shí),耳聽為虛。讀者可以在控制臺一步步點(diǎn)開例子1中的obj:
var?obj?=?{}; console.log(obj); console.log(typeof?Function.prototype.bind);?//?function console.log(typeof?Function.prototype.bind());??//?function console.log(Function.prototype.bind.name);??//?bind console.log(Function.prototype.bind().name);??//?bound Function.prototype.bind因此可以得出結(jié)論1:
1、bind是Functoin原型鏈中Function.prototype的一個(gè)屬性,每個(gè)函數(shù)都可以調(diào)用它。
2、bind本身是一個(gè)函數(shù)名為bind的函數(shù),返回值也是函數(shù),函數(shù)名是bound。(打出來就是bound加上一個(gè)空格)。知道了bind是函數(shù),就可以傳參,而且返回值'bound '也是函數(shù),也可以傳參,就很容易寫出例子2:
后文統(tǒng)一 bound 指原函數(shù)original bind之后返回的函數(shù),便于說明。
由此可以得出結(jié)論2:
1、調(diào)用bind的函數(shù)中的this指向bind()函數(shù)的第一個(gè)參數(shù)。
2、傳給bind()的其他參數(shù)接收處理了,bind()之后返回的函數(shù)的參數(shù)也接收處理了,也就是說合并處理了。
3、并且bind()后的name為bound + 空格 + 調(diào)用bind的函數(shù)名。如果是匿名函數(shù)則是bound + 空格。
4、bind后的返回值函數(shù),執(zhí)行后返回值是原函數(shù)(original)的返回值。
5、bind函數(shù)形參(即函數(shù)的length)是1。bind后返回的bound函數(shù)形參不定,根據(jù)綁定的函數(shù)原函數(shù)(original)形參個(gè)數(shù)確定。
根據(jù)結(jié)論2:我們就可以簡單模擬實(shí)現(xiàn)一個(gè)簡版bindFn
//?第一版?修改this指向,合并參數(shù) Function.prototype.bindFn?=?function?bind(thisArg){if(typeof?this?!==?'function'){throw?new?TypeError(this?+?'must?be?a?function');}//?存儲函數(shù)本身var?self?=?this;//?去除thisArg的其他參數(shù)?轉(zhuǎn)成數(shù)組var?args?=?[].slice.call(arguments,?1);var?bound?=?function(){//?bind返回的函數(shù)?的參數(shù)轉(zhuǎn)成數(shù)組var?boundArgs?=?[].slice.call(arguments);//?apply修改this指向,把兩個(gè)函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果return?self.apply(thisArg,?args.concat(boundArgs));}return?bound; } //?測試 var?obj?=?{name:?'若川', }; function?original(a,?b){console.log(this.name);console.log([a,?b]); } var?bound?=?original.bindFn(obj,?1); bound(2);?//?'若川',?[1,?2]如果面試官看到你答到這里,估計(jì)對你的印象60、70分應(yīng)該是會有的。但我們知道函數(shù)是可以用new來實(shí)例化的。那么bind()返回值函數(shù)會是什么表現(xiàn)呢。
接下來看例子3:
從例子3種可以看出this指向了new bound()生成的新對象。
可以分析得出結(jié)論3:
1、bind原先指向obj的失效了,其他參數(shù)有效。
2、new bound的返回值是以original原函數(shù)構(gòu)造器生成的新對象。original原函數(shù)的this指向的就是這個(gè)新對象。另外前不久寫過一篇文章:面試官問:能否模擬實(shí)現(xiàn)JS的new操作符。簡單摘要:new做了什么:
1.創(chuàng)建了一個(gè)全新的對象。
2.這個(gè)對象會被執(zhí)行[[Prototype]](也就是__proto__)鏈接。
3.生成的新對象會綁定到函數(shù)調(diào)用的this。
4.通過new創(chuàng)建的每個(gè)對象將最終被[[Prototype]]鏈接到這個(gè)函數(shù)的prototype對象上。
5.如果函數(shù)沒有返回對象類型Object(包含F(xiàn)unctoin, Array, Date, RegExg, Error),那么new表達(dá)式中的函數(shù)調(diào)用會自動返回這個(gè)新的對象。
所以相當(dāng)于new調(diào)用時(shí),bind的返回值函數(shù)bound內(nèi)部要模擬實(shí)現(xiàn)new實(shí)現(xiàn)的操作。話不多說,直接上代碼。
//?第三版?實(shí)現(xiàn)new調(diào)用 Function.prototype.bindFn?=?function?bind(thisArg){if(typeof?this?!==?'function'){throw?new?TypeError(this?+?'?must?be?a?function');}//?存儲調(diào)用bind的函數(shù)本身var?self?=?this;//?去除thisArg的其他參數(shù)?轉(zhuǎn)成數(shù)組var?args?=?[].slice.call(arguments,?1);var?bound?=?function(){//?bind返回的函數(shù)?的參數(shù)轉(zhuǎn)成數(shù)組var?boundArgs?=?[].slice.call(arguments);var?finalArgs?=?args.concat(boundArgs);// new 調(diào)用時(shí),其實(shí)this instanceof bound判斷也不是很準(zhǔn)確。es6 new.target就是解決這一問題的。if(this?instanceof?bound){//?這里是實(shí)現(xiàn)上文描述的?new?的第?1,?2,?4?步//?1.創(chuàng)建一個(gè)全新的對象//?2.并且執(zhí)行[[Prototype]]鏈接// 4.通過`new`創(chuàng)建的每個(gè)對象將最終被`[[Prototype]]`鏈接到這個(gè)函數(shù)的`prototype`對象上。// self可能是ES6的箭頭函數(shù),沒有prototype,所以就沒必要再指向做prototype操作。if(self.prototype){//?ES5?提供的方案?Object.create()//?bound.prototype?=?Object.create(self.prototype);//?但?既然是模擬ES5的bind,那瀏覽器也基本沒有實(shí)現(xiàn)Object.create()//?所以采用?MDN?ployfill方案?https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/createfunction?Empty(){}Empty.prototype?=?self.prototype;bound.prototype?=?new?Empty();}//?這里是實(shí)現(xiàn)上文描述的?new?的第?3?步// 3.生成的新對象會綁定到函數(shù)調(diào)用的`this`。var?result?=?self.apply(this,?finalArgs);//?這里是實(shí)現(xiàn)上文描述的?new?的第?5?步//?5.如果函數(shù)沒有返回對象類型`Object`(包含`Functoin`,?`Array`,?`Date`,?`RegExg`,?`Error`),//?那么`new`表達(dá)式中的函數(shù)調(diào)用會自動返回這個(gè)新的對象。var?isObject?=?typeof?result?===?'object'?&&?result?!==?null;var?isFunction?=?typeof?result?===?'function';if(isObject?||?isFunction){return?result;}return?this;}else{//?apply修改this指向,把兩個(gè)函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果return?self.apply(thisArg,?finalArgs);}};return?bound; }面試官看到這樣的實(shí)現(xiàn)代碼,基本就是滿分了,心里獨(dú)白:這小伙子/小姑娘不錯(cuò)啊。不過可能還會問this instanceof bound不準(zhǔn)確問題。上文注釋中提到this instanceof bound也不是很準(zhǔn)確,ES6 new.target很好的解決這一問題,我們舉個(gè)例子4:
instanceof 不準(zhǔn)確,ES6 new.target很好的解決這一問題
function?Student(name){if(this?instanceof?Student){this.name?=?name;console.log('name',?name);}else{throw?new?Error('必須通過new關(guān)鍵字來調(diào)用Student。');} } var?student?=?new?Student('若'); var?notAStudent?=?Student.call(student,?'川');?//?不拋出錯(cuò)誤,且執(zhí)行了。 console.log(student,?'student',?notAStudent,?'notAStudent');function?Student2(name){if(typeof?new.target?!==?'undefined'){this.name?=?name;console.log('name',?name);}else{throw?new?Error('必須通過new關(guān)鍵字來調(diào)用Student2。');} } var?student2?=?new?Student2('若'); var?notAStudent2?=?Student2.call(student2,?'川'); console.log(student2,?'student2',?notAStudent2,?'notAStudent2');?//?拋出錯(cuò)誤細(xì)心的同學(xué)可能會發(fā)現(xiàn)了這版本的代碼沒有實(shí)現(xiàn)bind后的bound函數(shù)的nameMDN Function.name和lengthMDN Function.length。面試官可能也發(fā)現(xiàn)了這一點(diǎn)繼續(xù)追問,如何實(shí)現(xiàn),或者問是否看過es5-shim的源碼實(shí)現(xiàn)L201-L335。如果不限ES版本。其實(shí)可以用ES5的Object.defineProperties來實(shí)現(xiàn)。
Object.defineProperties(bound,?{'length':?{value:?self.length,},'name':?{value:?'bound?'?+?self.name,} });es5-shim的源碼實(shí)現(xiàn)bind
直接附上源碼(有刪減注釋和部分修改等)
var?$Array?=?Array; var?ArrayPrototype?=?$Array.prototype; var?$Object?=?Object; var?array_push?=?ArrayPrototype.push; var?array_slice?=?ArrayPrototype.slice; var?array_join?=?ArrayPrototype.join; var?array_concat?=?ArrayPrototype.concat; var?$Function?=?Function; var?FunctionPrototype?=?$Function.prototype; var?apply?=?FunctionPrototype.apply; var?max?=?Math.max; //?簡版?源碼更復(fù)雜些。 var?isCallable?=?function?isCallable(value){if(typeof?value?!==?'function'){return?false;}return?true; }; var?Empty?=?function?Empty()?{}; //?源碼是?defineProperties //?源碼是bind筆者改成bindFn便于測試 FunctionPrototype.bindFn?=?function?bind(that)?{var?target?=?this;if?(!isCallable(target))?{throw?new?TypeError('Function.prototype.bind?called?on?incompatible?'?+?target);}var?args?=?array_slice.call(arguments,?1);var?bound;var?binder?=?function?()?{if?(this?instanceof?bound)?{var?result?=?apply.call(target,this,array_concat.call(args,?array_slice.call(arguments)));if?($Object(result)?===?result)?{return?result;}return?this;}?else?{return?apply.call(target,that,array_concat.call(args,?array_slice.call(arguments)));}};var?boundLength?=?max(0,?target.length?-?args.length);var?boundArgs?=?[];for?(var?i?=?0;?i?<?boundLength;?i++)?{array_push.call(boundArgs,?'$'?+?i);}//?這里是Function構(gòu)造方式生成形參length?$1,?$2,?$3...bound?=?$Function('binder',?'return?function?('?+?array_join.call(boundArgs,?',')?+?'){?return?binder.apply(this,?arguments);?}')(binder);if?(target.prototype)?{Empty.prototype?=?target.prototype;bound.prototype?=?new?Empty();Empty.prototype?=?null;}return?bound; };你說出es5-shim源碼bind實(shí)現(xiàn),感慨這代碼真是高效、嚴(yán)謹(jǐn)。面試官心里獨(dú)白可能是:你就是我要找的人,薪酬福利你可以和HR去談下。
最后總結(jié)一下
1、bind是Function原型鏈中的Function.prototype的一個(gè)屬性,它是一個(gè)函數(shù),修改this指向,合并參數(shù)傳遞給原函數(shù),返回值是一個(gè)新的函數(shù)。
2、bind返回的函數(shù)可以通過new調(diào)用,這時(shí)提供的this的參數(shù)被忽略,指向了new生成的全新對象。內(nèi)部模擬實(shí)現(xiàn)了new操作符。
3、es5-shim源碼模擬實(shí)現(xiàn)bind時(shí)用Function實(shí)現(xiàn)了length。
事實(shí)上,平時(shí)其實(shí)很少需要使用自己實(shí)現(xiàn)的投入到生成環(huán)境中。但面試官通過這個(gè)面試題能考察很多知識。比如this指向,原型鏈,閉包,函數(shù)等知識,可以擴(kuò)展很多。
讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎指出。另外覺得寫得不錯(cuò),可以點(diǎn)個(gè)贊,也是對筆者的一種支持。
文章中的例子和測試代碼放在github中bind模擬實(shí)現(xiàn) github。bind模擬實(shí)現(xiàn) 預(yù)覽地址 F12看控制臺輸出,結(jié)合source面板查看效果更佳。
//?最終版?刪除注釋?詳細(xì)注釋版請看上文 Function.prototype.bind?=?Function.prototype.bind?||?function?bind(thisArg){if(typeof?this?!==?'function'){throw?new?TypeError(this?+?'?must?be?a?function');}var?self?=?this;var?args?=?[].slice.call(arguments,?1);var?bound?=?function(){var?boundArgs?=?[].slice.call(arguments);var?finalArgs?=?args.concat(boundArgs);if(this?instanceof?bound){if(self.prototype){function?Empty(){}Empty.prototype?=?self.prototype;bound.prototype?=?new?Empty();}var?result?=?self.apply(this,?finalArgs);var?isObject?=?typeof?result?===?'object'?&&?result?!==?null;var?isFunction?=?typeof?result?===?'function';if(isObject?||?isFunction){return?result;}return?this;}else{return?self.apply(thisArg,?finalArgs);}};return?bound; }推薦閱讀
我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準(zhǔn)備阿里P6/P7前端面試--項(xiàng)目經(jīng)歷準(zhǔn)備篇
大廠面試官常問的亮點(diǎn),該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經(jīng)驗(yàn),做的項(xiàng)目沒什么技術(shù)含量,怎么辦?
若川知乎高贊:有哪些必看的 JS庫?
末尾
你好,我是若川,江湖人稱菜如若川,歷時(shí)一年只寫了一個(gè)學(xué)習(xí)源碼整體架構(gòu)系列~(點(diǎn)擊藍(lán)字了解我)
關(guān)注若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學(xué)習(xí)
我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯(cuò),可以點(diǎn)個(gè)在看呀^_^另外歡迎留言交流~
精選前端好文,伴你不斷成長
若川原創(chuàng)文章精選!可點(diǎn)擊
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間【源碼精選】按鈕,歡迎點(diǎn)擊閱讀,也可以星標(biāo)我的公眾號,便于查找
總結(jié)
以上是生活随笔為你收集整理的面试官问:能否模拟实现JS的bind方法(高频考点)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HTML5期末大作业:家乡介绍网站设计—
- 下一篇: 2020 全球 JS 现状调查报告