call/apply/bind
平常編碼中被開發者用來實現 「對象冒充」,也即 「顯示綁定 this
「。javascript
面試題:「call/apply/bind源碼實現」,事實上是對 JavaScript 基礎知識的一個綜合考覈。前端
相關知識點:java
this
;call/apply
的區別方式在於參數傳遞方式的不一樣;git
fn.call(obj, arg1, arg2, ...)
, 傳參數列表,以逗號隔開;fn.call(obj, [arg1, arg2, ...])
, 傳參數數組;bind
返回的是一個待執行函數,是函數柯里化的應用,而 call/apply
則是當即執行函數Function.prototype.myCall = function(context) { // 原型中 this 指向的是實例對象,因此這裏指向 [Function: bar] console.log(this); // [Function: bar] // 在傳入的上下文對象中,建立一個屬性,值指向方法 bar context.fn = this; // foo.fn = [Function: bar] // 調用這個方法,此時調用者是 foo,this 指向 foo context.fn(); // 執行後刪除它,僅使用一次,避免該屬性被其它地方使用(遍歷) delete context.fn; }; let foo = { value: 2 }; function bar() { console.log(this.value); } // bar 函數的聲明等同於:var bar = new Function("console.log(this.value)"); bar.call(foo); // 2;
初步思路有個大概,剩下的就是完善代碼。github
// ES6 版本 Function.prototype.myCall = function(context, ...params) { // ES6 函數 Rest 參數,使其可指定一個對象,接收函數的剩餘參數,合成數組 if (typeof context === 'object') { context = context || window; } else { context = Object.create(null); } // 用 Symbol 來做屬性 key 值,保持惟一性,避免衝突 let fn = Symbol(); context[fn] = this; // 將參數數組展開,做爲多個參數傳入 const result = context[fn](...params); // 刪除避免永久存在 delete(context[fn]); // 函數能夠有返回值 return result; } // 測試 var mine = { name: '以樂之名' } var person = { name: '無名氏', sayHi: function(msg) { console.log('個人名字:' + this.name + ',', msg); } } person.sayHi.myCall(mine, '很高興認識你!'); // 個人名字:以樂之名,很高興認識你!
知識點補充:面試
Symbol
,表示獨一無二的值;Object.create(null)
建立一個空對象// 建立一個空對象的方式 // eg.A let emptyObj = {}; // eg.B let emptyObj = new Object(); // eg.C let emptyObj = Object.create(null);
使用 Object.create(null)
建立的空對象,不會受到原型鏈的干擾。原型鏈終端指向 null
,不會有構造函數,也不會有 toString
、 hasOwnProperty
、valueOf
等屬性,這些屬性來自 Object.prototype
。有原型鏈基礎的夥伴們,應該都知道,全部普通對象的原型鏈都會指向 Object.prototype
。數組
因此 Object.create(null)
建立的空對象比其它兩種方式,更乾淨,不會有 Object
原型鏈上的屬性。app
ES5 版本:dom
Symobo
// ES5 版本 // 模擬Symbol function getSymbol(obj) { var uniqAttr = '00' + Math.random(); if (obj.hasOwnProperty(uniqAttr)) { // 若是已存在,則遞歸自調用函數 arguments.callee(obj); } else { return uniqAttr; } } Function.prototype.myCall = function() { var args = arguments; if (!args.length) return; var context = [].shift.apply(args); context = context || window; var fn = getSymbol(context); context[fn] = this; // 無其它參數傳入 if (!arguments.length) { return context[fn]; } var param = args[i]; // 類型判斷,否則 eval 運行會出錯 var paramType = typeof param; switch(paramType) { case 'string': param = '"' + param + '"' break; case 'object': param = JSON.stringify(param); break; } fnStr += i == args.length - 1 ? param : param + ','; // 藉助 eval 執行 var result = eval(fnStr); delete context[fn]; return result; } // 測試 var mine = { name: '以樂之名' } var person = { name: '無名氏', sayHi: function(msg) { console.log('個人名字:' + this.name + ',', msg); } } person.sayHi.myCall(mine, '很高興認識你!'); // 個人名字:以樂之名,很高興認識!
call
的源碼實現,那麼 apply
就簡單,二者只是傳遞參數方式不一樣而已。函數
Function.prototype.myApply = function(context, params) { // apply 與 call 的區別,第二個參數是數組,且不會有第三個參數 if (typeof context === 'object') { context = context || window; } else { context = Object.create(null); } let fn = Symbol(); context[fn] = this; const result context[fn](...params); delete context[fn]; return result; }
bind
與 call/apply
的區別就是返回的是一個待執行的函數,而不是函數的執行結果;bind
返回的函數做爲構造函數與 new
一塊兒使用,綁定的 this
須要被忽略;調用綁定函數時做爲this參數傳遞給目標函數的值。 若是使用new運算符構造綁定函數,則忽略該值。 —— MDN
Function.prototype.bind = function(context, ...initArgs) { // bind 調用的方法必定要是一個函數 if (typeof this !== 'function') { throw new TypeError('not a function'); } let self = this; let F = function() {}; F.prototype = this.prototype; let bound = function(...finnalyArgs) { // 將先後參數合併傳入 return self.call(this instanceof F ? this : context || this, ...initArgs, ...finnalyArgs); } bound.prototype = new F(); return bound; }
很多夥伴還會遇到這樣的追問,不使用 call/apply
,如何實現 bind
?
騷年先別慌,不用 call/apply
,不就是至關於把 call/apply
換成對應的自我實現方法,算是偷懶取個巧吧。
本篇 call/apply/bind
源碼實現,算是對以前文章系列知識點的一次加深鞏固。
「心中有碼,前路莫慌。」
參考文檔:
更多前端基石搭建,盡在 Github,期待 Star!
https://github.com/ZengLingYong/blog
做者:以樂之名 本文原創,有不當的地方歡迎指出。轉載請指明出處。