别再踩坑!JavaScript的this关键字,一次性讲透其“变脸”真相

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会执行以下步骤:

  1. 创建一个新对象
  2. 将新对象的原型指向函数的原型
  3. 将this绑定到新对象
  4. 执行函数体

若函数无返回值则返回新对象。此时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指向问题,我们可以按照以下步骤逐步判断,几乎能覆盖所有场景:

  1. 判断函数是否通过new调用?若是,this指向新创建的实例;
  2. 判断函数是否通过call、apply、bind显式绑定?若是,this指向手动指定的对象;
  3. 判断函数是否作为对象方法隐式绑定?若是,this指向调用方法的对象;
  4. 判断函数是否为箭头函数?若是,this继承定义时上下文的this;
  5. 若以上都不是,触发默认绑定:非严格模式指向全局对象,严格模式指向undefined。
相关推荐
kingwebo'sZone几秒前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090119 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农31 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式