【Dmitri Pavlutin】如何輕鬆處理JavaScript的this

翻譯:道奇
做者:Dmitri Pavlutin
原文:How to Handle Easily 'this' in JavaScriptjavascript

我喜歡JavaScript改變執行上下文的能力,也被稱爲this
舉個例子,能夠在類數組的對象上使用數組方法:java

const reduce = Array.prototype.reduce;

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

sumArgs(1, 2, 3); // => 6
複製代碼

另外一方面,this關鍵字很難掌握。你可能常常在查找爲何this獲得的值是不正確的。下面的章節會告訴你一些簡單的方法,如何將this綁定到所需的值上。git

在開始前,須要一個幫助函數execute(func),它以參數的形式執行函數:github

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是個新建的實例。bash

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

若是agent.getFullName方法由幫助函數Execute執行會發生什麼呢:優化

execute(agent.getFullName); // => 'undefined undefined'
複製代碼

執行結果會不對: 'undefined undefined',緣由是this的值不正確。ui

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綁定到類實例上。

相關文章
相關標籤/搜索