钱行慕

导航

Javascript: 你真的了解this的含义吗

原文链接:传送门

Javascript 的 this是许多笑话的笑点所在,那是因为它真的很复杂。然而,我见过许多开发者做了许多更加复杂的和特定域的事情来避免与this打交道。如果你对this还有点不太确定,我希望这篇文章可以对你有所帮助。

这篇文章是我的关于this对一个指导。

我将以最明确的场景开始,然后以最不明确的场景结束。这篇文章有点像一个大的if (…) … else if () … else if (…)结构,因此你可以直接导航到匹配你正在看的代码的那个部分。

如果函数被定义为一个箭头函数

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

在这个情况下,this的值总是和父作用域中this的值一样。

const outerThis = this;

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

箭头函数是强大的,这是因为其内部的this值不会被改变,其总是与外部的this值保持一致,让我们继续看看其他示例。

在箭头函数中,this的值不能以bind来改变:

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

在箭头函数中,this的值不能以call或者appy来改变。

// 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();

对于老式的构造函数来说,同样也是true。

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

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

其他示例。

当以new的方式被调用时,this的值不能以bind来改变。

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);

⚠️避免使用bind来绑定一个函数到它的外部this,相应的,我们应该使用箭头函数,因为从它的函数声明中,其使得this的意义比较明确,而不是晚一点在代码中发生的那些事。不要使用bind来设置this到一些与父对象不想关的值。它通常不是我们所其期望的而且这也是为什么this获得了这么坏的一个名声的原因。相应的我们应该考虑传递一个值作为参数。它更加的明确,并与箭头函数工作良好。

其他示例

当调用一个bound函数时,this不能以call或者apply来改变。

// 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);

当调用一个bound函数时,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的对象。

⚠️不要使用call/apply来将this的值设置为与父对象无关的值。这通常不是我们所期望的结果,而且这也是为什么this获得了如此坏的名声的原因。考虑传递一个值作为参数来替代。其更加明确并且与箭头函数工作良好。

不幸的是,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);

一个有趣的事实:并不是所有的APIs会在内部使用this。像console.log这样的控制台方法被更改为避免使用this引用,因此log便没有必要被绑定到console。

⚠️不要仅通过将this设置为与父对象不相关的值来将一个函数嫁接到一个对象上。它通常不是我们所期望的并会给this带来坏的名声。考虑作为参数传递一个值来代替。它更加显式,并与箭头函数工作良好。

如果函数或者父作用域处于一个strict模式下

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

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

在这种情况下,this的值会是underfined。如果父作用域已经是strict模式了,那么在函数中“use strict”便不是必须的了。

⚠️不要依赖这个,我的意思是,有更简单的方式来获取underfined值。

其他场景

function someFunction() {
  return this;
}

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

在这个场景下,this的值是与globalThis一样的。

⚠️避免使用this来引用全局对象。相反,请显式的使用globalThis。

【完结】

 

posted on 2021-05-30 20:08  钱行慕  阅读(94)  评论(0编辑  收藏  举报