原文链接:A guide to this in JavaScript
原文作者:Ashay Mandwarya
译者:JintNiu
推荐理由:this一直是JavaScript中的重难点,借助这篇文章,重新认识并理解this,加深印象。
this 无疑是 JavaScript 中使用最广泛但又容易被误解的关键字,今天我将会对其进行详细的解释。
当我们在学校学习英语代词时:
Phelps is swimming fast because he wants to win the race.
这句话中,我们不直接使用 Phelps,而是使用代词“he”来指代他。类似地,JavaScript 中使用 this 关键字指向引用上下文中的对象。
例:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
console.log(car.make + " " + car.model);
}
}
car.fullName();
// Lamborghini Huracán
// Lamborghini Huracán
在上面的代码中,我们定义了一个具有属性 make,model 和 fullName 的对象 car,其中 fullName 是一个函数,函数体内使用 2 种不同的方法输出 make 和 model。
- 使用
this时,this.make+ " " +this.model中的this指的是在上下文中的对象,也就是car,则this.make为car.make,this.model为car.model; - 使用点操作符时,我们可以直接访问对象的属性
car.make和car.model。
this
现在我们已经了解了什么是 this 以及它 最基本的用法,为方便记忆,我们将列出一些场景,并分别举例说明。
根据出现的位置,this 可分为以下几种情况:
- 在方法内使用
- 在函数内使用
- 单独存在
- 在事件中使用
call()和apply()
1. 在方法内使用 this
当 this 在方法内使用时,指向其所属的对象。
在对象内定义的函数称为方法。再来看看汽车的例子:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
console.log(car.make + " " + car.model);
}
}
car.fullName();
该例中,fullName() 即为方法,方法中的 this 指向对象 car。
2. 在函数内使用 this
函数中的 this 就有些复杂了。与对象一样,函数也具有属性。函数每次执行时都会获取 this,它指向调用它的对象。
this实际上只是“先行对象”的一种快捷引用,也就是对象调用。
— javascriptissexy.com
如果函数未被某对象调用,则函数内的 this 属于全局对象,该全局对象被称为 window。在这种情况下,this 将指向全局作用域中的变量。且看以下例子:
var make = "Mclaren";
var model = "720s"
function fullName() {
console.log(this.make + " " + this.model);
}
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
}
}
car.fullName(); // Lmborghini Huracán
window.fullName(); // Mclaren 720S
fullName(); // Mclaren 720S
该例中,在全局对象中定义了 make, model 和 fullName,对象 car 中也实现了 fullName 方法。当使用 car 调用该方法时,this 指向该对象内的变量;而使用另外两种调用方式时,this 指向全局变量。
3. 单独使用 this
当单独使用 this,不依附于任何函数或者对象时,指向全局对象。
这里的 this 指向全局变量 name。
4. 在事件内使用 this
JS 中有很多种事件类型,但为了描述简单,这里我们以点击事件为例。
每当单击按钮并触发一个事件时,可以调用另一个函数来去执行某个任务。如果在函数内使用 this,则指向触发事件中的元素。DOM 中,所有元素都以对象的形式储存,也就是说网页元素实际上就是 DOM 中的一个对象,因此每触发一个事件时,this 就会指向该元素。
例:
<button onclick="this.style.display='none'">
Remove Me!
</button>
5. call(), apply() & bind()
bind:允许我们在方法中设置this指向call&apply:允许我们借助其他函数并在函数调用中改变this的指向。
有关 call(), apply() 和 bind() 的知识会在另一篇文章阐述。
难点
理解掌握了 this 会使工作变轻松很多,但实际情况往往不是那么如意。请看以下例子。
例1:
var car = {
make: "Lamborghini",
model: "Huracán",
name: null,
fullName: function () {
this.name = this.make + " " + this.model;
console.log(this.name);
}
}
var anotherCar = {
make: "Ferrari",
model: "Italia",
name: null
}
anotherCar.name = car.fullName();
// Lamborghini Huracán
结果并不是我们所期望的。分析其原因:当我们使用 this 调用另一个对象的方法时,只是为 anotherCar 分配了该方法,但实际调用者是 car。因此返回的是 Lamborghini 而不是 Ferrari。
我们可以使用 call() 解决这个问题。
该例中利用 call() 方法使 anotherCar 对象调用 fullName(),该对象中原本并没有 fullName() 方法,但输出了 Ferrari Italia。
另外,当我们输出 car.name 和 anotherCar.name 的值时,前者输出 null,而后者输出了 Ferrari Italia,也就是说 fullName() 函数确实被 anotherCar 调用了,而不是被 car 调用。
例2:
var cars = [
{ make: "Mclaren", model: "720s" },
{ make: "Ferrari", model: "Italia" }
]
var car = {
cars: [{ make: "Lamborghini", model: "Huracán" }],
fullName: function () {
console.log(this.cars[0].make + " " + this.cars[0].model);
}
}
var vehicle = car.fullName;
vehicle() // Mclaren 720s
该例中,我们定义了一个全局变量 cars,并且在对象 car 中也定义了同名变量,接着将 fullName() 方法赋给变量 vehicle,然后调用它。该变量属于全局变量,由于上下文的关系,this 指向的是全局变量 cars 而不是局部变量。
我们可以使用 bind() 解决这个问题。
bind 改变了this 的指向,使变量 vehicle 指向局部变量 car。也就是说,this 的指向取决于 car 的上下文环境。
例3:
var car = {
cars: [
{ make: "Lamborghini", model: "Huracán" },
{ make: "Mclaren", model: "720s" },
{ make: "Ferrari", model: "Italia" }
],
brand:"lamborghini",
fullName: function () {
this.cars.forEach(function(car){
console.log(car.model + " " + this.brand);
})
}
}
car.fullName();
// Huracán undefined
// 720s undefined
// Italia undefined
在以上代码中,fullName() 使用 forEach 迭代数组 cars,每次迭代都产生一个没有上下文的匿名函数,这类定义在函数内部的函数,称之为闭包(closure)。闭包在 JavaScript 中非常重要,而且被广泛使用。
另一个重要的概念是作用域(scope)。定义在函数内部的变量不能访问其作用域以外的变量和属性;匿名函数中的 this 不能访问外部作用域,以至于 this 只能指向全局对象。该例中,全局对象中没有定义 this 所要访问的属性 brand,因此输出 undefined。
以上问题的解决方法是:我们可以在匿名函数外为 this 赋值,然后在函数内使用。
将 this 赋给变量 self,并代替函数体内的 this,输出期望结果。
例4:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function (cars) {
cars.forEach(function (vehicle) {
console.log(vehicle + " " + this.model);
})
}
}
car.fullName(['lambo', 'ferrari', 'porsche']);
// lambo undefined
// ferrari undefined
// porsche undefined
当无法使用 this 进行访问时,可以使用变量 self 来保存它(如例 3 ),但在该例中,也可以使用箭头函数来解决:
可以看出,在 forEach() 中使用箭头函数就可以解决该问题,而不是进行绑定或暂存 this。这是由于箭头函数绑定了上下文,this 实际上指向原始上下文或原始对象。
例5:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
}
}
var truck = {
make: "Tesla",
model: "Truck",
fullName: function (callback) {
console.log(this.make + " " + this.model);
callback();
}
}
truck.fullName(car.fullName);
// Tesla Truck
// undefined undefined
上述代码中定义了两个相同的对象,但其中一个包含回调函数,回调函数将作为参数传入另一个函数,然后通过外部函数调用来完成某种操作。
该代码中对象 truck 的 fullName 方法包含一个回调函数,并在方法中直接进行调用。当将 car.fullName 作为参数调用 truck.fullName() 时,输出 Tesla Truck 和 undefined undefined。
结果出乎意料。实际上,car.fullName 只是作为参数传入,而不是由 truck 对象调用。换句话说,回调函数调用了对象 car 的方法,但却把 this 绑定到全局作用域上,如下图:
为便于观察,我们输出了 this。可以看到回调函数中的 this 指向了全局作用域。继续创建全局变量 make 和 model 如下例:
显而易见,回调函数中的输出了全局变量 make 和 model,再次证明了 this 指向全局对象。
为得到期望结果,我们将使用 bind() 将 car 强制绑定到回调函数中。如下:
完成!
毫无疑问,this 是非常有用的,但不容易理解。希望通过这篇文章你可以逐渐了解它的使用方法。
如果这篇文章对您有所帮助,点个赞👏,加个关注👣 吧~