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

作者:valentinogagliardi

譯者:前端小智

來源:github

這幾天自己的公衆號無套路送現金 200+,參與方式如下 

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

揭祕 "this"

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

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

Python 類也有一個跟 this 差不多的東西,叫做 self

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

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

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時,都會返回正確的名字:

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 看起來似乎指向實際的類實例?

這是不對的。咱們不要忘記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引入了“安全模式”:嚴格模式。嚴格模式是通過使用 useStrict啓用。嚴格模式中的一個好處就是消除了默認綁定。在嚴格模式下,當試圖從全局上下文中訪問 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>	
    );	
  }	
}

現在 ReactHooks使得類幾乎沒有必要了,但是仍然有很多使用ES6類的“遺留”React組件。大多數初學者會問的一個問題是,爲什麼咱們要在React中通過bind` 方法重新綁定事件處理程序方法?

call、 apply、 bind 這三個方法都屬於 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代碼需要重構,這種方式是非常合適的。

此外,你可能想知道什麼是 apply和 bind。 apply具有與 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);	
  }

再次單擊該按鈕,運行正確。顯式綁定比隱式綁定和默認綁定都更強。使用 apply、 call和 bind,咱們可以通過爲函數提供一個動態上下文對象來隨意修改它。


規則 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,這有效地保護了我們避免愚蠢的錯誤。

除了隱式綁定和默認綁定之外,還有“顯式綁定”,我們可以使用三種方法來實現這一點: apply、 call和 bind。這些方法對於傳遞給定函數應在其上運行的顯式宿主對象很有用。

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

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

原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/README.md


交流

640?wx_fmt=jpeg

延伸閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章