上一篇從一道面試題,到“我可能看了假源碼”中,由淺入深介紹了關(guān)于一篇經(jīng)典面試題的解法。
最后在皆大歡喜的結(jié)尾中,突生變化,懸念又起。這一篇,就是為了解開(kāi)這個(gè)懸念。
如果你還沒(méi)有看過(guò)前傳,可以參看前情回顧:
回顧1. 題目是模擬實(shí)現(xiàn)ES5中原生bind函數(shù);
回顧2. 我們通過(guò)4種遞進(jìn)實(shí)現(xiàn)達(dá)到了完美狀態(tài);
回顧3. 可是ES5-shim中的實(shí)現(xiàn),又讓我們大跌眼鏡...
ES5-shim的懸念
ES5-shim實(shí)現(xiàn)方式源碼貼在了最后,我們看看他做了什么奇怪的事情:
1)從結(jié)果上看,返回了bound函數(shù)。
2)bound函數(shù)是這樣子聲明的:
bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);
3)bound使用了系統(tǒng)自己的構(gòu)造函數(shù)Function來(lái)聲明,第一個(gè)參數(shù)是binder,函數(shù)體內(nèi)又binder.apply(this, arguments)。
我們知道這種動(dòng)態(tài)創(chuàng)建函數(shù)的方式,類(lèi)似eval。最好不要使用它,因?yàn)橛盟x函數(shù)比用傳統(tǒng)方式要慢得多。
4)那么ES5-shim抽風(fēng)了嗎?
追根問(wèn)底
答案肯定是沒(méi)抽風(fēng),他這樣做是有理由的。
神秘的函數(shù)的length屬性
你可能不知道,每個(gè)函數(shù)都有l(wèi)ength屬性。對(duì),就像數(shù)組和字符串那樣。函數(shù)的length屬性,用于表示函數(shù)的形參個(gè)數(shù)。更重要的是函數(shù)的length屬性值是不可重寫(xiě)的。我寫(xiě)了個(gè)測(cè)試代碼來(lái)證明:
function test (){}
test.length // 輸出0
test.hasOwnProperty('length') // 輸出true
Object.getOwnPropertyDescriptor('test', 'length')
// 輸出:
// configurable: false,
// enumerable: false,
// value: 4,
// writable: false
撥云見(jiàn)日
說(shuō)到這里,那就好解釋了。
ES5-shim是為了最大限度的進(jìn)行兼容,包括對(duì)返回函數(shù)length屬性的還原。如果按照我們之前實(shí)現(xiàn)的那種方式,length值始終為零。
所以:既然不能修改length的屬性值,那么在初始化時(shí)賦值總可以吧!
于是我們可通過(guò)eval和new Function的方式動(dòng)態(tài)定義函數(shù)來(lái)。
同時(shí),很有意思的是,源碼里有這樣的注釋?zhuān)?/p>
// 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.
他解釋了為什么要使用動(dòng)態(tài)函數(shù),就如同我們上邊所講的那樣,是為了保證length屬性的合理值。但是在一些瀏覽器中出于安全考慮,使用eval或者Function構(gòu)造器都會(huì)被拋出異常。但是,巧合也就是這些瀏覽器基本上都實(shí)現(xiàn)了bind函數(shù),這些異常又不會(huì)被觸發(fā)。
So, What a coincidence!
嘆為觀止
我們明白了這些,再看他的進(jìn)一步實(shí)現(xiàn):
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
這是為了保證調(diào)用的正確性,他使用了isCallable做判斷,isCallable很好實(shí)現(xiàn):
isCallable = function isCallable(value) {
if (typeof value !== 'function') {
return false;
}
}
重設(shè)綁定函數(shù)的length屬性:
var boundLength = max(0, target.length - args.length);
構(gòu)造函數(shù)調(diào)用情況,在binder中也有效兼容。如果你不明白什么是構(gòu)造函數(shù)調(diào)用情況,可以參考上一篇。
if (this instanceof bound) {
... // 構(gòu)造函數(shù)調(diào)用情況
} else {
... // 正常方式調(diào)用
}
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
// Clean up dangling references.
Empty.prototype = null;
}
無(wú)窮無(wú)盡
當(dāng)然,ES5-shim里還歸納了幾項(xiàng)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.
比較簡(jiǎn)單,我就不再翻譯了。
源碼回放
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;
}
總結(jié)
通過(guò)學(xué)習(xí)ES5-shim的源碼實(shí)現(xiàn)bind方法,結(jié)合前一篇,希望讀者能對(duì)bind和JS包括閉包,原型原型鏈,this等一系列知識(shí)點(diǎn)能有更深刻的理解。
同時(shí)在程序設(shè)計(jì)上,尤其是邏輯的嚴(yán)密性上,有所積累。
PS:百度知識(shí)搜索部大前端繼續(xù)招兵買(mǎi)馬,有意向者火速聯(lián)系。。。