apply/call/bind 自我實現
- 2019 年 10 月 5 日
- 筆記
call/apply/bind
日常編碼中被開發者用來實現 「對象冒充」,也即 「顯示綁定this
「。 https://github.com/ZengLingYong/Blog/issues/30
面試題:「call/apply/bind源碼實現」,事實上是對 JavaScript 基礎知識的一個綜合考核。
相關知識點:
- 作用域;
- this 指向;
- 函數柯里化;
- 原型與原型鏈;
call/apply/bind 的區別
- 三者都可用於顯示綁定
this
; call/apply
的區別方式在於參數傳遞方式的不同;fn.call(obj, arg1, arg2, ...)
, 傳參數列表,以逗號隔開;fn.apply(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;
call 的源碼實現
初步思路有個大概,剩下的就是完善程式碼。
// 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, '很高興認識你!'); // 我的名字:以樂之名,很高興認識你!
知識點補充:
- ES6 新的原始數據類型
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
原型鏈上的屬性。
ES5 版本:
- 自行處理參數;
- 自實現
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, '很高興認識你!'); // 我的名字:以樂之名,很高興認識!
apply 的源碼實現
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 的源碼實現
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
源碼實現,算是對之前文章系列知識點的一次加深鞏固。
「心中有碼,前路莫慌。」
參考文檔:
- MDN – Function.prototype.bind()
- 不用call和apply方法模擬實現ES5的bind方法
