js中this
在JavaScript学习过程中,this几乎是每个人都会遇到的“经典难题”
很多人刚开始接触this时,常常会有这样的疑问:
- 为什么同一个函数,在不同的地方调用时,this的值不一样
- 为什么对象方法里的this指向对象本身,而定时器里的this却常常“跑偏”
- 为什么箭头函数又说“没有自己的this”
这些问题的根源在于:JavaScript中this的指向,并不是在函数定义时决定的,而是在函数调用时决定的。
理解这句话,是掌握this的关键。
这篇文章会从最基础的概念开始,逐步讲清楚this的常见绑定规则、实际开发的典型陷阱,以及如何快速判断一个函数中this到底指向谁
this到底是什么
在JavaScript中,this可以理解为函数运行时的上下文对象引用。可以简单的理解为这次函数的执行,到底关联的时谁。
来看一个简单的例子:
function showThis() {
console.log(this)
}
showThis()
这里的this指向谁,不是由函数showThis写在什么位置决定的,而是由showThis()这种调用方式决定的。
this让人困惑的地方
this难的地方在于:同一个函数,换一种调用方式,this可能就变了
例如:
function sayName() {
console.log(this.name)
}
const obj1 = {name: "Tom", sayName}
const obj2 = {name: "Jerry", sayName}
obj1.sayName() // "Tom"
obj2.sayName() // "Jerry"
sayName // undefined或者报错
明明是同一个函数sayName,但因为调用方式不同,this的值也不同
所以,学this不能死记硬背“它指向某个固定对象”,而应该掌握它背后的绑定规则
判断this的核心原则
在JavaScript中,this的指向通常可以根据以下几种规则来判断
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
- 箭头函数的词法绑定
接下来我们逐个来看:
- 当一个函数以普通函数调用时,会触发默认绑定
function foo() {
console.log(this)
}
foo()
此时分两种情况:严格模式下,this时undefined;非严格模式下,this指向全局对象global,在浏览器中通常时window
- 隐式绑定:作为对象的方法调用
const obj = {
name: "Tom"
sayName: function() {
console.log(this.name)
}
}
obj.sayName() // Tom
当函数通过某个对象来调用时,this通常指向这个调用它的对象。
容易出现误解的地方:很多人认为,只要函数定义在对象里面,this就一定指向这个对象。其实不是。关键还是看调用方式
例如:
const obj = {
name: "Tom",
syaName() {
console.log(this.name)
}
}
const fn = obj.sayName
fn()
这里虽然fn来自Obj.sayName,但真正调用的时候是:
fn()
它已经不是通过obj调用了,因此不再满足隐式绑定。这也是开发中最常见的this丢失问题之一。
- 显式绑定:call、apply、bind
JavaScript提供了三种手动指定this的方法: call、apply、bind,它们统称为显示绑定。
call会立即调用函数,并且第一个参数就是要绑定的this
function sayName() {
console.log(this.name)
}
const person1 = {name: "Tom"}
const person2 = {name: "Alice"}
sayName.call(person1) // "tom"
sayName.call(person2) // "Alice"
apply的作用和call基本一样,也是立即调用函数,不同点在于参数的传递方式不同。
function introduce(age, city) {
console.log(this.name, age, city)
}
const person = {name: "Bob"}
introduce.apply(person, [20, "Bei jing"]) // Bob 20 Bei jing
call的参数是以列表形式一个个传,apply参数放在数组传。例如:
introduce.all(person, 20, "Bei jing") // Bob 20 Bei jing introduce.apply(person, [20, "Bei jing"]) // Bob 20 Bei jing
bind:和前面两个最重要的区别就是:它不是立即执行函数,而是返回一个新的函数。
function sayName() {
console.log(this.name)
}
const person = {name: "Tom"}
const newFn = sayName.bind(person)
newFn() // Tom
显示绑定最大的价值是: 当默认绑定和隐式绑定不满足要求时,我们可以手动决定this指向谁,可以选择立即执行,或者保存后在适当时机调用。
- new绑定: 构造函数中的this
当一个函数通过new调用时,this会指向新创建的对象
function Person(name) {
this.name = name
}
const p = new Person("Jack")
console.log(p.name) // Jack
这里Person中的this指向new过程中创建出来的实例对象。
当我们执行:new Person("Jack") 时,大致会经历下面几个步骤:
- 创建一个空的新对象
- 让这个对象的隐式原型__proto__,指向函数的原型对象prototype
- 把构造函数中的this绑定到这个新对象
- 执行构造函数代码
- 判断构造函数有无显式返回一个对象,如果有则返回这个对象,否则返回新创建的对象。
上面的步骤也是关于构造函数的一个比较经典的面试题
- 箭头函数中的this
箭头函数是this判断中最容易混淆的地方,准确来说:this函数没有自己的this。它的this不是在调用时决定的,而是在定义时从外层作用域继承的
这也叫做:词法绑定
先看看普通函数
const obj = {
name: "Tom"
sayName:() {
console.log(this.name)
}
}
obj.sayName() // Tom
再看看箭头函数
const obj = {
name: "Tom"
sayName: () => {
console.log(this.name)
}
}
obj.sayName() //
很多人以为这里会输出Tom,其实通常不会。
原因是:箭头函数没有自己的this,他会去外层作用域找this。而不会因为通过obj.sayName() 这种调用方式就把this绑定到obj。
箭头函数适合什么场景
箭头函数最适合的一个场景,是需要保留外层this的回调函数中。
例如:
function Person() {
this.name = "Alice"
setTimeout(() => {
console.log(this.name)
}, 1000)
}
new Person() // 1秒后输入Alice
这里的箭头函数中的this继承外层Person函数,而Person又是通过new调用的,所以外层this指向实例对象。因此回调也能正常访问到实例的name
如果换成普通函数:
function Person() {
this.name = "Alice"
setTimeout(function() {
console.log(this.name)
}, 1000)
}
这里的 this 就不是外层实例对象了,因为传给 setTimeout 的是一个普通函数,它执行时会按照自己的调用规则决定 this。结果通常拿不到预期的 name。
实际开发中常见的 this 陷阱
理解规则还不够,真正写代码时,this 的问题通常出现在一些具体场景中。
1. 方法赋值后 this 丢失
const obj = {
name: "Tom",
sayName() {
console.log(this.name);
}
};
const fn = obj.sayName;
fn();
这里 this 丢失的原因是:调用时已经不是 obj.sayName(),而是 fn()。
解决方式
const fn = obj.sayName.bind(obj); fn();
或者:
const fn = () => obj.sayName(); fn();
2. 定时器中的 this
const obj = {
name: "Tom",
sayName() {
setTimeout(function () {
console.log(this.name);
}, 1000);
}
};
obj.sayName();
这里回调函数中的 this 通常不会指向 obj。
解决方式一:使用箭头函数
const obj = {
name: "Tom",
sayName() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
obj.sayName();
解决方式二:提前保存 this
const obj = {
name: "Tom",
sayName() {
const self = this;
setTimeout(function () {
console.log(self.name);
}, 1000);
}
};
obj.sayName();
解决方式三:使用 bind
const obj = {
name: "Tom",
sayName() {
setTimeout(function () {
console.log(this.name);
}.bind(this), 1000);
}
};
obj.sayName();
3. 事件回调中的 this
在浏览器中,事件处理函数里的 this 通常指向触发事件的 DOM 元素。
button.addEventListener("click", function () {
console.log(this); // button
});
如果改成箭头函数:
button.addEventListener("click", () => {
console.log(this);
});
这里的 this 就不再是按钮本身,而是继承自外层作用域。
这也是为什么很多时候,DOM 事件回调中更适合用普通函数,而不是箭头函数。
4. 类方法单独传递时 this 丢失
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
const p = new Person("Lucy");
const fn = p.sayName;
fn();
这里同样会发生 this 丢失。
解决方法
const fn = p.sayName.bind(p); fn();
或者在构造函数中提前绑定。
如何快速判断 this 指向谁
学了这么多规则之后,我们可以总结出一个实用的判断顺序。
当你看到一个函数中的 this 时,可以按照下面的步骤来判断:
第一步:看是不是箭头函数
如果是箭头函数,直接看它外层作用域的 this,它没有自己的 this。
第二步:看是不是通过 new 调用
如果是 new Fn(),那么 this 指向新创建的对象。
第三步:看是不是通过 call、apply、bind 显式绑定
如果用了这些方法,this 通常就是你手动指定的对象。
第四步:看是不是通过对象调用
如果调用形式是 obj.fn(),那么 this 通常指向 obj。
第五步:否则就是默认绑定
普通函数独立调用,就走默认绑定:
- 非严格模式下通常是全局对象
- 严格模式下是 undefined
这个顺序在分析绝大多数 this 问题时都很好用。
评论 (0)
为了减少垃圾评论,请先使用 GitHub 登录后再发表评论。
使用 GitHub 登录评论暂无评论,成为第一个评论者!