JavaScript專題之模擬實現call和apply
JS 函式 call 和 apply 用來手動改變 this 的指向,call 和 apply 唯一的區別就在於函式引數的傳遞方式不同,call 是以逗號的形式,apply 是以陣列的形式:
let person1 = { name: "person1", say: function(age, sex) { console.log(this.name + ' age: ' + age + ' sex: ' + sex); } } let person2 = { name: "person" } person1.say.call(person2, 20, "男"); person1.say.apply(person2, [20, "男"]); 複製程式碼
本文就嘗試用其他方式來模擬實現 call 和 apply。
首先觀察 call 和 apply 有什麼特點?
- 被函式呼叫(函式也是物件),相當於 call 和 apply 是函式的屬性
- 如果沒有傳入需要 this 指向物件,那麼 this 指向全域性物件
- 函式執行了
- 最後都改變了 this 的指向
一、初步實現
基於 call 函式是呼叫函式的屬性的特點,call 的 this 指向呼叫函式,我們可以嘗試把呼叫函式的作為傳入的新物件的一個屬性,執行後,再刪除這個屬性就好了。
Function.prototype.newCall = function (context) { context.fn = this;// this 指的是 say 函式 context.fn(); delete context.fn; } var person = { name: "jayChou" }; var say = function() { console.log(this.name); } say.newCall(person);// jayChou 複製程式碼
是不是就初步模擬實現了 call 函式呢,由於 call 還涉及到傳參的問題,所以我們進入到下一環節。
二、eval 方式
在給物件臨時一個函式,並執行時,傳入的引數是除了 context 其餘的引數。那麼我們可以擷取 arguments 引數陣列的第一個後,將剩餘的引數傳入臨時陣列。
在前面我有講過函式 arguments 類陣列物件的特點,arguments 是不支援陣列的大多數方法, 但是支援for 迴圈來遍歷陣列。
Function.prototype.newCall = function (context) { context.fn = this; let args = []; for(let i=1; i< arguments.length; i++) { args.push('arguments[' + i + ']'); } // args => [arguments[1], arguments[2], arguments[3], ...] context.fn(args.join(','));// ??? delete context.fn; } var person = { name: "jayChou" }; var say = function(age, sex) { console.log(`name: ${this.name},age: ${age}, sex: ${sex}`); } say.newCall(person); 複製程式碼
上面傳遞引數的方式最後肯定是失敗的,我們可以嘗試 eval 的方式,將引數新增子函式的作用域中。
eval() 函式可計算某個字串,並執行其中的的 JavaScript 程式碼
Function.prototype.newCall = function (context) { context.fn = this; let args = []; for(var i=1; i< arguments.length; i++) { args.push('arguments[' + i + ']'); } // args => [arguments[1], arguments[2], arguments[3], ...] eval('context.fn(' + args + ')'); delete context.fn; } var person = { name: "jayChou" }; function say(age, sex) { console.log(`name: ${this.name},age: ${age}, sex: ${sex}`); } say.newCall(person, 18, '男');// name: jayChou,age: 18, sex: 男 複製程式碼
成功啦!
實現了函式引數的傳遞,那麼函式返回值怎麼處理呢。而且,如果傳入的物件是 null,又該如何處理?所以還需要再做一些工作:
Function.prototype.newCall = function (context) { if (typeof context === 'object') { context = context || window } else { context = Object.create(null); } context.fn = this; let args = []; for(var i=1; i< arguments.length; i++) { args.push('arguments[' + i + ']'); } // args => [arguments[1], arguments[2], arguments[3], ...] var result = eval('context.fn(' + args + ')');// 處理返回值 delete context.fn; return result;// 返回返回值 } var person = { name: "jayChou" }; function say(age, sex) { console.log(`name: ${this.name},age: ${age}, sex: ${sex}`); return age + sex; } var check = say.newCall(person, 18, '男'); console.log(check); // 18男 複製程式碼
判斷傳入物件的型別,如果為 null 就指向 window 物件。利用 eval 來執行字串程式碼,並返回字串程式碼執行的結果,就完成了模擬 call。 大功告成!
三、ES 6 實現
前面我們用的 eval 方式可以用 ES6 的解決還存在的一些問題,有沒有注意到,這段程式碼是有問題的。
context.fn = this; 複製程式碼
假如物件在被 call 呼叫前,已經有 fn 屬性怎麼辦?
ES6 中提供了一種新的基本資料型別,Symbol,表示獨一無二的值,另外,Symbol 作為屬性的時候,不能使用點運算子。所以再加上 ES 的 rest 剩餘引數替代 arguments 遍歷的工作就有:
Function.prototype.newCall = function (context,...params) { if (typeof context === 'object') { context = context || window } else { context = Object.create(null); } let fn = Symbol(); context[fn] = this var result = context[fn](...params); delete context.fn; return result; } var person = { name: "jayChou" }; function say(age, sex) { console.log(`name: ${this.name},age: ${age}, sex: ${sex}`); return age + sex; } var check = say.newCall(person, 18, '男'); console.log(check); // 18男 複製程式碼
四、apply
apply 和 call 的實現原理,基本類似,區別在於 apply 的引數是以陣列的形式傳入。
Function.prototype.newApply = function (context, arr) { if (typeof context === 'object') { context = context || window } else { context = Object.create(null); } context.fn = this; var result; if (!arr) {// 判斷函式引數是否為空 result = context.fn(); } else { var args = []; for (var i = 0; i < arr.length; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')'); } delete context.fn; return result; } 複製程式碼
es6 實現
Function.prototype.newApply = function(context, parameter) { if (typeof context === 'object') { context = context || window } else { context = Object.create(null) } let fn = Symbol() context[fn] = this; var result = context[fn](...parameter); delete context[fn]; return result; } 複製程式碼
總結
本文通過原生 JS 的 ES5 的方法和 ES 6 的方法模擬實現了 call 和 apply 的原理,旨在深入瞭解這兩個方法的用法和區別,希望你能有所收穫。
歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。
