Bootstrap

【译】JavaScript: 带你彻底搞懂 this

  • 原文地址:

  • 原文作者:

  • 原文发布时间:2021-03-08

  • 本文永久链接:

  • 翻译、校对:

搞明白 JavaScript 中 的值有时候会很棘手,本文带你彻底搞懂 。

JavaScript 的 往往会成为许多笑话的笑柄,因为它相当复杂。然而,我发现很多开发人员为了避免处理 ,用了更加复杂和特定领域的处理。如果你对 还不熟悉,希望本文能帮助到你。下面进入我的 指南。

我将从最具体的情况开始,以最不具体的情况结束,本文的结构类似与一个大的 语句,所以你可以直接跳转到匹配你代码情况的章节。

如果是箭头函数:

const arrowFunction = () => {
  console.log(this);
};

在这种情况下, 的值永远与父作用域的 相同。

const outerThis = this;

const arrowFunction = () => {
  // 永远输出 `true`:
  console.log(this === outerThis);
};

箭头函数非常优秀,因为其内部 的值无法被改变,它与外部的 永远 相同。

其他例子

使用箭头函数, 的值无法 改变:

// 输出为 `true` - bind `this` 被忽略:
arrowFunction.bind({foo: 'bar'})();

使用箭头函数, 的值无法 改变:

// 输出为 `true` - call `this` 被忽略:
arrowFunction.call({foo: 'bar'});
// 输出为 `true` - apply `this` 被忽略:
arrowFunction.apply({foo: 'bar'});

使用箭头函数, 的值无法通过将函数作为另一个对象的成员变量来调用改变:

const obj = {arrowFunction};
// 输出为 `true` - 父对象被忽略:
obj.arrowFunction();

使用箭头函数, 的值无法通过将函数作为构造函数来调用而改变:

// TypeError: arrowFunction is not a constructor
new arrowFunction();

“绑定” 实例方法

对于实例方法,如果想要确保 始终指向类实例,最好的方法是使用箭头函数和

class Whatever {
  someMethod = () => {
    // 永远是 Whatever 的实例:
    console.log(this);
  };
}

这个模式在将实例方法作为组件内的事件监听器时十分有用(如 React 组件或者 Web Components)。

上面的代码貌似打破了“ 的值永远与父作用域的 相同”的规则,但是如果你将 class fields 看作将对象设置到构造函数的语法糖,那么就好理解了:

class Whatever {
  someMethod = (() => {
    const outerThis = this;
    return () => {
      // 永远输出 `true`:
      console.log(this === outerThis);
    };
  })();
}

// …大致等于:

class Whatever {
  constructor() {
    const outerThis = this;
    this.someMethod = () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  }
}

其他模式包括在构造函数中绑定现有函数,或在构造函数中对函数赋值。如果你由于某种原因不能使用 class fields,则在构造函数中对函数赋值是一种合理的选择:

class Whatever {
  constructor() {
    this.someMethod = () => {
      // …
    };
  }
}

否则,如果使用 调用函数/类:

new Whatever();

上面代码会调用 (或者它的构造函数,如果它是类),并将 设置为 的结果。

class MyClass {
  constructor() {
    console.log(
      this.constructor === Object.create(MyClass.prototype).constructor,
    );
  }
}

// 输出为 `true`:
new MyClass();

使用旧式的构造函数结果也一样:

function MyClass() {
  console.log(
    this.constructor === Object.create(MyClass.prototype).constructor,
  );
}

// 输出 `true`:
new MyClass();

其他例子

使用 调用, 的值无法 改变:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// 输出为 `true` - bind `this` 被忽略:
new BoundMyClass();

使用 调用, 的值无法通过将函数作为另一个对象的成员变量来调用改变:

const obj = {MyClass};
// 输出为 `true` - 父对象被忽略:
new obj.MyClass();

否则, 函数被 了 :

function someFunction() {
  return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

每当 被调用,它的 值就是通过 传入的值()。

// 输出 `false`:
console.log(someFunction() === boundObject);
// 输出 `true`:
console.log(boundFunction() === boundObject);

Warning: 避免使用 将函数绑定到其外部的 。使用替代,因为这样 可以在函数声明就能清楚地看出来,而非在后续代码中看到。不要使用 设置 为与父对象无关的值;这通常是出乎意料的,这也是 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。

其他例子

使用 调用函数, 的值无法 改变:

// 输出为 `true` - call `this` 被忽略:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// 输出为 `true` - apply `this` 被忽略:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);

使用 调用函数, 的值无法通过将函数作为另一个对象的成员变量来调用改变:

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);

否则, 如果 在调用时设置:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

// 输出 `true`:
console.log(someFunction.call(someObject) === someObject);
// 输出 `true`:
console.log(someFunction.apply(someObject) === someObject);

的值就是传递给 / 的对象。

警告: 不要使用 设置 为与父对象无关的值;这通常是出乎意料的,这也是 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。

不幸的是, 可能会被如 DOM 事件监听器之类的函数设置为其他值,使用它会导致代码难以理解:

不要这样:

element.addEventListener('click', function (event) {
  // 输出 `element`, 因为 DOM 将 `this` 设置为
  // click 绑定的元素上
  console.log(this);
});

我会避免在上述场景中使用 ,我会这样使用:

element.addEventListener('click', (event) => {
  // 理想情况, 从父作用域获得它:
  console.log(element);
  // 但是如果你不想这么做,可以从 event 对象获取它:
  console.log(event.currentTarget);
});

否则, 如果使用父对象() 调用函数:

const obj = {
  someMethod() {
    return this;
  },
};

// 输出 `true`:
console.log(obj.someMethod() === obj);

在这种情况下,函数作为 的成员变量被调用,所以 指向 。这是在调用时发生的,因此如果没有使用父对象调用,或者使用一个不同的父对象调用,该连接会断开:

const {someMethod} = obj;
// 输出 `false`:
console.log(someMethod() === obj);

const anotherObj = {someMethod};
// 输出 `false`:
console.log(anotherObj.someMethod() === obj);
// 输出 `true`:
console.log(anotherObj.someMethod() === anotherObj);

为 ,因为 不是 作为 的成员变量被调用的。尝试执行以下操作时,可能会遇到此陷阱:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

这个报错是因为 实现会寻找它的 值,并期望其某种 DOM 节点,上述代码破坏了连接。为了正确实现上述功能,可以这样写:

const $ = document.querySelector.bind(document);
// 或者:
const $ = (...args) => document.querySelector(...args);

有趣的事实:并不是所有的 API 都在其内部使用了 。Console 方法(如 )就改为了不使用 引用,因此 方法不需要绑定 。

警告: 不要使用 设置 为与父对象无关的值;这通常是出乎意料的,这也是 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。

否则, 如果函数或者其父作用域使用严格模式:

function someFunction() {
  'use strict';
  return this;
}

// 输出 `true`:
console.log(someFunction() === undefined);

在这种情况下, 的值是 。如果父作用域处于(而且所有模块都处在严格模式),则不需要在函数内部使用 。

警告: 不要依赖这个。我的意思是,有更简单的方式来得到一个 值 😀。

否则:

function someFunction() {
  return this;
}

// 输出 `true`:
console.log(someFunction() === globalThis);

在这种情况下, 的值与 相同。

很多人(包括我)把 称为 对象,但这不是 100% 技术正确的。在 中,有它为什么叫 而不是 的原因。

警告: 避免使用 指向 对象(对,我仍然这么叫它)。改为使用,它更加明确。

结语

好了,这就是我理解的 的全部了。如果有任何问题或者我有所遗漏,请

感谢 , , 和 的审阅。