call,apply,bind
Posted: 11.13.2019
介绍
我们都知道 JS 的 this 指向……很迷。
尤其是在普通函数和箭头函数同时出现的时候。
而 call/apply/bind 三剑客就能够改变 this 的指向。
call / apply
call 和 apply 是同一类型的,都在 Function.prototype 上。
call 的话,function.call(thisArg, arg1, arg2, ...),第一个参数是 this,之后是要执行的函数的参数。
apply 的话,function.apply(thisArg, [argsArray]),第一个参数也是 this,之后是一个包含了执行函数参数的数组。
call 和 apply 唯一的区别就是第二个参数了,别的其实都一样。
本来的话,一个对象调用函数的时候,this 会指向调用函数的对象。举个🌰:
Array.prototype.back = function () {
// 这里的 this 就指向了调用 back 函数的数组
return this[this.length - 1];
}
const arr = [1, 2, 3];
console.log(arr.back()); // 会打印 3
如果我们用 call/apply 绑定了一个新的数组,那么 this 就会指向那个新的数组。
const arr2 = [4, 5, 6];
console.log(Array.prototype.back.call(arr2)); // 会打印 6
console.log(Array.prototype.back.apply(arr2)); // 会打印 6
在上面的例子里,this 指向了 arr2。
bind
bind 的定义如下:function.bind(thisArg[, arg1[, arg2[, ...]]])。
bind 和 call 以及 apply 不太一样。
call 和 apply 都是在指定的 this 环境下执行函数。
但 bind 会返回一个绑定了指定 this 的函数,并不会立即执行函数。
var obj = {
name: "lynch",
length: 18
};
function test(sex, duration) {
console.log(this.name, this.length, sex, duration);
}
// 这里返回了一个新的函数 mytest
const mytest = test.bind(obj);
// 会打印 lynch 18 male 30min
mytest("male", "30min");
// 在这里,返回的新的函数绑定了 test 的第一个参数
const mytestWithParam = test.bind(obj, "male");
// 会打印 lynch 18 male 30min
mytestWithParam("30min");
bind 一个典型的用例就是 React。可以参考这这篇文章:为什么 React 需要 bind this
手动实现 call
首先来观察一下 call 的参数:function.call(thisArg, arg1, arg2, ...)
第一个是 this,剩下可能有无数个参数。
Function.prototype.mycall = function(obj) {
/**
* 得到所有参数
* 因为 arguments 是伪数组,所以 slice 需要用 call
*/
var args = Array.prototype.slice.call(arguments, 1);
/**
* 目前,this 指向的便是调用 mycall 的函数本身
* 建立一个暂时的fn,用来存储 this,并且用它来执行 this 这个函数
*/
obj.fn = this;
obj.fn(...args);
// 删除对象的属性
delete obj.fn;
};
测试一下看看:
var obj = {
name: "lynch",
length: 18
};
function test(sex, duration) {
console.log(this.name, this.length, sex, duration);
}
test.mycall(obj, "male", "30min"); // lynch 18 male 30min
手动实现 apply
实现 apply 和实现 call 的思路是一样的,但是 call 和 apply 的参数不太一样。
apply 的第二个参数是一个数组:function.apply(thisArg, [argsArray])。
Function.prototype.myapply = function(obj, arr) {
// 建立一个暂时的fn,用来存储 this,并且用它来执行 this 这个函数
obj.fn = this;
// 检查第二个参数是否被传进来了
if (!arr) {
obj.fn();
} else {
obj.fn(...arr);
}
// 删除对象的属性
delete obj.fn;
};
最后测试的结果如下:
var obj = {
name: "lynch",
length: 18
};
function test(sex, duration) {
console.log(this.name, this.length, sex, duration);
}
test.myapply(obj, "male", "30min"); // lynch 18 male 30min
手动实现 bind
最终要实现的效果:const mytest = test.bind(obj, sex, duration)。
Function.prototype.mybind = function(oThis) {
// 检查绑定的对象是否是函数,不是的话throw TypeError
if (typeof this !== "function") {
throw new TypeError("Can only bind to functions!");
}
/**
* var foo = function.mybind(this, time, num, people);
* 1. args 包含了调用 foo 时所需要传进去的参数(因为是类数组所以不能直接用slice)
* 调用的时候就像这样:foo(...args) = foo(time, num, people)
* 2. fToBind 就是 function to bind,也就是需要被绑定的函数
* 3. fNOP 是中转函数,下面会提到
* 3. fBound 是最终要绑定到的函数,因为最终要返回一个函数,所以 fBound 是一个函数
*/
var args = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// 返回一个函数
return fToBind.apply(
/**
* apply 的第一个参数
* this instanceof fBound 的目的就是检查 this 是不是最终需要绑定到的函数的实例
* 如果是的话,就相当于在执行构造函数:const xxx = new fBound(),需要绑定的就是 fBound 的实例
* 如果不是的话,就相当于在执行普通的函数:fBound(),因此需要绑定传入的参数
*/
this instanceof fBound ? this : oThis,
/**
* 这里 arguments 是这个返回函数执行的时候传递的一系列参数
* 所以是从第一个参数开始,只有两者合并了之后才是返回函数的完整参数
* 在上个部分的 bind 的例子里,我一开始绑定了一个参数,然后再传递另一个参数
* 这两个参数合起来,形成一个数组,然后作为 apply 的第二个参数
*/
args.concat(Array.prototype.slice.call(arguments, 0))
);
};
/**
* 因为被 new 了以后要继承原型链上的方法,所以需要 fNOP 这个中转函数
* 目的是把 this 上面的原型链给继承下来
* 在这一步后,fNOP 的 prototype 指向了需要绑定的函数的 prototype
*/
if (this.prototype) {
fNOP.prototype = this.prototype;
}
/**
* 下行的代码将 fBound.prototype 指向了 fNOP 的实例
* 因此,fBound.prototype.__proto___ = this.prototype
* 最简单的原型链继承,fBound 的实例可以获取到 this.prototype 的方法和属性
*/
fBound.prototype = new fNOP();
return fBound;
};
参考资料
This is why we need to bind event handlers in Class Components in React