上一篇從一道面試題的進階,到“我可能看了假原始碼”中,由淺入深介紹了關於一篇經典面試題的解法。
最後在皆大歡喜的結尾中,突生變化,懸念又起。這一篇,就是為了解開這個懸念。
如果你還沒有看過前傳,可以參看前情回顧:
回顧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)
微信公眾號
關注微信公眾號:顏海鏡,最新博文優先推送,不再錯過精彩內容。