call、apply 和 bind 都是对 this 值得改变,那三者有什么不同呢?本篇通过模拟三者代码的形式来讲解。

1、call

W3school中对 call()方法的定义是:它可以用来调用所有者对象作为参数的方法。通过 call(),能够使用属于另一个对象的方法。
我们举个例子:

1
2
3
4
5
6
7
var foo = {
name: "Jack",
};
function bar() {
console.log(this.name);
}
bar.call(foo);

由上可以看出 call()函数主要做了两件事,第一个是改变 this 的指向,将 bar 中的 this 指向了 foo,第二个是执行了 bar 方法。

接下来我们来模拟 call 方法:

第一步:实现简单的 call 方法:

上面的例子可以改造为:

1
2
3
4
5
6
7
var foo = {
name: "Jack",
bar: function () {
console.log(this.name);
},
};
foo.bar();

输出的结果和上述的一模一样,唯一的不同就是 foo 中多了一个 bar 的属性,那我们执行完后删除这个属性就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.myCall = function (Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
Context.fn();
delete Context.fn;
};
var foo = {
name: "Jack",
};
function bar() {
console.log(this.name);
}
bar.myCall(foo);

结果和 call 一致。

第二步:因为 call 可以带参数,所以我们接下来实现这个

看一下原版的:

1
2
3
4
5
6
7
8
9
var foo = {
name: "Jack",
};
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.call(foo, 10, "自行车");

因为参数是不固定的,所以可以想到 arguments。怎么将 arguments 传入对象中,这就是个难题,高兴的是 ES6 给了我们方法:解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myCall = function (Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [...arguments].slice(1);
Context.fn(...args);
delete Context.fn;
};
var foo = {
name: "Jack",
};
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.myCall(foo, 10, "自行车");

是不是感觉特别简单,但是 call 是 ES3 的方法,解构赋值是 ES6 的方法,感觉有点欺负它,那我们就需要重新想一个。这个方法也是查看了资料后才找到的,自己能力还是有限啊!
可以运用 eval(),eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myCall = function(Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
for (var i = 1; i < arguments.length; i++>) {
args.push('arguments[+' i '+]');
}
eval('Context.fn(+' args '+)');
delete Context.fn;
}
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.myCall(foo, 10, '自行车');

感觉很完美了,但是依旧有两小点需要注意:

  1. 当 this 为 null 时
  2. 当函数有返回值时
    我们先来看第一点:
1
2
3
4
5
var name = "Jack";
function bar() {
console.log(this.name); // Jack
}
bar.call(null);

当 this 为 null 时,默认走向 window
第二点:

1
2
3
4
5
6
7
8
9
10
var foo = {
name: "Jack",
};
function bar() {
return {
age: 1,
goods: "自行车",
};
}
bar.call(foo); // {age: 1, goods: "自行车"}

当函数有返回值时,结果就是这个返回值。

第三步:我们对自己模拟的方法进行最后的优化:

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myCall = function (Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push("arguments[" + i + "]");
}
var result = eval("Context.fn(" + args + ")");
delete Context.fn;
return result;
};

2、apply

call() 和 apply() 之间的区别,不同之处是:

  1. call() 方法分别接受参数。
  2. apply() 方法接受数组形式的参数。

所以 call 和 apply 只是接受参数的不同,思路还是和 call 一样,这次就不重复了直接贴代码:
运用解构赋值:

1
2
3
4
5
6
7
8
Function.prototype.myApply = function (Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = arguments[1] ? arguments[1] : [];
var result = Context.fn(...args);
delete Context.fn;
return result;
};

运用 eval:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Function.prototype.myApply = function (Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
var arguments = arguments[1] ? arguments[1] : [];
for (var i = 0; i < arguments.length; i++) {
args.push("arguments[" + i + "]");
}
var result = eval("Context.fn(" + args + ")");
delete Context.fn;
return result;
};
var foo = {
name: "Jack",
};
function bar() {
console.log(this.name);
}
bar.myApply(foo);

3、bind

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind()的第一个参数, 它的参数是 bind()的其他参数和其原本的参数。
所以 bind 执行两个步骤:

  1. 返回一个新函数
  2. 可以传入参数

我们按照分析 call()函数一样的思路来分析 bind(),首先看一下原先的 bind:

