最近在幫朋友複習 JS 相關的基礎知識,遇到不會的問題,她就會來問我。
這不是很簡單?三下五除二,分分鐘解決。
function bind(fn, obj, ...arr) {
return fn.apply(obj, arr)
}
於是我就將這段程式碼發了過去
這時候立馬被女朋友進行了一連串的靈魂拷問。
這個時候,我馬老師就坐不住了,我不服氣,我就去複習了一下 bind,發現太久不寫基礎程式碼,還是會需要一點時間複習,這一次我得寫一個有深度的 bind,深的馬老師的真傳,給他分成了五層速記法。
第一層 - 繫結在原型上的方法
這一層非常的簡單,得益於 JS 原型鏈的特性。由於 function xxx 的原型鏈 指向的是 Function.prototype
, 因此我們在呼叫 xxx.bind 的時候,呼叫的是 Function.prototype 上的方法。
Function.prototype._bind = function() {}
這樣,我們就可以在一個建構函式上直接呼叫我們的bind方法啦~例如像這樣。
funciton myfun(){}
myfun._bind();
想要詳細理解這方面的可以看這張圖和這篇文章(https://github.com/mqyqingfeng/blog/issues/2)
第二層 - 改變 this 的指向
這可以說是 bind 最核心的特性了,就是改變 this 的指向,並且返回一個函式。而改變 this , 我們可以通過已知的 apply 和 call 來實現,這裡我們就暫且使用 apply 來進行模擬。首先通過 self
來儲存當前 this,也就是傳入的函式。因為我們知道 this 具有 隱式繫結
的規則(摘自 《你不知道的JavaScript(上)》2.2.2 ),
function foo() {console.log(this.a)}
var obj = {a: 2, foo};
obj.foo(); // 2
通過以上特性,我們就可以來寫我們的 _bind 函式。
Function.prototype._bind = function(thisObj) {
const self = this;
return function () {
self.apply(thisObj);
}
}
var obj = {a:1}
function myname() {console.log(this.a)}
myname._bind(obj)(); // 1
可能很多朋友都止步於此了,因為在一般的面試中,特別是一些校招面試中,可能你只需要知道前面兩個就差不多了。但是想要在面試中驚豔所有人,仍然是不夠的,接下來我們繼續我們的探索與研究。
第三層 - 支援柯里化
函式柯里化是一個老生常談的話題,在這裡再複習一下。
function fn(x) {
return function (y) {
return x + y;
}
}
var fn1 = fn(1);
fn1(2) // 3
不難發現,柯里化使用了閉包,當我們執行 fn1 的時候,函式內使用了外層函式的 x, 從而形成了閉包。
而我們的 bind 函式也是類似,我們通過獲取當前外部函式的 arguments
,並且去除了繫結的物件,儲存成變數 args
,最後 return
的方法,再一次獲取當前函式的 arguments
, 最終用 finalArgs
進行了一次合併。
Function.prototype._bind = function(thisObj) {
const self = this;
const args = [...arguments].slice(1)
return function () {
const finalArgs = [...args, ...arguments]
self.apply(thisObj, finalArgs);
}
}
通過以上程式碼,讓我們 bind 方法,越來越健壯了。
var obj = { i: 1}
function myFun(a, b, c) {
console.log(this.i + a + b + c);
}
var myFun1 = myFun._bind(obj, 1, 2);
myFun1(3); // 7
一般到了這層,可以說非常棒了,但是再堅持一下下,就變成了完美的答卷。
第四層 - 考慮 new 的呼叫
要知道,我們的方法,通過 bind 繫結之後,依然是可以通過 new 來進行例項化的, new
的優先順序會高於 bind
(摘自 《你不知道的JavaScript(上)》2.3 優先順序)。
這一點我們通過原生 bind 和我們第四層的 _bind 來進行驗證對比。
// 原生
var obj = { i: 1}
function myFun(a, b, c) {
// 此處用new方法,this指向的是當前函式 myFun
console.log(this.i + a + b + c);
}
var myFun1 = myFun.bind(obj, 1, 2);
new myFun1(3); // NAN
// 第四層的 bind
var obj = { i: 1}
function myFun(a, b, c) {
console.log(this.i + a + b + c);
}
var myFun1 = myFun._bind(obj, 1, 2);
new myFun1(3); // 7
注意,這裡使用的是 bind
方法
因此我們需要在 bind 內部,對 new 的進行處理。而 new.target
屬性,正好是用來檢測構造方法是否是通過 new
運算子來被呼叫的。
接下來我們還需要自己實現一個 new ,
而根據
MDN
,new
關鍵字會進行如下的操作:1.建立一個空的簡單JavaScript物件(即
{}
);2.連結該物件(設定該物件的constructor)到另一個物件 ;
3.將步驟1新建立的物件作為
this
的上下文 ;4.如果該函式沒有返回物件,則返回
this
。
Function.prototype._bind = function(thisObj) {
const self = this;
const args = [...arguments].slice(1);
return function () {
const finalArgs = [...args, ...arguments];
// new.target 用來檢測是否是被 new 呼叫
if(new.target !== undefined) {
// this 指向的為建構函式本身
var result = self.apply(this, finalArgs);
// 判斷改函式是否返回物件
if(result instanceof Object) {
return reuslt;
}
// 沒有返回物件就返回 this
return this;
} else {
// 如果不是 new 就原來的邏輯
return self.apply(thisArg, finalArgs);
}
}
}
看到這裡,你的造詣已經如火純情了,但是最後還有一個小細節。
第五層 - 保留函式原型
以上的方法在大部分的場景下都沒有什麼問題了,但是,當我們的建構函式有 prototype 屬性的時候,就出問題啦。因此我們需要給 prototype 補上,還有就是呼叫物件必須為函式。
Function.prototype._bind = function (thisObj) {
// 判斷是否為函式呼叫
if (typeof target !== 'function' || Object.prototype.toString.call(target) !== '[object Function]') {
throw new TypeError(this + ' must be a function');
}
const self = this;
const args = [...arguments].slice(1);
var bound = function () {
var finalArgs = [...args, ...arguments];
// new.target 用來檢測是否是被 new 呼叫
if (new.target !== undefined) {
// 說明是用new來呼叫的
var result = self.apply(this, finalArgs);
if (result instanceof Object) {
return result;
}
return this;
} else {
return self.apply(thisArg, finalArgs);
}
};
if (self.prototype) {
// 為什麼使用了 Object.create? 因為我們要防止,bound.prototype 的修改而導致self.prototype 被修改。不要寫成 bound.prototype = self.prototype; 這樣可能會導致原函式的原型被修改。
bound.prototype = Object.create(self.prototype);
bound.prototype.constructor = self;
}
return bound;
};
以上就是一個比較完整的 bind 實現了,如果你想了解更多細節的實踐,可以檢視。(也是 MDN 推薦的)
https://github.com/Raynos/function-bind
結語
** ❤️關注+點贊+收藏+評論+轉發❤️ **,原創不易,鼓勵筆者創作更好的文章
關注公眾號秋風的筆記
,一個專注於前端面試、工程化、開源的前端公眾號