用一個例子開始
greet = "Hi, Im global";
let foo = {
greet: "Hello"
};
let bar = function(name, character) {
console.log(this.greet);
return {
greet: this.greet,
person: `${name} is ${character}`
};
};
call() 方法
let result = bar.call(foo, "Lucy", "sexy");
console.log(result);
輸出
Hello
{ greet: ‘Hello’, person: ‘Lucy is sexy’ }
let result = bar.call(null, "Lucy", "sexy");
console.log(result);
Hi, Im global
{ greet: ‘Hi, Im global’, person: ‘Lucy is sexy’ }
思路
對於 bar.call(foo) 的情況,就好像 foo 對象調用了 bar 方法。
用代碼描述
let newFoo = {
greet: "Hello",
bar: function(name, sex) {
console.log(this.greet);
return {
greet: this.greet,
person: `${name} is ${sex}`
};
}
};
把 bar 方法搬進了 foo 對象,成了後者的一個屬性。
如何實現這種效果呢,先上結果,解析如下。
Function.prototype.fakeCall = function(context) {
context = context || global;
context.__fn__ = this;
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push(`arguments[${i}]`);
}
let result = eval(`context.__fn__(${args})`);
delete context.__fn__;
return result;
};
解析
如何模擬這個 newFoo 對象
bar.fakeCall(foo)
context 就是上面那行代碼中傳入的 foo 對象;
this 就是 fakeCall 函數的調用者,即 bar 函數。
給 foo 對象添加一個臨時的方法, 通過 this ,給它賦值爲 bar 函數 ,由此模擬了上面那個 newFoo 對象。
context.__fn__()
//相當於
newFoo.bar()
如何爲 bar 函數傳參
bar.fakeCall(foo, "Lucy", "sexy")
ECMAScript 中的參數在內部是用一個類數組來表示的。在函數體內,可以通過 arguments 對象來訪問這個類數組,從而獲得傳遞給函數的每一個參數。
call() 方法的第一個參數用來指定 this 綁定的對象,我們需要將從第二個開始的參數傳遞給 bar 函數。
for 循環獲取從 arguments[1]
到 arguments[arguments.length - 1]
的參數,用一個 args 數組來保存這些參數。
eval() 方法的作用
在熟悉 ES6 的情況下,既然已經獲得了參數數組 args ,就很容易想到在 context.__fn__()
中展開數組。
但事實上 ...args
這些都是語法糖,它的實現也用到了 apply()
方法。要模擬 call()
,用這些現成工具難免有取巧之嫌。
而用eval()
,可以利用 JavaScript 隱式類型轉換,先將數組 toString() 成字符串以後,填入下面這句的參數位置。再進行解析。
context.__fn__(arguments[1], arguments[2])
所以,上面獲取 args 數組的時候,不是直接獲取值,而是獲取個數。
args.push(`arguments[${i}]`); //正
args.push(arguments[i]); //誤
否則解析就會出問題,因爲將字符串參數 “qq” 作爲變量名來解析了。
bar.ffCall(foo, "qq", "ali")
context.fn(qq,ali)
^
ReferenceError: qq is not defined
at eval (eval at Function.ffCall (/Users/beijiyang/coding/goodJS/test.js:26:12), :1:16)
at Function.ffCall (/Users/beijiyang/coding/goodJS/test.js:26:12)
一些細節
全局對象
非嚴格模式下,如果第一個參數是 null
或 undefined
,則 this
指向全局對象。
console.log(bar.ffCall(null, "qq", "ali"));
Hi, Im global
{ greet: ‘Hi, Im global’, person: ‘qq is ali’ }
代碼中
context = context || global;
- 在 node 環境下,是
global
- 瀏覽器中是
window
嚴格模式
context = Object(context) || global
context 爲 null
或 undefined
時 Object(context)
返回空對象,不會被賦值爲global。
console.log(bar.ffCall(undefined, "qq", "ali"));
undefined
{ greet: undefined, person: ‘qq is ali’ }
防止原有屬性被覆蓋
如果傳入的對象原本就有 __fn__
屬性,那麼該屬性將會被覆蓋並刪除。
所以,可以增添一個取名函數,其中用 hasOwnProperty()
方法檢測目前屬性是否已經存在。
function getPropName(obj) {
let propName = Math.random();
if (obj.hasOwnProperty(propName)) {
getPropName(obj)
} else {
return propName
}
}
完整代碼如下
call() 方法的兩種實現
一
Function.prototype.fakeCall = function(context) {
context = context || global;
let __fn__ = getPropName(context);
context.__fn__ = this;
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push(`arguments[${i}]`);
}
let result = eval(`context.__fn__(${args})`);
delete context.__fn__;
return result;
};
二 ES6版
Function.prototype.fakeCallEs6 = function(context, ...args) {
context = context || global;
let __fn__ = getPropName(context);
context.__fn__ = this;
let result = context.__fn__(...args);
delete context.__fn__;
return result;
};
apply() 方法的實現
apply()
與 call()
的區別在於傳入參數的形式不同。
apply()
接受兩個參數,第一個參數指定了函數體內 this 對象的指向,第二個參數爲一個數組或類數組,apply()
方法把這個集合中的元素作爲參數傳遞給被調用的函數。
二者的實現很相似。
一
Function.prototype.fakeApply = function(context) {
context = context || global;
let __fn__ = getPropName(context);
context.__fn__ = this;
let result = eval(`context.__fn__(${arguments[1]})`);
delete context.__fn__;
return result;
};
二 ES6版
Function.prototype.fakeApplyEs6 = function(context, args) {
context = context || global;
let __fn__ = getPropName(context);
context.__fn__ = this;
let result = context.__fn__(...args);
delete context.__fn__;
return result;
};