四种绑定的优先级
以下是 this 绑定的优先级顺序,从 最高 到 最低:
-
new 绑定 (New Binding)
- 触发方式: 使用 new 关键字调用函数(构造函数调用)。
- this 指向: 新创建的对象实例。
- 优先级最高: 如果函数是通过 new 调用的,this 始终指向新创建的对象,即使该函数之前被 bind, call, 或 apply 处理过。
-
显式绑定 (Explicit Binding)
- 触发方式: 使用 .call(), .apply(), 或 .bind() 方法调用函数。
- this 指向: 显式传入 call/apply/bind 的第一个参数(thisArg)。
- 优先级次高: 如果使用了显式绑定,它会覆盖隐式绑定和默认绑定。bind() 创建的函数具有“硬绑定”特性,其 this 不能被后续的 call 或 apply 修改(但会被 new 覆盖)。
-
隐式绑定 (Implicit Binding)
- 触发方式: 函数作为对象的方法被调用(例如 obj.method() 或 obj'method')。
- this 指向: 调用该方法的上下文对象(点 . 或方括号 [] 左边的对象)。
- 优先级居中: 如果函数是作为方法调用的,并且没有应用显式绑定或 new 绑定,则 this 指向该对象。
-
默认绑定 (Default Binding)
-
触发方式: 独立的、直接的函数调用(例如 myFunction()),不满足以上任何一种绑定规则。
-
this 指向:
- 严格模式 ('use strict')下: undefined。
- 非严格模式下: 全局对象 (window 在浏览器中,global 在 Node.js 中)。
-
优先级最低: 这是最后的备用规则,只有在其他绑定都不适用时才会生效。
-
总结记忆口诀/流程:
当判断一个函数调用中的 this 指向时,按以下顺序检查:
-
是 new 调用吗? 如果是,this 就是新创建的对象。(new 绑定)
-
是 call, apply, 或 bind 调用吗? 如果是,this 就是显式指定的那个对象。(显式绑定)
-
是作为对象方法调用吗 (obj.method())? 如果是,this 就是那个对象 (obj)。( 隐式绑定)
-
都不是? 那么:
- 严格模式下,this 是 undefined。
- 非严格模式下,this 是全局对象。(默认绑定)
特别注意:箭头函数 (=>)
箭头函数不遵循上述任何绑定规则。它们没有自己的 this 绑定。箭头函数内部的 this 值由其定义时所在的词法作用域(lexical scope)决定,并且一旦确定就不会改变。你可以认为箭头函数的 this 优先级是“无限高”,因为它在定义时就已经确定了,完全不受调用方式的影响。
代码案例
好的,这是用中文注释和解释的代码,用于验证 JavaScript 中 this 绑定的优先级:
new 绑定 > 显式绑定 (call/apply/bind) > 隐式绑定 > 默认绑定
// ----- 准备工作 -----
// 1. 全局上下文 (用于非严格模式下的默认绑定)
// 使用 'var' 使其成为全局对象的属性 (浏览器是 window, Node.js 是 global)
var name = '全局 Window/Global';
// 2. 一个简单的测试 'this' 的函数
function identify(callerInfo) {
console.log(`--- 调用者: ${callerInfo} ---`);
// 使用 try-catch 处理严格模式下 'this' 可能为 undefined 的情况
try {
console.log(`this.name = ${this.name}`);
} catch (e) {
console.log(`访问 this.name 失败: ${e.message}`);
}
console.log(`this = ${this}`); // 显示 'this' 实际是什么
console.log('--------------------------\n');
}
// 函数的严格模式版本
function identifyStrict(callerInfo) {
'use strict'; // 启用严格模式
console.log(`--- 调用者: ${callerInfo} (严格模式) ---`);
try {
console.log(`this.name = ${this.name}`);
} catch (e) {
console.log(`访问 this.name 失败: ${e.message}`);
}
console.log(`this = ${this}`);
console.log('--------------------------\n');
}
// 3. 用于隐式和显式绑定测试的对象
const obj1 = {
name: '对象 1',
identifyMethod: identify // 用于隐式绑定的方法
};
const obj2 = {
name: '对象 2'
};
const explicitContext = { // 用于显式绑定的上下文对象
name: '显式绑定的上下文'
};
// ----- 验证测试 -----
console.log(">>> 验证默认绑定 (最低优先级) <<<");
identify("默认绑定 (非严格)");
identifyStrict("默认绑定 (严格)"); // 'this' 应该是 undefined
// ---
console.log(">>> 验证隐式绑定 (优先级 > 默认绑定) <<<");
obj1.identifyMethod("隐式绑定 (obj1.identifyMethod)"); // 'this' 应该是 obj1
// ---
console.log(">>> 验证显式绑定 (优先级 > 隐式绑定) <<<");
// 即使我们通过 obj1 调用 identifyMethod, .call() 也会覆盖隐式的 'this'
obj1.identifyMethod.call(explicitContext, "显式绑定 (.call)"); // 'this' 应该是 explicitContext
obj1.identifyMethod.apply(obj2, ["显式绑定 (.apply)"]); // 'this' 应该是 obj2
console.log(">>> 验证显式绑定 (.bind) <<<");
const boundToObj2 = identify.bind(obj2); // 创建一个永久绑定到 obj2 的函数
boundToObj2("显式绑定 (.bind 的结果)"); // 'this' 应该是 obj2
// 演示 .bind() 也会覆盖隐式绑定,即使绑定后的函数作为方法调用
const obj3 = { name: '对象 3', identifyMethod: boundToObj2 };
obj3.identifyMethod("对 BIND 后的函数进行隐式调用"); // 'this' 仍然是 obj2, 而不是 obj3
// 演示 .bind() 是“硬绑定” - 不能被后续的 call/apply 覆盖
boundToObj2.call(obj1, "尝试对 BIND 后的函数使用 .call"); // 'this' 仍然是 obj2
// ---
console.log(">>> 验证 'new' 绑定 (最高优先级, 覆盖显式绑定) <<<");
function ConstructorExample(name, callerInfo) {
// 在用 'new' 调用的函数内部, 'this' 是新创建的对象。
this.name = name; // 在新对象上设置属性
console.log(`--- 调用者: ${callerInfo} ---`);
console.log(`构造函数中的 this.name = ${this.name}`);
console.log(`构造函数中的 this = ${this}`); // 显示新创建的实例对象
console.log('--------------------------\n');
}
const explicitForNew = { name: '显式上下文 (会被 new 忽略)' };
// 1. 首先绑定构造函数 (尝试显式绑定)
const BoundConstructor = ConstructorExample.bind(explicitForNew, '绑定的名称');
// 2. 现在用 'new' 调用这个 BIND 后的构造函数
// 'new' 绑定优先于 .bind()!
// 'this' 将是一个 *新* 对象, 而不是 'explicitForNew'.
// 传递给 'new' 的参数可以覆盖由 bind 预设的参数 (如果构造函数使用了这些参数)。
const instance1 = new BoundConstructor('来自 new 的实例名称', "'new' 调用 BIND 后的构造函数");
// 3. 测试 new 绑定覆盖了先前的 bind
console.log(">>> 通过 'new' 调用绑定后的构造函数创建的实例:");
console.log("instance1.name =", instance1.name); // 应该是 '来自 new 的实例名称'
// 4. 普通的 'new' 调用作为对比
const instance2 = new ConstructorExample('直接 New 的实例', "'new' 调用原始构造函数");
console.log(">>> 通过直接 'new' 创建的实例:");
console.log("instance2.name =", instance2.name); // 应该是 '直接 New 的实例'
预期输出及中文解释:
-
默认绑定:
- 在非严格模式下,this.name 会输出 '全局 Window/Global',this 指向全局对象 (window 或 global)。
- 在严格模式下,访问 this.name 会失败 (TypeError),this 指向 undefined。
-
隐式绑定: 调用 obj1.identifyMethod() 时,this 指向 obj1,所以 this.name 输出 '对象 1'。
-
显式绑定:
- obj1.identifyMethod.call(explicitContext, ...): call 强制将 this 指向 explicitContext,输出 '显式绑定的上下文'。它覆盖了由 obj1. 提供的隐式上下文。
- obj1.identifyMethod.apply(obj2, ...): apply 将 this 指向 obj2,输出 '对象 2'。
- boundToObj2(...): 调用由 bind(obj2) 创建的函数,this 始终是 obj2,输出 '对象 2'。
- 即使 boundToObj2 作为 obj3 的方法调用 (obj3.identifyMethod(...)) 或尝试用 call(obj1) 再次改变它,this 仍然是 obj2,证明了 bind 的硬绑定特性及其优先级高于隐式和后续的显式调用。
-
new 绑定:
-
当执行 new BoundConstructor('来自 new 的实例名称', ...) 时:
- JavaScript 创建了一个新的空对象。
- ConstructorExample 函数被调用,其内部的 this 被设置为这个新对象,完全忽略了之前 BoundConstructor 通过 .bind(explicitForNew) 绑定的 explicitForNew 对象。
- 传递给 new 的 name 参数 ('来自 new 的实例名称') 被用来设置新实例的 this.name,覆盖了 bind 时预设的 '绑定的名称'。
- 最终 instance1 指向这个新创建的对象,其 name 属性是 '来自 new 的实例名称'。
-
this 绑定的优先级规则:new 调用具有最高优先级,能覆盖所有其他绑定方式;显式绑定 (call, apply, bind) 优先级次之,能覆盖隐式和默认绑定;隐式绑定(方法调用)优先级再次,能覆盖默认绑定;默认绑定是最低优先级的后备规则。