【JS 口袋書】第 8 章:以更細的角度來看 JS 中的 this

做者:valentinogagliardi
譯者:前端小智
來源:github

阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...

爲了保證的可讀性,本文采用意譯而非直譯。html

揭祕 "this"

JS 中的this關鍵字對於初學者來講是一個謎,對於經驗豐富的開發人員來講則是一個永恆的難題。this 其實是一個移動的目標,在代碼執行過程當中可能會發生變化,而沒有任何明顯的緣由。首先,看一下this關鍵字在其餘編程語言中是什麼樣子的。
如下是 JS 中的一個 Person 類:前端

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("Hello " + this.name);
  }
}

Python 類也有一個跟 this 差很少的東西,叫作selfgit

class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return 'Hello' + self.name

Python類中,self表示類的實例:即從類開始建立的新對象github

me = Person('Valentino')

PHP中也有相似的東西:編程

class Person {
    public $name; 

    public function __construct($name){
        $this->name = $name;
    }

    public function greet(){
        echo 'Hello ' . $this->name;
    }
 }

這裏$this是類實例。再次使用JS類來建立兩個新對象,能夠看到每當我們調用object.name時,都會返回正確的名字:json

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("Hello " + this.name);
  }
}

const me = new Person("前端小智");
console.log(me.name); // '前端小智'

const you = new Person("小智");
console.log(you.name); // '小智'

JS 中相似乎相似於PythonJavaPHP,由於 this 看起來彷佛指向實際的類實例?segmentfault

這是不對的。我們不要忘記JS不是一種面向對象的語言,並且它是寬鬆的、動態的,而且沒有真正的類。this與類無關,我們能夠先用一個簡單的JS函數(試試瀏覽器)來證實這一點:數組

function whoIsThis() {
  console.log(this);
}

whoIsThis();

規則1:回到全局「this」(即默認綁定)

若是在瀏覽器中運行如下代碼瀏覽器

function whoIsThis() {
  console.log(this);
}

whoIsThis();

輸出以下:安全

Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

如上所示,我們當 this 沒有在任何類中的時候,this 仍然有值。當一個函數在全局環境中被調用時,該函數會將它的this指向全局對象,在我們的例子中是window

這是JS的第一條規則,叫做默認綁定。默認綁定就像一個回退,大多數狀況下它是不受歡迎的。在全局環境中運行的任何函數均可能「污染」全局變量並破壞代碼。考慮下面的代碼:

function firstDev() {
  window.globalSum = function(a, b) {
    return a + b;
  };
}

function nastyDev() {
  window.globalSum = null;
}

firstDev();
nastyDev();
var result = firstDev();
console.log(result);

// Output: undefined

第一個開發人員建立一個名爲globalSum的全局變量,併爲其分配一個函數。接着,另外一個開發人員將null分配給相同的變量,從而致使代碼出現故障。

處理全局變量老是有風險的,所以JS引入了「安全模式」:嚴格模式。嚴格模式是經過使用「use Strict」啓用。嚴格模式中的一個好處就是消除了默認綁定。在嚴格模式下,當試圖從全局上下文中訪問this時,會獲得 undefined

"use strict";

function whoIsThis() {
  console.log(this);
}

whoIsThis();

// Output: undefined

嚴格的模式使JS代碼更安全。

小結一下,默認綁定是JS中的第一條規則:當引擎沒法找出this是什麼時,它會返回到全局對象。接下看看另外三條規則。

規則2: 當「this」是宿主對象時(即隱式綁定)

「隱式綁定」是一個使人生畏的術語,但它背後的理論並不那麼複雜。它把範圍縮小到對象。

var widget = {
  items: ["a", "b", "c"],
  printItems: function() {
    console.log(this.items);
  }
};

當一個函數被賦值爲一個對象的屬性時,該對象就成爲函數運行的宿主。換句話說,函數中的this將自動指向該對象。這是JS中的第二條規則,名爲隱式綁定。即便在全局上下文中調用函數,隱式綁定也在起做用

function whoIsThis() {
  console.log(this);
}

whoIsThis();

我們沒法從代碼中看出,可是JS引擎將該函數分配給全局對象 window 上的一個新屬性,以下所示:

window.whoIsThis = function() {
  console.log(this);
};

我們能夠很容易地證明這個假設。在瀏覽器中運行如下代碼:

function whoIsThis() {
  console.log(this);
}

console.log(typeof window.whoIsThis)

打印"function"。對於這一點你可能會問:在全局函數中this 的真正規則是什麼?

