面試官問:能否模擬實現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
指向的就是這個新物件。 另外前不久寫過一篇文章: ofollow,noindex">面試官問:能否模擬實現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
JavaScript%2FReference%2FGlobal_Objects%2FFunction%2Fname" rel="nofollow,noindex">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; } 複製程式碼