JavaScript this 绑定机制
JavaScript this 绑定机制
引言
this 是 JavaScript 中最容易被误解又最常出错的机制之一。与大多数面向对象语言不同,JavaScript 的 this 指向并非由函数定义决定,而是由函数的调用方式决定。理解 this 的四种绑定规则,是写出健壮 JavaScript 代码的基石。本文将系统讲解 this 的绑定逻辑,并配合大量代码示例帮助你建立清晰的认知。
---
一、this 的本质
在 JavaScript 中,this 是执行上下文的一个属性,它指向一个对象,取决于函数的调用方式。this 的绑定只发生在函数被调用时,而非定义时。
理解这一点至关重要:
var person = {
name: "Alice",
greet: function() {
console.log(this.name);
}
};
var person2 = { name: "Bob" };
person2.greet = person.greet; // 将方法引用给另一个对象
person.greet(); // "Alice" — 调用时 this 指向 person
person2.greet(); // "Bob" — 调用时 this 指向 person2
同一个函数,不同的调用方式,this 指向完全不同。
---
二、四种绑定规则
2.1 默认绑定(Default Binding)
独立调用函数时,this 指向全局对象(浏览器中为 window,Node.js 中为 global)。在严格模式下则为 undefined:
function showThis() {
console.log(this);
}
showThis(); // 浏览器中打印 window 对象
严格模式下:
"use strict";
function showThisStrict() {
console.log(this);
}
showThisStrict(); // undefined
注意: 将函数作为回调传入其他函数时,仍属于独立调用:
var count = 10;
var obj = {
count: 5,
fn: function() {
console.log(this.count);
}
};
setTimeout(obj.fn, 100); // 10,打印全局 count,因为 setTimeout 内部独立调用了 fn
2.2 隐式绑定(Implicit Binding)
当函数作为对象的方法被调用时,this 指向调用该方法的对象:
var obj = {
name: "Charlie",
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // "Charlie" — this 指向 obj
隐式丢失(Implicit Binding Loss):
var obj = {
name: "David",
greet: function() {
console.log(this.name);
}
};
var greetFn = obj.greet; // 方法被提取为独立函数引用
greetFn(); // undefined(在浏览器中 this 指向 window)
常见隐式丢失场景:
- 将对象方法作为回调传递
- 将方法赋值给变量后再调用
2.3 显式绑定(Explicit Binding)
通过 call()、apply() 或 bind() 强制指定 this 的绑定对象:
function introduce(lang) {
console.log(`I am ${this.name}, I love ${lang}`);
}
var person = { name: "Eve" };
introduce.call(person, "JavaScript"); // 立即调用,this 指向 person
introduce.apply(person, ["Python"]); // 立即调用,参数以数组传递
<code>bind()</code> 创建并返回一个新函数,this 被永久绑定:
var boundIntroduce = introduce.bind(person, "Go");
boundIntroduce(); // "I am Eve, I love Go" — this 永远指向 person
bind() 的重要性在事件处理和异步回调中尤为突出:
var counter = {
count: 0,
increment: function() {
this.count++;
}
};
document.getElementById("btn").addEventListener("click", counter.increment.bind(counter));
// 不使用 bind 时,this 将指向 DOM 元素而非 counter
2.4 new 绑定(new Binding)
使用 new 关键字调用构造函数时,this 指向新创建的实例对象:
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce = function() {
console.log(`I am ${this.name}, ${this.age} years old.`);
};
}
var alice = new Person("Alice", 25);
alice.introduce(); // "I am Alice, 25 years old."
var bob = new Person("Bob", 30);
bob.introduce(); // "I am Bob, 30 years old."
new 关键字实际做了以下事情:
1. 创建一个新的空对象
2. 将构造函数的 this 绑定到该对象
3. 执行构造函数(为对象添加属性/方法)
4. 返回该对象
---
三、箭头函数的 this 绑定
箭头函数是 ES6 引入的特殊函数,它不绑定自己的 this,而是继承外层词法作用域的 this:
var obj = {
name: "Frank",
greet: function() {
// 传统函数,this 指向 obj
setTimeout(function() {
console.log(this.name); // undefined
}, 100);
},
greetArrow: function() {
// 箭头函数,this 继承外层(obj)
setTimeout(() => {
console.log(this.name); // "Frank"
}, 100);
}
};
箭头函数的 this 不可被 <code>call</code>、<code>apply</code>、<code>bind</code> 改变:
var person = { name: "Grace" };
var arrowFn = () => console.log(this.name);
arrowFn.call(person); // undefined(window.name),箭头函数不适用显式绑定
何时使用箭头函数:
- 适合作为回调函数(需要继承外层 <code>this</code>)
- 不适合作为对象方法(无法获得正确的 <code>this</code>)
- 不适合作为构造函数(无法 <code>new</code>)
---
四、绑定的优先级
当多种绑定规则同时生效时,按以下优先级判断:
1. <code>new</code> 绑定 — 最高优先级
2. 显式绑定(call/apply/bind)
3. 隐式绑定(作为对象方法调用)
4. 默认绑定(独立调用)
function foo(something) {
this.value = something;
}
var obj1 = {};
var obj2 = {};
// 显式绑定优先于隐式绑定
foo.call(obj1, "test1");
console.log(obj1.value); // "test1"
obj2.foo = foo;
obj2.foo("test2");
console.log(obj2.value); // "test2"
// 显式绑定优先级更高
obj2.foo.call(obj1, "test3");
console.log(obj1.value); // "test3",obj1 的值被覆盖
console.log(obj2.value); // "test2"
<code>new</code> 绑定优先级验证:
function bar(a) {
this.val = a;
}
var obj3 = {};
var boundBar = bar.bind(obj3);
boundBar(100);
console.log(obj3.val); // 100
var instance = new boundBar(200);
console.log(instance.val); // 200
console.log(obj3.val); // 100,new 创建了新实例,未影响 obj3
注意:bind 在 new 调用时会被忽略,因为 new 需要创建新实例。
---
五、常见问题与解决方案
问题一:setTimeout 回调中的 this
var counter = {
count: 0,
start: function() {
setTimeout(function() {
this.count++; // this 指向 window,window.count 为 NaN
}, 1000);
}
};
解决方案:
// 方案一:变量暂存 this
start: function() {
var self = this;
setTimeout(function() {
self.count++;
}, 1000);
}
// 方案二:箭头函数
start: function() {
setTimeout(() => {
this.count++;
}, 1000);
}
// 方案三:bind
start: function() {
setTimeout(function() {
this.count++;
}.bind(this), 1000);
}
问题二:类方法作为回调时 this 丢失
class Toggle {
constructor() {
this.isOn = false;
}
toggle() {
this.isOn = !this.isOn;
}
}
var toggle = new Toggle();
document.getElementById("btn").addEventListener("click", toggle.toggle);
// this 指向 DOM 元素,抛出错误
// 解决方案
document.getElementById("btn").addEventListener("click", toggle.toggle.bind(toggle));
// 或
document.getElementById("btn").addEventListener("click", () => toggle.toggle());
问题三:类中箭头函数的 this
class Counter {
count = 0;
// 箭头函数作为实例属性,this 始终指向实例
increment = () => {
this.count++;
}
}
var counter = new Counter();
var inc = counter.increment;
inc(); // 正常工作,this 指向 counter 实例
---
六、运行效果
完整示例,展示各种绑定方式的行为差异:
function identify(suffix) {
return this.name + " (" + suffix + ")";
}
var person1 = { name: "Henry" };
var person2 = { name: "Iris" };
console.log(identify.call(person1, "engineer")); // "Henry (engineer)"
console.log(identify.call(person2, "designer")); // "Iris (designer)"
var bound = identify.bind(person1, "teacher");
console.log(bound()); // "Henry (teacher)"
var obj = {
name: "Jack",
describe: function() {
return `Name: ${this.name}`;
}
};
console.log(obj.describe()); // "Name: Jack"
var desc = obj.describe;
console.log(desc()); // "Name: undefined"(严格模式下报错)
console.log(desc.bind(obj)()); // "Name: Jack"
---
七、延伸阅读
1. 《你不知道的 JavaScript(上卷)》第二部分 — 深入讲解 this 和对象原型机制
2. 《JavaScript 高级程序设计(第4版)》第6章 — 面向对象编程中的 this 行为
3. MDN Web Docs — this — developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
4. ECMAScript 规范 — tc39.es/ecma262/ 12.2.1 Function Binding
5. TypeScript 中的 this 类型 — 了解 TypeScript 如何通过编译时检查减少 this 相关的运行时错误