在上一节中我们已经知道,this 的值是在函数被调用时动态确定的,同一个函数在不同调用方式下可以指向完全不同的对象。那么,具体由什么决定 this 的指向?JavaScript 引擎在计算 this 时遵循一套固定的优先级规则,概括为四大绑定规则。理解这四条规则,你就掌握了预测 this 值的完整方法。
5.2.1 默认绑定
当一个函数以独立函数的形式被调用时,应用默认绑定。此时 this 在严格模式下为 undefined,在非严格模式下指向全局对象(浏览器中是 window,Node.js 中是 global)。
这是最简单、也最容易被忽略的规则。只要函数调用前没有对象引用、没有显式指定 this,也没有使用 new,就落到默认绑定。
// 非严格模式
function showThis() {
console.log(this);
}
showThis(); // window(浏览器环境)
// 严格模式
'use strict';
function showThisStrict() {
console.log(this);
}
showThisStrict(); // undefined
注意,只要函数体处于严格模式,默认绑定就是 undefined,无论调用点是否在严格模式下。另外,在 Node.js 的 REPL 里全局对象是 global,但模块顶层 this 不等于 global,这一点会在第 3 章中涉及。
5.2.2 隐式绑定
当函数以方法的形式被调用,即通过 obj.fn() 的形式触发时,函数内部的 this 指向调用它的最后一个对象。
换句话说,谁最后“点”它,this 就指向谁。例子一目了然:
const user = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
user.sayName(); // 'Alice' —— this 指向 user
多层属性引用只看最后一层:
const obj1 = {
name: 'obj1',
obj2: {
name: 'obj2',
fn: function() {
console.log(this.name);
}
}
};
obj1.obj2.fn(); // 'obj2' —— this 指向 obj2,不是 obj1
隐式丢失
隐式绑定的最大陷阱叫隐式丢失:将一个方法赋值给另一个变量再调用,或者作为回调函数传递,它就会退化成默认绑定。
const user = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
const fn = user.sayName;
fn(); // undefined(严格模式)或 报错 —— 此时是独立调用,this 丢失
// 常见场景:作为回调传入
setTimeout(user.sayName, 100); // this 同样丢失,输出可能是 undefined
回调函数里 this 丢失是非常经典的前端坑,解决方案正是下一节要讲的显式绑定(如 bind)。
5.2.3 显式绑定
通过 call、apply、bind 方法明确指定函数执行时的 this 值,称为显式绑定。
这三个方法都允许你直接告诉引擎:“这次调用就用这个对象作为 this”。
- call:
fn.call(thisArg, arg1, arg2, ...),立即调用,参数逐个传递。 - apply:
fn.apply(thisArg, [arg1, arg2, ...]),立即调用,参数以数组形式传递。 - bind:
fn.bind(thisArg)返回一个新函数,永久绑定 this,不会立即执行。
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const person = { name: 'Bob' };
greet.call(person, 'Hello'); // Hello, Bob
greet.apply(person, ['Hi']); // Hi, Bob
const boundGreet = greet.bind(person, 'Hey');
boundGreet(); // Hey, Bob
使用显式绑定可以彻底解决隐式丢失问题:
const user = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
setTimeout(user.sayName.bind(user), 100); // 'Alice',this 被锁定
绑定的不可变性
bind 生成的函数一旦绑定了 this,后续再用 call/apply 也无法改变。这一点在书写高阶函数时特别有用,可以避免 this 被意外篡改。
5.2.4 new 绑定
当函数通过 new 关键字调用时,会创建一个全新的对象,并将这个新对象绑定为函数内部的 this。
这种场景下的函数我们称之为构造函数,尽管 JavaScript 中并没有“构造”和“普通”的严格区分——任何函数都可以用 new 调用,但通常约定首字母大写以示区别。
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 'Alice'
new 调用在背后完成了四件事(详见第 4 章):
- 创建一个全新的空对象。
- 将这个新对象的
[[Prototype]]指向构造函数的prototype。 - 将函数内部的
this绑定到这个新对象。 - 执行构造函数体,若无显式返回对象,则返回这个新对象。
所以,new 绑定创建了一个全新的上下文,其优先级高于其他所有绑定规则——但这一点我们在 5.3 节中会具体展开。
小结
| 绑定规则 | 调用方式 | this 指向 |
|---------|----------|----------|
| 默认绑定 | 独立函数调用 | 严格模式 undefined,非严格模式全局对象 |
| 隐式绑定 | obj.fn() | 调用时的上下文对象 |
| 显式绑定 | call/apply/bind | 指定的对象 |
| new 绑定 | new Fn() | 新创建的对象 |
这四种规则覆盖了 JavaScript 中 this 指向的绝大多数情况,也是面试和日常开发中最频繁用到的基础知识。在下一节,我们将讨论当同时使用多种规则时,this 最终如何确定——也就是优先级排序,并顺带拨开一些易混淆的特殊场景。