JavaScript:这是什么意思?

在 JavaScript 中算出 this 的值可能比较困难,具体方法如下...

杰克·阿奇博尔德
Jake Archibald

JavaScript 的 this 是许多笑话的支柱,其原因在于它相当复杂。不过,我发现开发者会采取更复杂且特定领域的任务,以避免处理此 this。如果您不确定 this,希望这会对您有所帮助。这是我的this指南。

我将从最具体的情况开始,并以最不具体的情况收尾。本文有点像一个大 if (…) … else if () … else if (…) …,因此您可以直接前往与您正在查看的代码匹配的第一部分。

  1. 如果函数被定义为箭头函数
  2. 否则,如果使用 new 调用函数/类
  3. 否则,如果该函数具有“绑定”的this
  4. 否则,如果在通话时设置了 this
  5. 否则,如果通过父级对象 (parent.func()) 调用该函数
  6. 否则,如果函数或父级作用域处于严格模式
  7. 否则

如果函数被定义为箭头函数:

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

在这种情况下,this 的值始终与父级作用域中的 this 相同:

const outerThis = this;

const arrowFunction = () => {
  // Always logs `true`:
  console.log(this === outerThis);
};

箭头函数非常实用,因为 this 的内部值无法更改,而其始终与外部 this 相同。

其他示例

使用箭头函数时,无法通过 bind 更改 this 的值:

// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();

使用箭头函数时,无法通过 callapply 更改 this 的值:

// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});

使用箭头函数时,不能通过将该函数作为另一个对象的成员调用来更改 this 的值:

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();

使用箭头函数时,不能通过将该函数作为构造函数调用来更改 this 的值:

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

“绑定”实例方法

对于实例方法,如果您想确保 this 始终引用类实例,最好的方法是使用箭头函数和类字段

class Whatever {
  someMethod = () => {
    // Always the instance of Whatever:
    console.log(this);
  };
}

在组件(如 React 组件或 Web 组件)中将实例方法用作事件监听器时,此模式非常有用。

上面的代码可能看起来破坏了“this 将与父作用域中的 this 相同”规则,但如果您将类字段视为在构造函数中进行设置时的语法糖,那么这会变得合理:

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

// …is roughly equivalent to:

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

替代模式涉及在构造函数中绑定现有函数,或在构造函数中分配函数。如果您出于某种原因无法使用类字段,在构造函数中分配函数是一种合理的替代方案:

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

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

new Whatever();

以上代码将调用 Whatever(如果是类,则调用其构造函数函数),并将 this 设置为 Object.create(Whatever.prototype) 的结果。

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

// Logs `true`:
new MyClass();

旧式构造函数也是如此:

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

// Logs `true`:
new MyClass();

其他示例

使用 new 调用时,无法通过 bind 更改 this 的值:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

使用 new 进行调用时,无法通过将该函数作为另一个对象的成员调用来更改 this 的值:

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

否则,如果该函数具有“绑定”this 值:

function someFunction() {
  return this;
}

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

每次调用 boundFunction 时,其 this 值都将是传递给 bind (boundObject) 的对象。

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

其他示例

在调用绑定函数时,无法通过 callapply 更改 this 的值:

// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);

在调用绑定函数时,无法更改 this 的值,方法是将该函数作为另一个对象的成员调用:

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

否则,如果在通话时设置了 this

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

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

this 的值是传递给 call/apply 的对象。

遗憾的是,this 被 DOM 事件监听器等元素设置为其他值,使用它可能会导致代码难以理解:

错误做法
element.addEventListener('click', function (event) {
  // Logs `element`, since the DOM spec sets `this` to
  // the element the handler is attached to.
  console.log(this);
});

在上述情况中,我避免使用 this,而是:

正确做法
element.addEventListener('click', (event) => {
  // Ideally, grab it from a parent scope:
  console.log(element);
  // But if you can't do that, get it from the event object:
  console.log(event.currentTarget);
});

否则,如果通过父对象 (parent.func()) 调用函数:

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

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

在本例中,函数作为 obj 的成员进行调用,因此 this 将为 obj。这发生在调用时,因此,如果在没有父对象的情况下或使用不同的父对象调用该函数,链接就会被破坏:

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

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

someMethod() === obj 为 false,因为没有将 someMethod 作为 obj 的成员调用。您在尝试如下操作时,可能会遇到此问题:

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

这会中断,因为 querySelector 的实现会查看自己的 this 值,并预期该节点是某种类型的 DOM 节点,而上述代码会断开该连接。要正确实现上述目标,请执行以下操作:

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

趣味小知识:并非所有 API 在内部都使用 this。更改了 console.log 等控制台方法以避免 this 引用,因此 log 无需绑定到 console

否则,如果函数或父级范围处于严格模式:

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

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

在本例中,this 的值未定义。如果父范围处于严格模式(并且所有模块都处于严格模式),则函数中不需要 'use strict'

否则:

function someFunction() {
  return this;
}

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

在本例中,this 的值与 globalThis 相同。

呼!

这样就大功告成了!以上就是我所知道的关于this的所有信息。有什么问题吗? 有没有错过什么内容?欢迎随时发推文

感谢 Mathias BynensIngvar StepanyanThomas Steiner 的审核。