從一道面試題,到“我可能看了假源碼[2]

上一篇從一道面試題,到“我可能看了假源碼”中,由淺入深介紹了關(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)系。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天想談?wù)勔坏狼岸嗣嬖囶},我做面試官的時(shí)候經(jīng)常喜歡用它來(lái)考察面試者的基礎(chǔ)是否扎實(shí),以及邏輯、思維能力和臨場(chǎng)表現(xiàn),題...
    LucasHC閱讀 9,058評(píng)論 28 45
  • 本文首發(fā)我的個(gè)人博客:前端小密圈,評(píng)論交流送1024邀請(qǐng)碼,嘿嘿嘿??。 來(lái)自朋友去某信用卡管家的做的一道面試題,用...
    微醺歲月閱讀 3,370評(píng)論 4 32
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,724評(píng)論 19 139
  • 請(qǐng)參看我github中的wiki,不定期更新。https://github.com/ivonzhang/Front...
    zhangivon閱讀 7,791評(píng)論 2 19
  • 山間小徑,在寂靜的午后,人跡更是寥落。草木無(wú)聲,只有在深叢之中傳出低低的蟲(chóng)鳴,清風(fēng)習(xí)習(xí),時(shí)而輕輕拂過(guò),晃動(dòng)著樹(shù)葉和...
    可可豆子閱讀 407評(píng)論 0 6

友情鏈接更多精彩內(nèi)容