前言:
原文首發於我的部落格,說實話,這半年來在各大社群看別人分享的面試題中 bind 函式已經出現 n 多次了,這次準備詳細探究下
首先讓我們看看 mdn 對於 bind 函式的描述是什麼
語法
fun.bind(thisArg[, arg1[, arg2[, ...]]])
引數
- thisArg 當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用 new 操作符呼叫繫結函式時,該引數無效。
- arg1, arg2, ... 當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法。
返回值
返回由指定的 this 值和初始化引數改造的原函式拷貝
當程式碼 new Foo(...) 執行時,會發生以下事情: 1、一個繼承自 Foo.prototype 的新物件被建立。 2、使用指定的引數呼叫建構函式 Foo ,並將 this 繫結到新建立的物件。new Foo 等同於 new Foo(),也就是沒有指定引數列表,Foo 不帶任何引數呼叫的情況。 3、由建構函式返回的物件就是 new 表示式的結果。如果建構函式沒有顯式返回一個物件,則使用步驟 1 建立的物件。(一般情況下,建構函式不返回值,但是使用者可以選擇主動返回物件,來覆蓋正常的物件建立步驟)如果你看不懂這段話,沒關係,看完下面這段程式碼你就清楚了
function Foo(){
}
下面程式碼就是執行new Foo()時的簡單實現
let obj = {};
obj.__proto__ = Foo.prototype
return Foo.call(obj)
複製程式碼
對於new的完整實現可以參考這位大神的部落格
實現
乞丐版, 無法預先填入引數,僅實現執行時改變 this 指向
let obj = {
ll: 'seve'
};
Function.prototype.bind = function(that) {
var self = this;
return function() {
return self.apply(that, arguments);
};
};
let func0 = function(a, b, c) {
console.log(this.ll);
console.log([a, b, c]);
}.bind(obj, 1, 2);
func0(3); // seve
// [ 3, undefined, undefined ] 發現1,2並沒有填入
複製程式碼
乞丐版也太 low 了對吧,所以我們繼續完善
es6 進階版
es6 提供了結構運算子,可以很方便的利用其功能實現 bind
Function.prototype.bind = function(that, ...argv) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not callable`);
}
// 儲存原函式
let self = this;
// 獲取bind後函式傳入的引數
return function(...argu) {
return self.apply(that, [...argv, ...argu]);
};
};
let func1 = function(a, b, c) {
console.log(this.ll);
console.log([a, b, c]);
}.bind(obj, 1, 2);
func1(3); // seve
// [ 1, 2, 3 ]
複製程式碼
es6 版實現很簡單對吧,但是面試官說我們的執行環境是 es5,這時你心中竊喜,bable 大法好,但是你可千萬不要說有 babel,因為面試官的意圖不太可能是問你 es6 如何轉換成 es5,而是考察你其他知識點,比如下面的類陣列如何轉換為真正的陣列
es5 進階版
Function.prototype.bind = function() {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not callable`);
}
var self = this;
var slice = [].slice;
// 模擬es6的解構效果
var that = arguments[0];
var argv = slice.call(arguments, 1);
return function() {
// slice.call(arguments, 0)將類陣列轉換為陣列
return self.apply(that, argv.concat(slice.call(arguments, 0)));
};
};
let func2 = function(a, b, c) {
console.log(this.ll);
console.log([a, b, c]);
}.bind(obj, 1, 2);
func2(3); // seve
// [ 1, 2, 3 ]
複製程式碼
當然,寫到這裡,對於絕大部分面試,這份程式碼都是一份不錯的答案,但是為了給面試官留下更好的印象,我們需要上終極版 實現完整的bind函式,這樣還可以跟面試官吹一波
終極版
為了當使用new操作符時,bind後的函式不丟失this。我們需要把bind前的函式的原型掛載到bind後函式的原型上
但是為了修改bind後函式的原型而對bind前的原型不產生影響,都是物件惹的禍。。。直接賦值只是賦值物件在堆中的地址 所以需要把原型繼承給bind後的函式,而不是直接賦值,我有在一些地方看到說Object.crate可以實現同樣的效果,有興趣的可以瞭解一下,但是我自己試了下,發現效果並不好,new 操作時this指向錯了(可能是我使用姿勢錯了)
通過直接賦值的效果
Function.prototype.bind = function(that, ...argv) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not callable`);
}
// 儲存原函式
let self = this;
let func = function() {};
// 獲取bind後函式傳入的引數
let bindfunc = function(...arguments) {
return self.apply(this instanceof func ? this : that, [...argv, ...arguments]);
};
// 把this原型上的東西掛載到func原型上面
// func.prototype = self.prototype;
// 為了避免func影響到this,通過new 操作符進行復制原型上面的東西
bindfunc.prototype = self.prototype;
return bindfunc;
};
function bar() {
console.log(this.ll);
console.log([...arguments]);
}
let func3 = bar.bind(null);
func3.prototype.value = 1;
console.log(bar.prototype.value) // 1 可以看到bind後的原型對bind前的原型產生的同樣的影響
複製程式碼
通過繼承賦值的效果
Function.prototype.bind = function(that, ...argv) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not callable`);
}
// 儲存原函式
let self = this;
let func = function() {};
// 獲取bind後函式傳入的引數
let bindfunc = function(...argu) {
return self.apply(this instanceof func ? this : that, [...argv, ...argu]);
};
// 把this原型上的東西掛載到func原型上面
func.prototype = self.prototype;
// 為了避免func影響到this,通過new 操作符進行復制原型上面的東西
bindfunc.prototype = new func();
return bindfunc;
};
function bar() {
console.log(this.ll);
console.log([...arguments]);
}
let func3 = bar.bind(null);
func3.prototype.value = 1;
console.log(bar.prototype.value) // undefined 可以看到bind後的原型對bind前的原型不產生影響
func3(5); // seve
// [ 5 ]
new func3(5); // undefined
// [ 5 ]
複製程式碼
以上程式碼或者表述如有錯誤或者不嚴謹的地方,歡迎指出,或者在評論區討論,覺得我的文章有用的話,可以訂閱或者star支援我的部落格
下系列文章我打算寫關於koa框架的實現,第一篇我會帶大家探究Object.create的效果及實現