1. 程式人生 > 實用技巧 >淺析這句經常在框架中出現的JS程式碼加深對bind的理解

淺析這句經常在框架中出現的JS程式碼加深對bind的理解

  call、bind這類方法我們雖然在平時開發中用到的不多,但是在看框架原始碼時,我們會經常看到。比如我們經常在框架級的原始碼中看到類似如下的一句程式碼:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

  在這一句程式碼中既使用call方法,同時也使用bind方法,乍看之下,有點暈!這到底是想幹嘛?

  無妨,我們呼叫看看,傳入不同的型別試試,效果如下:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);
console.log(toStr1({}));      
// "[object Object]" console.log(toStr1([])); // "[object Array]" console.log(toStr1(123)); // "[object Number]" console.log(toStr1("abc")); // "[object String]" console.log(toStr1("abc")); // "[object String]" console.log(toStr1(new Date));// "[object Date]"

  從結果中可以看到該方法的主要功能是用於檢測物件的型別。但通常型別檢測,我們可能更多地看到如下程式碼實現:

var toStr2 = obj => Object.prototype.toString.call(obj);
console.log(toStr2({}));      // "[object Object]"
console.log(toStr2([]));      // "[object Array]"
console.log(toStr2(123));     // "[object Number]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2(new Date));// "[object Date]"

  第二種方法更簡潔,僅僅使用一次call就能獲得我們想要的功能,且程式碼邏輯清晰,理解起來更加容易,可在眾多框架中為何更多使用第一種呢?

  其實主要的原因是防止原型汙染。

  比如我們在業務程式碼中覆寫了Object.prototype.toString方法,第二種寫法將得不到正確的結果,而第一種寫法仍然可以。我們用程式碼來來試試:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

var toStr2 = obj => Object.prototype.toString.call(obj);

Object.prototype.toString = function(){
 return'toString方法被覆蓋!';
}
// 接著我們再呼叫上述方法

// toStr1呼叫結果如下:
console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

// toStr2呼叫結果如下:
console.log(toStr2({}));      // "toString方法被覆蓋!"
console.log(toStr2([]));      // "toString方法被覆蓋!"
console.log(toStr2(123));     // "toString方法被覆蓋!"
console.log(toStr2("abc"));   // "toString方法被覆蓋!"
console.log(toStr2("abc"));   // "toString方法被覆蓋!"
console.log(toStr2(new Date));// "toString方法被覆蓋!"

  結果很明顯。第一種方法仍然能正確得到結果,而第二種則不行!那麼為什麼會這樣呢?

  我們知道bind函式返回結果是一個函式,這個函式是函式內部的函式,會被延遲執行,那麼很自然聯想到這裡可能存在閉包!不過在現代版瀏覽器中call和bind都已經被js引擎內部實現了,我們沒有辦法除錯!但是我們可以通過polly-fill提供的近似實現的原始碼來理解引擎內部的邏輯,下面是個簡單的demo:

// 模擬實現call
// ES6實現
Function.prototype.mycall = function (context) {
 context = context ? Object(context) : window;
 var fn = Symbol();
 context[fn] = this;

 let args = [...arguments].slice(1);
 let result = context[fn](...args);

 delete context[fn]
 return result;
}
// 模擬實現bind
Function.prototype.mybind = function (context) {
 if (typeof this !== "function") {
   throw new Error("請使用函式物件呼叫我,謝謝!");
}

 var self = this;
 var args = Array.prototype.slice.call(arguments, 1);

 var fNOP = function () { }; // 注意這個是用來處理new的特性

 var fBound = function () {
   var bindArgs = Array.prototype.slice.call(arguments);
   return self.myapply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}

 fNOP.prototype = this.prototype;
 fBound.prototype = new fNOP();
 return fBound;
}
// 模擬實現apply
// ES6實現
Function.prototype.myapply = function (context, arr) {
 context = context ? Object(context) : window;
 context.fn = this;
 let result;
 if (!arr) {
   result = context.fn();
} else {
   result = context.fn(...arr);
}

 delete context.fn
 return result;
}
var toStr1 = Function.prototype.mycall.mybind(Object.prototype.toString);

console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

  上述的實現略去一些健壯性的程式碼,僅保留核心邏輯,具體的實現細節這裡不做解釋,有興趣的可以自己研究,從devtools我們看到mybind形成的閉包確實在函式物件toStr1的作用域上!當然如果你對原型鏈有深刻理解的話,其實這句有趣的程式碼還可以寫成如下方式:

var toStr3 = Function.call.bind(Object.prototype.toString);
var toStr4 = Function.call.call.bind(Object.prototype.toString);
var toStr5 = Function.call.call.call.bind(Object.prototype.toString);

  其實這個程式碼拆分出來看就好理解了:

1、Function.prototype.call表示呼叫函式原型上的call函式,跟呼叫Array.prototype.slice的方式沒什麼區別,都是調的原型上的方法;

2、(Function.prototype.call).bind(Object.prototype.toString); 這樣看只是,給call方法做了個bind而已,改變了call函式的this指向為toString;

  作個假設,call函式內部通過呼叫this取得呼叫的函式,比如Array.prototype.slice.call則call內部this可以取得slice,

3、同理,Function.prototype.call 內部通過bind繫結,已經將this上下文綁定了成了toString,文中toStr1({}) 只不過在使用call({}),並且是提前綁定了this上下文為Object.prototype.toString,等價於Object.prototype.toString.call({})。

  可能你會疑惑為什麼不寫成Function.prototype.call(Object.prototype.toString, {}),別忘了call函式本身第一個引數就是作為引數使用,就像Array.prototype.slice.call({0:'1',1:'2',length:2})這個一樣,根本無法改變到call的this,而是改變的slice的this,所以才會出現(Function.prototype.call).bind(Object.prototype.toString)這種寫法,通過bind去改變call的this為Object.prototype.toString,這樣,就等同於使用(Object.prototype.toString).call了,因為此時call的this指向了Object.prototype.toString。這樣用法實際都是設計模式種鴨子模式的使用,只要長的像鴨子,叫聲像鴨子,那麼就可以當成鴨子用,這也是為什麼Array.prototype.slice.call({0:'1',1:'2',length:2}),能夠轉化類陣列的原因,因為有索引,有length,slice內部的this就能像運算元組的索引和length一樣去取屬性,自然也就能正常執行下去。

  其實就一句話:bind形參剛開始 和 object.prototype.tostring 指向同一塊堆,後來改變了object.prototype.tostring指向,但是bind形參沒有改變。

1、call,apply是動態的改變this的指向,即換個物件執行原物件方法的方法,並立即執行;

2、bind是靜態改變this的指向,並返回一個修改後的函式。靜態指向之後就不會改變,除非new。

  關於call、apply、bind的一些相關知識,可以詳見我之前總結的部落格:

  原生JS實現bind()函式

  深入理解this和call、bind、apply對this的影響及用法

  JavaScript中的bind方法及其常見應用