重提this指向

203 阅读4分钟

在 JavaScript 中,this 关键字是一个非常重要的概念,但也是一个常见的困惑点。它的值在函数执行时才确定,并且取决于函数的调用方式。理解 this 的绑定规则对于编写可预测和健壮的 JavaScript 代码至关重要,尤其是在前端开发中,涉及到 DOM 事件、组件化等场景。

this 的绑定规则主要有以下五种,它们之间存在优先级:

1. 默认绑定 (Default Binding)

这是最基本的 this 绑定规则,当函数作为独立函数被调用时,this 会指向全局对象。

  • 在浏览器环境中this 指向 window 对象。
  • 在严格模式 (Strict Mode) 下this 会绑定到 undefined。严格模式下,JavaScript 引擎会禁止 this 自动指向全局对象,这有助于避免意外的全局变量创建。

示例:

function showThis() {
  console.log(this);
}

showThis(); // 在浏览器中输出: Window 对象; 在严格模式下输出: undefined

function showStrictThis() {
  "use strict";
  console.log(this);
}

showStrictThis(); // 输出: undefined

2. 隐式绑定 (Implicit Binding)

当函数作为某个对象的方法被调用时,this 会隐式地绑定到那个对象。

  • 规则:谁调用了函数,this 就指向谁。
  • 注意this 总是指向调用它的直接对象,而不是更上层的对象。

示例:

const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

person.greet(); // 输出: Hello, my name is Alice (this 指向 person 对象)

const anotherPerson = {
  name: 'Bob',
  greet: person.greet // 将 person.greet 赋值给 anotherPerson.greet
};

anotherPerson.greet(); // 输出: Hello, my name is Bob (this 指向 anotherPerson 对象)

// 隐式丢失 (Implicitly Lost)
const greetFunc = person.greet;
greetFunc(); // 输出: Hello, my name is undefined (this 变成了默认绑定,指向 window/undefined)
             // 因为 greetFunc 是一个独立函数调用,没有通过任何对象来调用。

3. 显式绑定 (Explicit Binding)

你可以使用 call(), apply(), 或 bind() 方法来明确地指定函数执行时的 this 值。

  • call(thisArg, arg1, arg2, ...)

    • 立即执行函数。
    • 第一个参数是 this 的目标对象。
    • 后续参数是函数要接收的参数,以逗号分隔。
  • apply(thisArg, [argsArray])

    • 立即执行函数。
    • 第一个参数是 this 的目标对象。
    • 第二个参数是函数要接收的参数数组。
  • bind(thisArg, arg1, arg2, ...)

    • 不立即执行函数,而是返回一个新的函数
    • 这个新函数的 this 永远被绑定到 thisArg,即使后续尝试使用其他绑定规则也无法改变。
    • 可以预先传入部分参数(柯里化)。

示例:

function introduce(age, city) {
  console.log(`My name is ${this.name}, I am ${age} years old, and I live in ${city}.`);
}

const user = {
  name: 'Charlie'
};

// 使用 call
introduce.call(user, 30, 'New York'); // 输出: My name is Charlie, I am 30 years old, and I live in New York.

// 使用 apply
introduce.apply(user, [25, 'London']); // 输出: My name is Charlie, I am 25 years old, and I live in London.

// 使用 bind
const boundIntroduce = introduce.bind(user, 35); // 绑定 this 和第一个参数 age
boundIntroduce('Paris'); // 输出: My name is Charlie, I am 35 years old, and I live in Paris.
const anotherBoundIntroduce = introduce.bind(user); // 只绑定 this
anotherBoundIntroduce(40, 'Tokyo'); // 输出: My name is Charlie, I am 40 years old, and I live in Tokyo.

4. new 绑定 (New Binding / Constructor Call)

当使用 new 关键字调用一个函数(作为构造函数)时,会发生以下四件事:

  1. 创建一个全新的空对象。
  2. 这个新对象会被链接到构造函数的原型 (prototype)。
  3. 构造函数内部的 this 会被绑定到这个新创建的对象。
  4. 如果构造函数没有显式返回一个对象,那么 new 表达式会默认返回这个新创建的对象。如果构造函数显式返回了一个对象,那么将返回那个对象。

示例:

function Person(name, age) {
  this.name = name; // this 指向新创建的实例对象
  this.age = age;
  this.greet = function() {
    console.log(`Hello, my name is ${this.name}.`);
  };
}

