this 关键字是JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。
节选自《你不知道的JavaScript》
之所以this会成为JavaScript开发者的"拦路虎",核心在于它的指向并非固定不变,而是取决于函数的调用方式而非定义位置。但在抱怨其复杂之前,我们首先要弄明白一个关键问题: JavaScript为什么需要this?
一、JavaScript为什么需要this?
若没有this关键字,JavaScript在处理对象与函数的关联时会陷入繁琐与僵化的困境。其核心价值主要体现在两个方面:
首先是简化代码,提升复用性。假设我们定义了一个描述用户的对象,需要一个方法打印用户信息。若没有this,就必须通过对象名手动引用属性,如:
js
const user = {
name: "张三",
age: 25,
printInfo: function() {
console.log("姓名:" + user.name + ",年龄:" + user.age);
}
};
user.printInfo(); // 姓名:张三,年龄:25
这种写法看似可行,但当对象名发生改变(如重构时将user改为person),printInfo方法内部的引用也必须同步修改。 若要创建多个用户实例,每个实例的printInfo方法都需要重新编写,无法复用。
this的出现彻底解决了这个问题---------它能自动指向调用方法的对象,让函数摆脱对具体对象名的依赖:
js
const user1 = {
name: "张三",
age: 25,
printInfo: function() {
console.log("姓名:" + this.name + ",年龄:" + this.age);
}
};
const user2 = {
name: "李四",
age: 30,
printInfo: user1.printInfo // 复用同一方法
};
user1.printInfo(); // 姓名:张三,年龄:25
user2.printInfo(); // 姓名:李四,年龄:30
明确上下文关联:在面向对象编程中,函数(方法)需要知道自己属于哪个对象实例,才能正确操作实例的属性和方法。
this就像一个"动态指针",在函数调用时自动绑定到当前上下文对象,让方法能精准访问所属实例的资源。例如在构造函数中,this直接指向新创建的实例,确保每个实例都能拥有独立的属性:
js
function User(name, age) {
this.name = name; // this指向新创建的User实例
this.age = age;
this.printInfo = function() {
console.log("姓名:" + this.name + ",年龄:" + this.age);
};
}
const user1 = new User("张三", 25);
const user2 = new User("李四", 30);
user1.printInfo(); // 姓名:张三,年龄:25
二、this与作用域:容易混淆的"邻居"
很多开发者会将this与作用域混为一谈,实则二者是完全不同的概念。作用域解决的是"变量在哪里可以被访问"的问题,它在函数定义时就已确定,是静态的;而this解决的是"函数调用时指向哪个对象"的问题,它在函数调用时才确定,是动态的。
我们可以通过一个例子清晰区分二者:
js
const name = "全局姓名";
function print() {
const name = "函数内姓名";
console.log("作用域中的name:" + name); // 访问当前作用域的变量
console.log("this指向的name:" + this.name); // 访问this指向对象的name
}
const obj = {
name: "对象姓名",
print: print
};
// 1. 全局调用
print();
// 作用域中的name:函数内姓名(作用域静态确定,访问函数内变量)
// this指向的name:全局姓名(this动态绑定到全局对象)
// 2. 对象调用
obj.print();
// 作用域中的name:函数内姓名(作用域未变)
// this指向的name:对象姓名(this动态绑定到obj)
从结果可见,无论函数如何调用,作用域始终由定义位置决定;而this的指向则随着调用方式的变化而改变。这正是二者最核心的区别。
三、this的指向规则:从调用方式看本质
既然this的指向由调用方式决定,那么我们只需掌握不同调用场景下的绑定规则,就能精准判断this的指向。以下是四种核心规则,优先级从高到低排列:
new绑定:指向新创建的实例
当函数通过new关键字调用时,JavaScript会执行以下步骤:
- 创建一个新对象
- 将新对象的原型指向函数的原型
- 将this绑定到新对象
- 执行函数体
若函数无返回值则返回新对象。此时this必然指向新创建的实例。
js
function Person(name) {
this.name = name; // this指向new创建的Person实例
}
const person = new Person("张三");
console.log(person.name); // 张三
console.log(this.name); // 全局姓名(此处this为全局对象,与函数内this无关)
显式绑定:指向手动指定的对象
JavaScript函数的原型上提供了call、apply、bind三个方法,允许我们手动指定函数调用时this的指向。其中call和apply会立即执行函数,bind则返回一个绑定了this的新函数,三者的核心作用都是"显式绑定this"。
js
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2" };
function printName() {
console.log(this.name);
}
// call绑定:第一个参数为this指向的对象,后续为函数参数
printName.call(obj1); // obj1
// apply绑定:第一个参数为this指向的对象,第二个参数为参数数组
printName.apply(obj2); // obj2
// bind绑定:返回绑定this的新函数,需手动调用
const bindFunc = printName.bind(obj1);
bindFunc(); // obj1
隐式绑定:指向调用方法的对象
当函数作为对象的方法被调用时,this会隐式绑定到调用该方法的对象。简单来说,"谁调用,this就指向谁"。
js
const obj = {
name: "张三",
print: function() {
console.log(this.name);
},
child: {
name: "李四",
print: function() {
console.log(this.name);
}
}
};
obj.print(); // 张三(obj调用print,this指向obj)
obj.child.print(); // 李四(child调用print,this指向child)
默认绑定:指向全局对象或undefined
当函数既不通过new调用,也不通过call/apply/bind显式绑定,更不是作为对象方法隐式绑定,而是以普通函数形式调用时,就会触发默认绑定。此时在非严格模式下,this指向全局对象(浏览器中为window,Node.js中为global);在严格模式下,this指向undefined,这是为了避免意外修改全局对象。
js
// 非严格模式
const name = "全局";
function print() {
console.log(this.name);
}
print(); // 全局(this指向window)
// 严格模式
function strictPrint() {
"use strict";
console.log(this); // undefined
}
strictPrint();
四、特殊场景:打破规则的"例外"
除了上述四种核心规则,还有两种特殊场景会改变this的指向逻辑,需要特别注意:
箭头函数:无独立this,继承上下文this
ES6引入的箭头函数并不遵循上述任何规则,它没有自己的this绑定。箭头函数会捕获其定义时所在上下文的this,并将其作为自己的this,且这个绑定是永久的,无法通过call、apply、bind修改,也不能作为构造函数使用new。
js
const obj = {
name: "obj",
print: function() {
// 箭头函数继承print方法的this(即obj)
const arrowFunc = () => console.log(this.name);
arrowFunc();
}
};
obj.print(); // obj
// 尝试修改箭头函数this
const arrowFunc = () => console.log(this.name);
arrowFunc.call({ name: "newObj" }); // 全局(非严格模式下,箭头函数继承全局this)
事件处理函数:指向触发事件的元素
在浏览器的DOM事件处理中,当函数作为事件监听器被调用时,this会自动指向触发该事件的DOM元素。但如果使用箭头函数作为事件处理函数,由于其this继承自定义时的上下文,就会失去这种默认绑定。
js
// HTML:<button id="btn">点击</button>
const btn = document.getElementById("btn");
// 普通函数作为事件处理函数
btn.addEventListener("click", function() {
console.log(this); // <button id="btn">点击</button>(指向触发元素)
});
// 箭头函数作为事件处理函数
btn.addEventListener("click", () => {
console.log(this); // window(继承全局上下文this)
});
五、总结:判断this指向的核心步骤
面对复杂的this指向问题,我们可以按照以下步骤逐步判断,几乎能覆盖所有场景:
- 判断函数是否通过new调用?若是,this指向新创建的实例;
- 判断函数是否通过call、apply、bind显式绑定?若是,this指向手动指定的对象;
- 判断函数是否作为对象方法隐式绑定?若是,this指向调用方法的对象;
- 判断函数是否为箭头函数?若是,this继承定义时上下文的this;
- 若以上都不是,触发默认绑定:非严格模式指向全局对象,严格模式指向undefined。