本文共 1320 字,讀完只需 5 分鐘
概述
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 的原理,旨在深入瞭解這兩個方法的用法和區別,希望你能有所收穫。
歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。