JavaScript 的秘密生活:理解 “this”

发布: (2025年12月8日 GMT+8 12:41)
6 min read
原文: Dev.to

Source: Dev.to

第 2 章:上下文决定一切

Timothy 手捧一杯热茶,神情困惑地来到图书馆。他上午一直在调试一个简单的 JavaScript 应用,结果大脑已经“短路”。

Timothy: “Margaret,我需要帮助。我写的代码看起来很直白,但 this 总是指向错误的对象。有时是我期望的对象,有时是 undefined,有时是全局的 window 对象。这让我快疯了。”

Margaret 看了看他的代码——一个绑定在按钮上的简单事件处理器。

const user = {
  name: "Alice",
  greet: function() {
    console.log("Hello, " + this.name);
  }
};

user.greet(); // "Hello, Alice" - works!

const greetFunction = user.greet;
greetFunction(); // "Hello, undefined" - WHAT?!

她会心一笑。

Margaret: “啊,最著名的 JavaScript 迷惑来源。你已经发现了真相:this 并不是你想的那样。”

Timothy: “但它是同一个函数!为什么表现不一样?”

Margaret: “因为在 JavaScript 中,this 的决定不是看函数在哪里定义的,而是看函数如何被调用的。这和 Python 完全是不同的思维模型。”

Timothy 发出一声叹息。

Timothy: “已经在和 Python 比较了?”

Margaret: “你必须弄清这点差别。Python 中的 self 是显式的,你在每个方法签名里都写上它,毫不含糊。JavaScript 中的 this 是隐式的,它在调用时才确定,而不是在定义时。这正是困惑产生的根源。”

this 的四条规则

Margaret 拿出她那本破旧的笔记本,翻到标有 “JavaScript 之谜” 的章节。

规则 1:方法调用

const user = {
  name: "Alice",
  greet: function() {
    console.log(this); // The object (user)
  }
};

user.greet(); // this = user

当你以对象的方法形式(使用点号)调用函数时,this 指向该对象本身。这是直观的,也是大多数人所期待的。

规则 2:函数调用

function greet() {
  console.log(this);
}

greet(); // this = undefined (in strict mode) or window (in non-strict)

直接调用函数(不是作为对象的方法)时,this 取决于是否处于严格模式。严格模式下(现代 JavaScript)thisundefined;非严格模式下则是全局对象(浏览器中为 window,Node.js 中为 global)。无论哪种,都不是大多数情况下想要的结果。

Timothy: “谁会这么做?”

Margaret: “没人会有意这么写。但看看把方法从对象中抽离出来会发生什么。”

const user = {
  name: "Alice",
  greet: function() {
    console.log("Hello, " + this.name);
  }
};

user.greet(); // "Hello, Alice" – Rule 1 (method call)

const greetFunction = user.greet; // 抽离函数
greetFunction(); // "Hello, undefined" – Rule 2 (function call)

抽离函数后,它变成了普通函数调用,于是 this 变成了 undefined。函数并不会记得它原本来自哪个对象。

规则 3:构造函数调用

function User(name) {
  this.name = name;
  console.log(this); // A brand‑new object
}

const alice = new User("Alice");
console.log(alice.name); // "Alice"

使用 new 关键字会创建一个全新的对象,并把 this 绑定到该对象上。构造函数随后为对象添加属性。

规则 4:使用 callapplybind 显式绑定

function greet(greeting) {
  console.log(greeting + ", " + this.name);
}

const user = { name: "Alice" };

greet.call(user, "Hello");   // "Hello, Alice"
greet.apply(user, ["Hello"]); // "Hello, Alice"

const boundGreet = greet.bind(user);
boundGreet("Hello"); // "Hello, Alice"

callapply 为单次调用显式设定 this,而 bind 则返回一个新函数,使 this 永久绑定到指定对象。

判断 this 的决策树

函数是如何被调用的?

├─ 使用 'new' ?
│  └─ 规则 3:构造函数调用
│     → this = 新创建的对象

├─ 使用 .call()、.apply() 或 .bind() ?
│  └─ 规则 4:显式绑定
│     → this = 你指定的对象

├─ 作为 object.method() ?
│  └─ 规则 1:方法调用
│     → this = 点号前的对象

└─ 直接调用 function() ?
   └─ 规则 2:函数调用
      → this = undefined(严格模式)或全局对象(非严格模式)

在调试 this 问题时,按这棵树逐层检查,找出适用的规则。

问题:失去上下文

Timothy 意识到,他最初的困扰正是因为抽离方法后触发了规则 2。

Margaret 给出一个更贴近实际的事件监听器例子:

const button = document.querySelector('button');

const user = {
  name: "Alice",
  handleClick: function() {
    console.log(this.name); // What is this?
  }
};

button.addEventListener('click', user.handleClick);
// When clicked: console.log undefined
// addEventListener calls the function with `this` set to the button element,

addEventListener 在调用处理函数时会把 this 绑定到按钮元素本身,所以 this.nameundefined

解决方案

方案 1:使用箭头函数

const user = {
  name: "Alice",
  setupButton: function() {
    const button = document.querySelector('button');
    button.addEventListener('click', () => {
      console.log(this.name); // this = user (captured from setupButton's scope)
    });
  }
};

user.setupButton();

箭头函数会从外层词法作用域继承 this,从而保持 user 的上下文。

方案 2:使用 bind

button.addEventListener('click', user.handleClick.bind(user));
// When clicked: console.log "Alice"
Back to Blog

相关文章

阅读更多 »

函数与箭头函数

什么是函数?如果用简单的话来说,它们是 JavaScript 的主要构建块之一。它们用于将你的代码组织成小的模块……