call、apply、bind
这三个方法都是用来改变函数的this指向的,但是有所不同。
call和apply都是立即执行函数,而bind是返回一个新的函数,需要手动执行。
首先看下面的代码:
let name = '小王',age = 18;
let obj = {
name:'小李',
objAge:this.age,
myFun:function () {
console.log(this.name + ' ' + this.age);
}
}
obj.objAge; // 18
obj.myFun(); // 小李 18
还有第二个例子:
function myFun() {
console.log(this.fav);
}
myFun(); // undefined
this 一般指向调用它的对象,第一个例子中,this指向obj,第二个例子中,this指向window。(也有意外情况,比如箭头函数没有自己的this,它的this指向上一级的this,如果上一级没有this,那么this就指向window,比如下面的例子)
var fav = '小张';
let obj = {
fav:'小李',
myFun:() => {
console.log(this.fav);
}
}
obj.myFun(); // 小张
这里要注意一个问题 如果使用 let 对 fav 赋值,其值并不会被赋值到 window 上。
功能
call、apply、bind 都可以改变 this 的指向。
let name = '小王',age = 18;
let obj = {
name:'小李',
objAge:this.age,
myFun:function () {
console.log(this.name + ' ' + this.age);
}
}
let db = {
name:'小张',
age:20
}
obj.myFun.call(db); // 小张 20
obj.myFun.apply(db); // 小张 20
obj.myFun.bind(db)(); // 小张 20
他们三个方法都将 this 指向了 db 对象,所以输出的结果都是小张 20。obj 对象中并没有叫 age 的属性,并且 name 属性原本是小李,但是现在变成了小张。这表示手动改变 this 指向可以改变函数的执行环境。
这里注意到 bind 后面有一个括号,这是因为 bind 返回的是一个新的函数,需要手动执行。其他两个都是立即执行函数。
传参
call、apply、bind 都可以传参,但是传参的方式不同。
let name = '小王',age = 18;
let obj = {
name:'小李',
objAge:this.age,
myFun:function (from,to) {
console.log(this.name + ' ' + this.age + 'from' + from + 'to' + to);
}
}
let db = {
name:'小张',
age:20
}
obj.myFun.call(db,'北京','上海'); // 小张 20from北京to上海
obj.myFun.apply(db,['北京','上海']); // 小张 20from北京to上海
obj.myFun.bind(db,'北京','上海')(); // 小张 20from北京to上海
call 中传参是直接和第一个 this 指向并列传入的,apply 中传参是放在一个数组中传入的,bind 中传参和 call 一样。
// ...
let params = ['北京','上海'];
obj.myFun.call(db,...params); // 小张 20from北京to上海
obj.myFun.apply(db,params); // 小张 20from北京to上海
obj.myFun.bind(db,...params)(); // 小张 20from北京to上海
因此对于数组,可以使用扩展运算符来传参给 call 和 bind。
具体实践
说了那么多理论上的东西,那么它们在我们具体实践中能发挥什么作用呢?
1. 使用 call 来继承
function Person(name,age) {
this.name = name;
this.age = age;
}
function Student(name,age,grade) {
Person.call(this,name,age);
this.grade = grade;
}
let stu = new Student('小王',18,3);
console.log(stu); // Student { name: '小王', age: 18, grade: 3 }
在这个例子中,我们使用 call 来继承 Person,这样就可以在 Student 中使用 Person 中的属性和方法了。因为 Student 中本身是没有 name 和 age 这两个属性的。
2. 保持 this 的指向
// 定义一个按钮
var btn = document.getElementById("btn");
// 给按钮添加点击事件
btn.onclick = function() {
// 点击后禁用按钮
this.disabled = true;
// 使用 bind 方法延迟执行函数,并保持 this 指向
setTimeout(function() {
// 恢复按钮状态
this.disabled = false;
}, 2000);
}
执行 this.disabled = false
时会报错,因为 this 指向的是 window,而 window 中并没有 disabled 这个属性。这时候我们可以使用 bind 来保持 this 的指向。
// 定义一个按钮
var btn = document.getElementById("btn");
// 给按钮添加点击事件
btn.onclick = function() {
// 点击后禁用按钮
this.disabled = true;
// 使用 bind 方法延迟执行函数,并保持 this 指向
setTimeout(function() {
// 恢复按钮状态
this.disabled = false;
}.bind(this), 2000);
}
这样就能保持 this 的指向了。
箭头函数无法使用 call、apply、bind
let name = '小王',age = 18;
let obj = {
name:'小李',
objAge:this.age,
myFun:() => {
console.log(this.name + ' ' + this.age);
}
}
let db = {
name:'小张',
age:20
}
obj.myFun.call(db); // Uncaught TypeError: Canot read prosperties of undefined (reading 'call')
obj.myFun.apply(db); // Uncaught TypeError: Cannot read properties of undefined (reading 'apply')
obj.myFun.bind(db)(); // Uncaught TypeError: Cannot read properties of undefined (reading 'bind')
因为箭头函数中的 this 是在定义时就确定了,而不是在执行时确定的,所以无法使用 call、apply、bind 来改变 this 的指向。这是一个很大的坑,需要注意。