用一个例子开始
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;
};