const p1 = new Person('David', 40);
console.log(p1.name); // 输出: David
p1.greet(); // 输出: Hello, my name is David. (this 指向 p1 实例)

// 如果构造函数返回一个对象,则 new 表达式返回该对象
function SpecialObject() {
  this.value = 1;
  return { custom: 'special' }; // 显式返回一个对象
}
const s1 = new SpecialObject();
console.log(s1); // 输出: { custom: 'special' }
console.log(s1.value); // 输出: undefined (因为返回了 custom 对象)

// 如果构造函数返回非对象值,则忽略,仍返回新创建的实例
function AnotherObject() {
  this.value = 1;
  return 'hello'; // 返回非对象值
}
const a1 = new AnotherObject();
console.log(a1); // 输出: AnotherObject { value: 1 }

5. 箭头函数绑定 (Lexical Binding / Arrow Functions)

箭头函数 (Arrow Functions) 是 ES6 引入的一种特殊函数,它们没有自己的 this 绑定

  • 规则:箭头函数中的 this 值由其外层(词法)作用域决定,即它在定义时所处的最近的非箭头函数作用域的 this 值。
  • 不可改变:箭头函数的 this 一旦确定,就无法通过 call(), apply(), bind() 或其他方式来改变。

示例:

const obj = {
  name: 'Eve',
  sayHello: function() {
    // 这是一个普通函数,this 绑定到 obj
    setTimeout(function() {
      console.log(`Regular function: ${this.name}`); // this 默认绑定到 window/undefined
    }, 100);

    // 这是一个箭头函数,this 继承自外层 sayHello 函数的 this (即 obj)
    setTimeout(() => {
      console.log(`Arrow function: ${this.name}`); // this 绑定到 obj
    }, 200);
  }
};

obj.sayHello();
// 预期输出 (浏览器非严格模式):
// Regular function: undefined (或 Window 对象的 name 属性,通常是空字符串)
// Arrow function: Eve

// 另一个例子
function Counter() {
  this.count = 0;
  setInterval(() => {
    this.count++; // 这里的 this 始终指向 Counter 实例
    console.log(this.count);
  }, 1000);
}

// const counter = new Counter(); // 每秒输出 1, 2, 3...

绑定优先级

当多个规则可能同时适用时,this 的绑定会遵循以下优先级(从高到低):

  1. new 绑定:使用 new 关键字调用函数。

  2. 显式绑定:使用 call(), apply(), bind()

    • bind 的优先级高于 call/apply,因为 bind 会创建一个永久绑定的新函数。一旦被 bind 绑定,即使后续尝试用 call/apply 改变 this 也无效。
  3. 隐式绑定:函数作为对象的方法被调用。

  4. 默认绑定:独立函数调用(非严格模式下指向全局对象,严格模式下指向 undefined)。

箭头函数this 绑定不遵循上述任何规则,因为它根本就没有自己的 this。它的 this 总是词法继承自其外层非箭头函数作用域。因此,可以说箭头函数的 this 优先级最高,因为它“无视”了所有其他绑定规则。

优先级示例:

function foo() {
  console.log(this.a);
}

const obj1 = {
  a: 1,
  foo: foo
};

const obj2 = {
  a: 2
};

// 1. 默认绑定 (优先级最低)
// foo(); // 浏览器: undefined (或 Window.a), 严格模式: TypeError

// 2. 隐式绑定
obj1.foo(); // 输出: 1

// 3. 显式绑定 (高于隐式绑定)
obj1.foo.call(obj2); // 输出: 2 (尽管 obj1.foo 是 obj1 的方法,但 call 强制绑定到 obj2)

// 4. new 绑定 (高于显式绑定)
const bar = new obj1.foo(); // foo 被 new 调用,this 绑定到新创建的对象
// 输出: undefined (因为新创建的对象上没有 a 属性)
// 实际上,如果 foo 内部有 this.a = ... 这样的操作,new 会使其生效。

// 5. 箭头函数 (词法绑定,不参与优先级竞争,因为它没有自己的 this)
const obj3 = {
  a: 3,
  arrowFoo: () => {
    console.log(this.a); // 这里的 this 继承自 obj3 所在的外层作用域 (全局作用域)
  }
};
obj3.arrowFoo(); // 浏览器: undefined (或 Window.a)

理解这些规则是掌握 JavaScript 异步编程、面向对象编程和组件化开发的关键。在实际开发中,尤其是在事件处理函数、回调函数和类方法中,this 的指向问题经常出现,需要仔细分析其调用上下文。