用過React
的同窗都知道,常常會使用bind
來綁定this
。html
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
的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。html5
先看一下bind
是什麼。從上面的React
代碼中,能夠看出bind
執行後是函數,而且每一個函數均可以執行調用它。 眼見爲實,耳聽爲虛。讀者能夠在控制檯一步步點開例子1中的obj
:react
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
以後返回的函數,便於說明。git
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
複製代碼
一、調用bind
的函數中的this
指向bind()
函數的第一個參數。es6
二、傳給bind()
的其餘參數接收處理了,bind()
以後返回的函數的參數也接收處理了,也就是說合並處理了。github
三、而且bind()
後的name
爲bound + 空格 + 調用bind的函數名
。若是是匿名函數則是bound + 空格
。面試
四、bind
後的返回值函數,執行後返回值是原函數(original
)的返回值。segmentfault
五、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); // 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, '川'); // 不拋出錯誤,且執行了。
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
去談下。
一、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;
}
複製代碼
OshotOkill翻譯的 深刻理解ES6
簡體中文版 - 第三章 函數(雖然筆者是看的紙質書籍,但推薦下這本在線的書)
MDN Function.prototype.bind
冴羽: JavaScript深刻之bind的模擬實現
《react狀態管理與同構實戰》侯策:從一道面試題,到「我可能看了假源碼」
做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客
segmentfault
前端視野專欄,開通了前端視野專欄,歡迎關注~
掘金專欄,歡迎關注~
知乎前端視野專欄,開通了前端視野專欄,歡迎關注~
github blog,求個star
^_^~
可能比較有趣的微信公衆號,長按掃碼關注。也能夠加微信 lxchuan12
,註明來源,拉您進【前端視野交流羣】。