用過React
的同窗都知道,常常會使用bind
來綁定this
。javascript
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
到底作了什麼,怎麼模擬實現呢。html
附上以前寫文章寫過的一段話:已經有不少模擬實現
bind
的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。html5
先看一下bind
是什麼。從上面的React
代碼中,能夠看出bind
執行後是函數,而且每一個函數均可以執行調用它。 眼見爲實,耳聽爲虛。讀者能夠在控制檯一步步點開例子1中的obj
:java
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
一、bind
是Functoin
原型鏈中Function.prototype
的一個屬性,每一個函數均可以調用它。
二、bind
自己是一個函數名爲bind
的函數,返回值也是函數,函數名是bound
。(打出來就是bound加上一個空格
)。 知道了bind
是函數,就能夠傳參,並且返回值'bound '
也是函數,也能夠傳參,就很容易寫出例子2:
後文統一 bound
指原函數original
bind
以後返回的函數,便於說明。react
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
一、調用bind
的函數中的this
指向bind()
函數的第一個參數。git
二、傳給bind()
的其餘參數接收處理了,bind()
以後返回的函數的參數也接收處理了,也就是說合並處理了。es6
三、而且bind()
後的name
爲bound + 空格 + 調用bind的函數名
。若是是匿名函數則是bound + 空格
。github
四、bind
後的返回值函數,執行後返回值是原函數(original
)的返回值。面試
五、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()
生成的新對象。
一、bind
原先指向obj
的失效了,其餘參數有效。
二、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
去談下。
一、bind
是Function
原型鏈中的Function.prototype
的一個屬性,它是一個函數,修改this
指向,合併參數傳遞給原函數,返回值是一個新的函數。
二、bind
返回的函數能夠經過new
調用,這時提供的this
的參數被忽略,指向了new
生成的全新對象。內部模擬實現了new
操做符。
三、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; }