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