前段时间看了冴羽github上call与apply的模拟实现,文章由浅入深将整个实现的过程循序渐进地完善道来, 这里记录一下我学习过之后的一些总结:
整个模拟实现的过程分为三步:
第一步: 完成一个初步的call模拟调用过程;
第二步: 完善入参的传递与调用;
第三步: 补全调用对象缺失时的状况,代码实现apply调用;
第一步模拟调用的原理:
根据call的定义: call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法;
看一个简单的例子
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1 这里的bar方法被调用,在调用时this的指向为foo
通过上面的例子,其实我们可以得到模拟实现的原理分以下三步:
1.将bar 挂载到foo上 foo.fn = bar;
2.执行foo调用bar方法 foo.fn();
3.删除foo的bar方法属性 delete foo.fn;
根据这三步,我们就得到了如下模拟实现的第一版代码
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this; //1.挂载
context.fn(); //2. 调用
delete context.fn; //3.删除
}
// 测试一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
这里值得注意的是call2方法的第一个参数是当前调用的上下文环境, context对应foo对应, 方法里面的this 对应当前调用方法bar.
接下来的第二步, 是模拟实现当调用方法有传参时,传参的实现:
这意味着在第一版的实现的基础上我们要加入如下的将所有参数传递给fn的一个传参过程,
context.fn(arguments[1], arguments[2], ...);
由于arguments这个类数组对象是不定长的所以我们需要写一个for循环来把所有参数枚举出来之后传过去,
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) { // 这里i的初始值为1,是因为调用是第一个参数对象为上下文对象
args.push('arguments[' + i + ']');
}
// 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
这里需要深入去理解的地方是这里参数的传递实现有点特别的用到了拼接字符串来传参并用evel方法来实现函数的调用,
下面是第二版的代码:
// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}
// 测试一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
最后第三步,做一些边际的补充代码:
主要补充两点:
1. this的指向为空时我们将默认对象设为window对象;
2. 添加函数调用的结果作为返回值;
这样我们就得到了第三版的代码:
// 第三版
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
这样我们就跟着实现了一个call的调用过程,需要注意的是上面
由于apply与call的区别只是传参方式一个为数组,一个为调用参数枚举出来的不同, 所以我们得到如下作者参考知乎 @郑航的实现的代码
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
这里我们看到有一点稍微容易引起疑惑的小小区别在于apply方法传参for循环的初始值为0, 这是因为它循环的数组是直接去除了context上下文对象的参数数组.
至此我们就总结完了call与apply实现的完整过程,撒花✿✿ヽ(°▽°)ノ✿.
整个学习的文章参考地址: https://github.com/mqyqingfeng/Blog/issues/11
最近又在网上看到两段使用ES6来实现,写法看起来更加优雅的代码,贴出来供参考:
Function.prototype.call = function(){
let [context, ...arg] = [...arguments];
if (!context) {
context = typeof window === 'object' ? window : global;
}
context.fn = this;
let result = context.fn(...arg);
delete context.fn;
return result;
}
Function.prototype.apply = function(context, argArr) {
if (!context) {
context = typeof window === 'object' ? window : global;
}
context.fn = this;
let result;
if (!argArr) {
let result = context.fn();
} else {
let result = context.fn(...argArr);
}
delete context.fn;
return result;
}