【Dmitri Pavlutin】如何轻松处理JavaScript的this

213 阅读5分钟

翻译:道奇
作者:Dmitri Pavlutin
原文:How to Handle Easily 'this' in JavaScript

我喜欢JavaScript改变执行上下文的能力,也被称为this
举个例子,可以在类数组的对象上使用数组方法:

const reduce = Array.prototype.reduce;

function sumArgs() {
  return reduce.call(arguments, (sum, value) => {
    return sum += value;
  });
}

sumArgs(1, 2, 3); // => 6

另一方面,this关键字很难掌握。你可能经常在查找为什么this得到的值是不正确的。下面的章节会告诉你一些简单的方法,如何将this绑定到所需的值上。

在开始前,需要一个帮助函数execute(func),它以参数的形式执行函数:

function execute(func) {
  return func();
}

execute(function() { return 10 }); // => 10

现在继续理解围绕this错误的本质:方法分离。

1.方法分离问题

Person类包含字段firstNamelastName,另外还有个返回person全名的getFullName()方法。
Person的一种实现可能是:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;

  this.getFullName = function() {
    this === agent; // => true
    return `${this.firstName} ${this.lastName}`;
  }
}

const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'

可以看到Person函数被当作构造函数调用:new Person('Jonh','Smith'),在Person函数内部,this是个新建的实例。

agent.getFullName返回person的全名:'John Smith'。如你所料,getFullName()方法内部的this等于agent

如果agent.getFullName方法由帮助函数Execute执行会发生什么呢:

execute(agent.getFullName); // => 'undefined undefined'

执行结果会不对: 'undefined undefined',原因是this的值不正确。

getFullName()内部this的值指向的是全局对象(在浏览器环境下就是window对象)。因为this等于window,求值表达式${window.firstName} ${window.lastName}的结果是'undefined undefined'。

这是因为当调用execute(agent.getFullName)函数时,函数与对象分离了。基本上就相当于只是一个普通的函数调用(而不是方法调用):

execute(agent.getFullName); // => 'undefined undefined'

//等于:

const getFullNameSeparated = agent.getFullName;
execute(getFullNameSeparated); // => 'undefined undefined'

这就是为什么我说 “函数与对象分离”。当方法分离后再执行,它就和它所在的源对象没有关联了。

如果要确保方法内的this指向准确的对象,就必须:

  1. 以属性访问的方式执行方法:agent.getFullName()
  2. 或者将this静态的绑定到所在的对象(使用箭头函数,.bind()方法等等)

方法分离问题,导致this的值不对,会呈现多种不同的情况:

设置回调时

// `this` 在 `methodHandler()` 内部是全局对象
setTimeout(object.handlerMethod, 1000);

设置事件处理器时

// React: `this` 在 `methodHandler()`内部是全局对象
<button onClick={object.handlerMethod}>
  Click me
</button>

尽管方法和对象分离了,还是有一些可以将this指向所需对象的好用的方法。

2. 关闭上下文

最简单的方式就是使用额外的变量selfthis指向类的实例:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;

  const self = this;

  this.getFullName = function() {
    self === agent; // => true
    return `${self.firstName} ${self.lastName}`;
  }
}

const agent = new Person('John', 'Smith');

agent.getFullName();        // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'

getFullName()通过self变量,将方法手动绑定到this上。

现在调用execute(agent.getFullName)代码将正常执行,返回 'John Smith',因为getFullName()方法总是得到正确this值。

3. 箭头函数中this的词法

如果不通过额外的变量有没有方法静态的绑定this呢?是的,箭头函数就是做这个事情的。
使用箭头函数重构一下person:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;

  this.getFullName = () => `${this.firstName} ${this.lastName}`;
}

const agent = new Person('John', 'Smith');

agent.getFullName();        // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'

箭头函数只是从词法上绑定this,简单的说,它使用外部函数定义this的值。

当你要使用外部函数的上下文时,我推荐在所有的情况下都使用箭头函数。

4. 绑定上下文

现在我们再往前一步,使用ES2015的类来重构Person

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const agent = new Person('John', 'Smith');

agent.getFullName();        // => 'John Smith'
execute(agent.getFullName); // => 'undefined undefined'

很不幸,即使使用最新的类语法,execute(agent.getFullName)还是返回'undefined undefined'。在类里,使用额外变量self或箭头函数来固定this的值是行不通的。
但是有个小技巧是使用bind()方法在构造函数里绑定方法的上下文:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;

    this.getFullName = this.getFullName.bind(this);
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const agent = new Person('John', 'Smith');

agent.getFullName();        // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'

构造函数内部this.getFullName = this.getFullName.bind(this)这行语句将getFullName()方法绑定到了类实例上。

execute(agent.getFullName) 按预期的那样执行,返回'John Smith'。

5. 胖箭头方法

上面的方法通过手动绑定上下文需要写一行样板代码(this.getFullName = this.getFullName.bind(this);)。幸运的是,还有优化的空间。

可以使用JavaScript的类字段的建议允许定义胖箭头方法:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName = () => {
    return `${this.firstName} ${this.lastName}`;
  }
}

const agent = new Person('John', 'Smith');

agent.getFullName();        // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'

就算将方法和它的对象分离,胖箭头方法getFullName = () => { ... }绑定还是会绑定到类实例上。这种是在类中绑定this最有效且最简洁的方式了。

6. 总结

方法和它的对象分离导致了很多关于this的误解,需要意识到这种影响。

为了静态绑定this,可以手动的使用额外的变量self关联正确的上下文对象。但是,更好的选择是使用箭头函数,它的语法上是天然设计绑定this的。

在类中,可以在构造函数中使用bind()方法手动地绑定类方法。

如果你不想写样板代码,新的JavaScript的类字段建议增加了胖箭头方法可以自动的将this绑定到类实例上。