上一篇從一道面試題的進階,到“我可能看了假原始碼”中,由淺入深介紹了關於一篇經典面試題的解法。

最後在皆大歡喜的結尾中,突生變化,懸念又起。這一篇,就是為了解開這個懸念。

如果你還沒有看過前傳,可以參看前情回顧:

回顧1。 題目是模擬實現ES5中原生bind函式;

回顧2。 我們透過4種遞進實現達到了完美狀態;

回顧3。 可是ES5-shim中的實現,又讓我們大跌眼鏡…

ES5-shim的懸念

ES5-shim實現方式原始碼貼在了最後,我們看看他做了什麼奇怪的事情:

1)從結果上看,返回了bound函式。

2)bound函式是這樣子宣告的:

bound = Function(‘binder’, ‘return function (’ + boundArgs。join(‘,’) + ‘){ return binder。apply(this, arguments); }’)(binder);

3)bound使用了系統自己的建構函式Function來宣告,第一個引數是binder,函式體內又binder。apply(this, arguments)。

我們知道這種動態建立函式的方式,類似eval。最好不要使用它,因為用它定義函式比用傳統方式要慢得多。

4)那麼ES5-shim抽風了嗎?

追根問底

答案肯定是沒抽風,他這樣做是有理由的。

神秘的函式的length屬性

你可能不知道,每個函式都有length屬性。對,就像陣列和字串那樣。函式的length屬性,用於表示函式的形參個數。更重要的是函式的length屬性值是不可重寫的。我寫了個測試程式碼來證明:

function test (){}

test。length // 輸出0

test。hasOwnProperty(‘length’) // 輸出true

Object。getOwnPropertyDescriptor(‘test’, ‘length’)

// 輸出:

// configurable: false,

// enumerable: false,

// value: 4,

// writable: false

撥雲見日

說到這裡,那就好解釋了。

ES5-shim是為了最大限度的進行相容,包括對返回函式length屬性的還原。如果按照我們之前實現的那種方式,length值始終為零。

所以:既然不能修改length的屬性值,那麼在初始化時賦值總可以吧!

於是我們可透過eval和new Function的方式動態定義函式來。

同時,很有意思的是,原始碼裡有這樣的註釋:

// XXX Build a dynamic function with desired amount of arguments is the only

// way to set the length property of a function。

// In environments where Content Security Policies enabled (Chrome extensions,

// for ex。) all use of eval or Function costructor throws an exception。

// However in all of these environments Function。prototype。bind exists

// and so this code will never be executed。

他解釋了為什麼要使用動態函式,就如同我們上邊所講的那樣,是為了保證length屬性的合理值。但是在一些瀏覽器中出於安全考慮,使用eval或者Function構造器都會被丟擲異常。但是,巧合也就是這些瀏覽器基本上都實現了bind函式,這些異常又不會被觸發。

So, What a coincidence!

歎為觀止

我們明白了這些,再看他的進一步實現:

if (!isCallable(target)) {

throw new TypeError(‘Function。prototype。bind called on incompatible ’ + target);

}

這是為了保證呼叫的正確性,他使用了isCallable做判斷,isCallable很好實現:

isCallable = function isCallable(value) {

if (typeof value !== ‘function’) {

return false;

}

}

重設繫結函式的length屬性:

var boundLength = max(0, target。length - args。length);

建構函式呼叫情況,在binder中也有效相容。如果你不明白什麼是建構函式呼叫情況,可以參考上一篇。

if (this instanceof bound) {

。。。 // 建構函式呼叫情況

} else {

。。。 // 正常方式呼叫

}

if (target。prototype) {

Empty。prototype = target。prototype;

bound。prototype = new Empty();

// Clean up dangling references。

Empty。prototype = null;

}

無窮無盡

當然,ES5-shim裡還歸納了幾項todo…

// TODO

// 18。 Set the [[Extensible]] internal property of F to true。

// 19。 Let thrower be the [[ThrowTypeError]] function Object (13。2。3)。

// 20。 Call the [[DefineOwnProperty]] internal method of F with

// arguments “caller”, PropertyDescriptor {[[Get]]: thrower, [[Set]]:

// thrower, [[Enumerable]]: false, [[Configurable]]: false}, and

// false。

// 21。 Call the [[DefineOwnProperty]] internal method of F with

// arguments “arguments”, PropertyDescriptor {[[Get]]: thrower,

// [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},

// and false。

// 22。 Return F。

比較簡單,我就不再翻譯了。

原始碼回放

bind: 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 = target。apply(

this,

array_concat。call(args, array_slice。call(arguments))

);

if ($Object(result) === result) {

return result;

}

return this;

} else {

return target。apply(

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);

}

bound = Function(‘binder’, ‘return function (’ + boundArgs。join(‘,’) + ‘){ 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方法,結合前一篇,希望讀者能對bind和JS包括閉包,原型原型鏈,this等一系列知識點能有更深刻的理解。

同時在程式設計上,尤其是邏輯的嚴密性上,有所積累。

PS:百度知識搜尋部大前端繼續招兵買馬,有意向者火速聯絡。。。

本文作者:留學法國的帥哥,國家二級運動員,百度高階FE,快來關注 Lucas HC - 知乎

原文連結:從一道面試題的進階,到“我可能看了假原始碼”(2)

微信公眾號

關注微信公眾號:顏海鏡,最新博文優先推送,不再錯過精彩內容。