像是缺省綁定,但實際上更像是隱式綁定。有點使人困惑,但只要記住,JS引擎在在沒法肯定上下文(默認綁定)時老是返回全局this。另外一方面,當函數做爲對象的一部分調用時,this 指向該調用的對象(隱式綁定)。

規則 3: 顯示指定 「this」(即顯式綁定)

若是不是 JS 使用者,很難看到這樣的代碼:

someObject.call(anotherObject);
Someobject.prototype.someMethod.apply(someOtherObject);

這就是顯式綁定,在 React 會常常看到這中綁定方式:

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    // bounded method
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

如今React Hooks 使得類幾乎沒有必要了,可是仍然有不少使用ES6類的「遺留」React組件。大多數初學者會問的一個問題是,爲何我們要在 React 中經過 bind` 方法從新綁定事件處理程序方法?

callapplybind 這三個方法都屬於Function.prototype。用於的顯式綁定(規則3):顯式綁定指顯示地將this綁定到一個上下文。但爲何要顯式綁定或從新綁定函數呢?考慮一些遺留的JS代碼:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

showModal是綁定到對象legacyWidget的「方法」。this.html 屬於硬編碼,把建立的元素寫死了(div)。這樣我們沒有辦法把內容附加到我們想附加的標籤上。

解決方法就是可使用顯式綁定this來更改showModal的對象。。如今,我們能夠建立一個小部件,並提供一個不一樣的HTML元素做附加的對象:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

var shinyNewWidget = {
  html: "",
  init: function() {
    // A different HTML element
    this.html = document.createElement("section");
  }
};

接着,使用 call 調用原始的方法:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

var shinyNewWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("section");
  }
};

// 使用不一樣的HTML元素初始化
shinyNewWidget.init();

// 使用新的上下文對象運行原始方法
legacyWidget.showModal.call(shinyNewWidget, "p");

若是你仍然對顯式綁定感到困惑,請將其視爲重用代碼的基本模板。這種看起來有點繁瑣冗長,但若是有遺留的JS代碼須要重構,這種方式是很是合適的。

此外,你可能想知道什麼是applybindapply具備與call相同的效果,只是前者接受一個參數數組,然後者是參數列表。

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams.call(newObj, "aa", "bb", "cc");

apply須要一個參數數組

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams.apply(newObj, ["aa", "bb", "cc"]);

那麼bind呢? bind 是綁定函數最強大的方法。bind仍然爲給定的函數接受一個新的上下文對象,但它不僅是用新的上下文對象調用函數,而是返回一個永久綁定到該對象的新函數。

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

var newFunc = obj.printParams.bind(newObj);

newFunc("aa", "bb", "cc");

bind的一個常見用例是對原始函數的 this 永久從新綁定:

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams = obj.printParams.bind(newObj);

obj.printParams("aa", "bb", "cc");

從如今起obj.printParams 裏面的 this 老是指向newObj。如今應該清楚爲何要在 React 使用 bind來從新綁定類方法了吧。

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

但現實更爲微妙,與「丟失綁定」有關。當我們將事件處理程序做爲一個prop分配給React元素時,該方法將做爲引用而不是函數傳遞,這就像在另外一個回調中傳遞事件處理程序引用:

// 丟失綁定
const handleClick = this.handleClick;

element.addEventListener("click", function() {
  handleClick();
});

賦值操做會破壞了綁定。在上面的示例組件中,handleClick方法(分配給button元素)試圖經過調用this.setState()更新組件的狀態。當調用該方法時,它已經失去了綁定,再也不是類自己:如今它的上下文對象是window全局對象。此時,會獲得"TypeError: Cannot read property 'setState' of undefined"的錯誤。

React組件大多數時候導出爲ES2015模塊:this未定義的,由於ES模塊默認使用嚴格模式,所以禁用默認綁定,ES6 的類也啓用嚴格模式。我們可使用一個模擬React組件的簡單類進行測試。handleClick調用setState方法來響應單擊事件

class ExampleComponent {
  constructor() {
    this.state = { text: "" };
  }

  handleClick() {
    this.setState({ text: "New text" });
    alert(`New state is ${this.state.text}`);
  }

  setState(newState) {
    this.state = newState;
  }

  render() {
    const element = document.createElement("button");
    document.body.appendChild(element);
    const text = document.createTextNode("Click me");
    element.appendChild(text);

    const handleClick = this.handleClick;

    element.addEventListener("click", function() {
      handleClick();
    });
  }
}

const component = new ExampleComponent();
component.render();

錯誤的代碼行是

const handleClick = this.handleClick;

而後點擊按鈕,查看控制檯,會看到 ·"TypeError: Cannot read property 'setState' of undefined"·.。要解決這個問題,可使用bind使方法綁定到正確的上下文,即類自己

constructor() {
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

再次單擊該按鈕,運行正確。顯式綁定比隱式綁定和默認綁定都更強。使用applycallbind,我們能夠經過爲函數提供一個動態上下文對象來隨意修改它。

規則 4:"new" 綁定

構造函數模式,有助於用JS封裝建立新對象的行爲:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"

這裏,我們爲一個名爲「Person」的實體建立一個藍圖。根據這個藍圖,就能夠經過「new」調用「構造」Person類型的新對象:

var me = new Person("Valentino");

在JS中有不少方法能夠改變 this 指向,可是當在構造函數上使用new時,this 指向就肯定了,它老是指向新建立的對象。在構造函數原型上定義的任何函數,以下所示

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

這樣始終知道「this」指向是啥,由於大多數時候this指向操做的宿主對象。在下面的例子中,greet是由me的調用

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"

因爲me是經過構造函數調用構造的,因此它的含義並不含糊。固然,仍然能夠從Person借用greet並用另外一個對象運行它:

Person.prototype.greet.apply({ name: "Tom" });

// Output: "Hello Tom"

正如我們所看到的,this很是靈活,可是若是不知道this所依據的規則,我們就不能作出有根據的猜想,也不能利用它的真正威力。長話短說,this是基於四個「簡單」的規則。

箭頭函數和 "this"

箭頭函數的語法方便簡潔,可是建議不要濫用它們。固然,箭頭函數有不少有趣的特性。首先考慮一個名爲Post的構造函數。只要我們從構造函數中建立一個新對象,就會有一個針對REST API的Fetch請求:

"use strict";

function Post(id) {
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(function(response) {
      return response.json();
    })
    .then(function(json) {
      this.data = json;
    });
}

var post1 = new Post(3);

上面的代碼處於嚴格模式,所以禁止默認綁定(回到全局this)。嘗試在瀏覽器中運行該代碼,會報錯:"TypeError: Cannot set property 'data' of undefined at :11:17"

這報錯作是對的。全局變量 this 在嚴格模式下是undefined爲何我們的函數試圖更新 window.data而不是post.data?

緣由很簡單:由Fetch觸發的回調在瀏覽器中運行,所以它指向 window。爲了解決這個問題,早期有個老作法,就是使用臨時亦是:「that」。換句話說,就是將this引用保存在一個名爲that的變量中:

"use strict";

function Post(id) {
  var that = this;
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(function(response) {
      return response.json();
    })
    .then(function(json) {
      that.data = json;
    });
}

var post1 = new Post(3);

若是不用這樣,最簡單的作法就是使用箭頭函數:

"use strict";

function Post(id) {
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(response => {
      return response.json();
    })
    .then(json => {
      this.data = json;
    });
}

var post1 = new Post(3);

問題解決。如今 this.data 老是指向post1。爲何? 箭頭函數將this指向其封閉的環境(也稱「詞法做用域」)。換句話說,箭頭函數並不關心它是否在window對象中運行。它的封閉環境是對象post1,以post1爲宿主。固然,這也是箭頭函數最有趣的用例之一。

總結

JS 中 this 是什麼? 這得視狀況而定。this 創建在四個規則上:默認綁定、隱式綁定、顯式綁定和 「new」綁定。

隱式綁定表示當一個函數引用 this 並做爲 JS 對象的一部分運行時,this 將指向這個「宿主」對象。但 JS 函數老是在一個對象中運行,這是任何全局函數在所謂的全局做用域中定義的狀況。

在瀏覽器中工做時,全局做用域是 window。在這種狀況下,在全局中運行的任何函數都將看到this 就是 window:它是 this 的默認綁定。

大多數狀況下,不但願與全局做用域交互,JS 爲此就提供了一種用嚴格模式來中和默認綁定的方法。在嚴格模式下,對全局對象的任何引用都是 undefined,這有效地保護了咱們避免愚蠢的錯誤。

除了隱式綁定和默認綁定以外,還有「顯式綁定」,咱們可使用三種方法來實現這一點:applycallbind。 這些方法對於傳遞給定函數應在其上運行的顯式宿主對象頗有用。

最後一樣重要的是「new」綁定,它在經過調用「構造函數」時在底層作了五處理。對於大多數開發人員來講,this 是一件可怕的事情,必須不惜一切代價避免。可是對於那些想深刻研究的人來講,this 是一個強大而靈活的系統,能夠重用 JS 代碼。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:https://github.com/valentinog...

交流

阿里雲最近在作活動,低至2折,有興趣能夠看看:https://promotion.aliyun.com/...

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq449245884/xiaozhi

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

clipboard.png

相關文章
相關標籤/搜索