1
2
3
4
5
6
7
8
var foo = {
name: "Jack",
};
function bar() {
console.log(this.name);
}
var bindFoo = bar.bind(foo);
bindFoo(); // Jack

所以可以看出 bar.bind(foo);返回一个新的函数,当这个函数执行时,才返回其中的结果,那我们先模拟一下这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myBind = function (Context) {
var self = this;
return function () {
return self.apply(Context);
};
};
var foo = {
name: "Jack",
};
function bar() {
console.log(this.name);
}
var bindFoo = bar.myBind(foo);
bindFoo(); // Jack

结果一样,说明这一步模拟的没有问题。那我们进行下一步的模拟,因为 bind 也是可以携带参数的,携带参数的方式和 call 相同:

1
2
3
4
5
6
7
8
9
10
var foo = {
name: "Jack",
};
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
var bindFoo = bar.bind(foo, 1, "1");
bindFoo(); // Jack 1 1

模拟板升级:

1
2
3
4
5
6
7
Function.prototype.myBind = function (Context) {
var self = this;
var args = [].slice.call(arguments, 1);
return function () {
return self.apply(Context, args);
};
};

很完美,但是 bind 有个特点被忽视了,因为 bind 返回一个新的函数,那我们将返回的函数里面传参,会有什么效果,我们看一下:

1
2
3
4
5
6
7
8
9
10
var foo = {
name: "Jack",
};
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
var bindFoo = bar.bind(foo, 1);
bindFoo("1"); // Jack 1 1

可以看出 bind 可以只传入 age,然后再从返回的新函数中传入 goods,那我们需要把上述模拟进行升级:

1
2
3
4
5
6
7
8
Function.prototype.myBind = function (Context) {
var self = this;
var args = [].slice.call(arguments, 1);
return function () {
var bindArgs = [].slice.call(arguments);
return self.apply(Context, args.concat(bindArgs));
};
};

思路其实很简单,就是将两个 arguments 进行合并。
本以为这样就结束了,但是 MDN 提到了 bind 的另外一个特点:绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。什么意思呢?用代码演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
var foo = {
name: "Jack",
};
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
bar.prototype.friends = "a";
var BindFoo = bar.bind(foo, 1);
var bindFoo = new BindFoo("1"); // undefined 1 1
console.log(bindFoo.friends); // a

this.name 竟然输出 undefined,那是因为 new 后,BindFoo 中 this 的指向改变了,指向了 bindFoo,而 BindFoo 实际是 bar 函数,并且 bindFoo 没有 value 属性,所以就输出了 undefined,通过 instanceof 就可以看出来,bindFoo 是 BindFoo 的实例,也是 bar 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function (Context) {
var self = this;
var args = [].slice.call(arguments, 1);
var cacheFn = function () {};
var bindFun = function () {
var bindArgs = [].slice.call(arguments);
return self.apply(
this instanceof cacheFn ? this : Context,
args.concat(bindArgs)
);
};
cacheFn.prototype = this.prototype;
bindFun.prototype = new cacheFn();
return bindFun;
};

我们进行分步讲解:
1、为什么要判断 this instanceof bindFun?
之前也说到,当将 bind 返回后函数当做构造函数时,bindFoo 即是 BindFoo 的实例也是 bar 的实例,BindFoo 即为返回来的函数,在我们模拟的代码中就是 bindFun 这个函数,并且当 new 之后 this 指向的是实例,所以用 this instanceof bindFun 判断的实际就是函数前有没有 new 这个关键词。
2、为什么要继承 this 的原型?
这是为了继承 bar 原型上的属性。
最后一步,健壮模拟的 bind,判断传过来的 this 是否为函数,也是最终版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.myBind = function (Context) {
if (typeof this !== "function") {
throw new Error(
"Function.prototype.bind - what is trying to be bound is not callable"
);
}
var self = this;
var args = [].slice.call(arguments, 1);
var cacheFn = function () {};
var bindFun = function () {
var bindArgs = [].slice.call(arguments);
return self.apply(
this instanceof cacheFn ? this : Context,
args.concat(bindArgs)
);
};
cacheFn.prototype = this.prototype;
bindFun.prototype = new cacheFn();
return bindFun;
};