JavaScript 的秘密生活:理解 “this”
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)this 为 undefined;非严格模式下则是全局对象(浏览器中为 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:使用 call、apply 与 bind 显式绑定
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"
call 与 apply 为单次调用显式设定 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.name 为 undefined。
解决方案
方案 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"