JS this 3 学习 + 面试题

64 阅读6分钟

四种绑定的优先级

以下是 this 绑定的优先级顺序,从 最高 到 最低

  1. new 绑定 (New Binding)

    • 触发方式:  使用 new 关键字调用函数(构造函数调用)。
    • this 指向:  新创建的对象实例。
    • 优先级最高:  如果函数是通过 new 调用的,this 始终指向新创建的对象,即使该函数之前被 bind, call, 或 apply 处理过。
  2. 显式绑定 (Explicit Binding)

    • 触发方式:  使用 .call(), .apply(), 或 .bind() 方法调用函数。
    • this 指向:  显式传入 call/apply/bind 的第一个参数(thisArg)。
    • 优先级次高:  如果使用了显式绑定,它会覆盖隐式绑定和默认绑定。bind() 创建的函数具有“硬绑定”特性,其 this 不能被后续的 call 或 apply 修改(但会被 new 覆盖)。
  3. 隐式绑定 (Implicit Binding)

    • 触发方式:  函数作为对象的方法被调用(例如 obj.method() 或 obj'method')。
    • this 指向:  调用该方法的上下文对象(点 . 或方括号 [] 左边的对象)。
    • 优先级居中:  如果函数是作为方法调用的,并且没有应用显式绑定或 new 绑定,则 this 指向该对象。
  4. 默认绑定 (Default Binding)

    • 触发方式:  独立的、直接的函数调用(例如 myFunction()),不满足以上任何一种绑定规则。

    • this 指向:

      • 严格模式 ('use strict')下:  undefined。
      • 非严格模式下:  全局对象 (window 在浏览器中,global 在 Node.js 中)。
    • 优先级最低:  这是最后的备用规则,只有在其他绑定都不适用时才会生效。

总结记忆口诀/流程:

当判断一个函数调用中的 this 指向时,按以下顺序检查:

  1. 是 new 调用吗?  如果是,this 就是新创建的对象。(new 绑定)

  2. 是 call, apply, 或 bind 调用吗?  如果是,this 就是显式指定的那个对象。(显式绑定)

  3. 是作为对象方法调用吗 (obj.method())?  如果是,this 就是那个对象 (obj)。( 隐式绑定)

  4. 都不是?  那么:

    • 严格模式下,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 的实例'
    

预期输出及中文解释:

  1. 默认绑定:

    • 在非严格模式下,this.name 会输出 '全局 Window/Global',this 指向全局对象 (window 或 global)。
    • 在严格模式下,访问 this.name 会失败 (TypeError),this 指向 undefined。
  2. 隐式绑定: 调用 obj1.identifyMethod() 时,this 指向 obj1,所以 this.name 输出 '对象 1'。

  3. 显式绑定:

    • 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 的硬绑定特性及其优先级高于隐式和后续的显式调用。
  4. 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) 优先级次之,能覆盖隐式和默认绑定;隐式绑定(方法调用)优先级再次,能覆盖默认绑定;默认绑定是最低优先级的后备规则。