面試官問:能否模擬實現bind
前言
用過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
複製程式碼
因此可以得出結論1:
1、bind
是Functoin
原型鏈中Function.prototype
的一個屬性,每個函式都可以呼叫它。
2、bind
本身是一個函式名為bind
的函式,返回值也是函式,函式名是bound
。(打出來就是bound加上一個空格
)。 知道了bind
是函式,就可以傳參,而且返回值'bound '
也是函式,也可以傳參,就很容易寫出例子2:
後文統一 bound
指原函式original
bind
之後返回的函式,便於說明。
var obj = {
name: '軒轅Rowboat',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // '軒轅Rowboat', [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: '軒轅Rowboat',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
}
var bound = original.bindFn(obj, 1);
bound(2); // '軒轅Rowboat', [1, 2]
複製程式碼
如果面試官看到你答到這裡,估計對你的印象60、70分應該是會有的。 但我們知道函式是可以用new
來例項化的。那麼bind()
返回值函式會是什麼表現呢。
接下來看例子3:
var obj = {
name: '軒轅Rowboat',
};
function original(a, b){
console.log('this', this); // original {}
console.log('typeof this', typeof this); // object
this.name = b;
console.log('name', this.name); // 2
console.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/create
function 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, 'Rowboat'); // 不丟擲錯誤,且執行了。
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, 'Rowboat');
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;
}
複製程式碼
參考
OshotOkill翻譯的 深入理解ES6
簡體中文版 - 第三章 函式(雖然筆者是看的紙質書籍,但推薦下這本線上的書)
MDN Function.prototype.bind
冴羽: JavaScript深入之bind的模擬實現
《react狀態管理與同構實戰》侯策:從一道面試題,到“我可能看了假原始碼”
關於
作者:常以軒轅Rowboat為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人部落格
segmentfault個人主頁
掘金個人主頁
知乎
github