從 JavaScript 執行上下文的視角講清楚 this

語言: CN / TW / HK

theme: devui-blue highlight: a11y-dark


我正在參加「掘金·啟航計劃」

前言

在對象內部的方法中使用對象內部的屬性是一個非常普遍的需求。但是 JavaScript 的作用域機制並不支持這一點,基於這個需求,JavaScript 有另外一套 this 機制。

this 是和執行上下文綁定的,也就是説每個執行上下文中都有一個 this。執行上下文主要分為三種—— 全局執行上下文函數執行上下文eval 執行上下文,所以對應的 this 也只有這三種——全局執行上下文中的 this、函數中的 this 和 eval 中的 this。

全局執行上下文中的 this

全局執行上下文中的 this 是指向 window 對象的。這也是 this 和作用域鏈的 唯一交點,作用域鏈的最底端包含了 window 對象,全局執行上下文中的 this 也是指向 window 對象。

函數執行上下文中的 this

js function foo() { console.log(this); } foo();

執行這段代碼,打印出來的也是 window 對象,這説明在默認情況下調用一個函數,其執行上下文中的 this 也是指向 window 對象的。

通常情況下,有下面三種方式來設置函數執行上下文中的 this 值:

1. 通過函數的 call 方法設置

js let bar = { myName: "極客邦", test1: 1, }; function foo() { this.myName = "極客時間"; } foo.call(bar); console.log(bar); console.log(myName);

執行這段代碼,你就能發現 foo 函數內部的 this 已經指向了 bar 對象,因為通過打印 bar 對象,可以看出 barmyName 屬性已經由“極客邦”變為“極客時間”了,同時在全局執行上下文中打印 myName,JavaScript 引擎提示該變量未定義。其實除了 call 方法,你還可以使用 bindapply 方法來設置函數執行上下文中的 this

2. 通過對象調用方法設置

js var myObj = { name: "極客時間", showThis: function () { console.log(this); }, }; myObj.showThis();

執行這段代碼,你可以看到,最終輸出的 this 值是指向 myObj 的。所以,你可以得出這樣的結論:使用對象來調用其內部的一個方法,該方法的 this 是指向對象本身的。

接下來我們稍微改變下調用方式,把 showThis 賦給一個全局對象,然後再調用該對象,代碼如下所示:

js var myObj = { name: "極客時間", showThis: function () { this.name = "極客邦"; console.log(this); }, }; var foo = myObj.showThis; foo();

執行這段代碼,你會發現 this 又指向了全局 window 對象。

結論:

  1. 在全局環境中調用一個函數,函數內部的 this 指向的是全局變量 window

  2. 通過一個對象來調用其內部的一個方法,該方法的執行上下文中的 this 指向對象本身。

3. 通過構造函數中設置

js function CreateObj() { this.name = "極客時間"; } var myObj = new CreateObj();

當執行 new CreateObj() 的時候,JavaScript 引擎做了如下四件事:

  1. 首先創建了一個空對象 tempObj

  2. 接着調用 CreateObj.call 方法,並將 tempObj 作為 call 方法的參數,這樣當 CreateObj 的執行上下文創建時,它的 this 就指向了 tempObj 對象

  3. 然後執行 CreateObj 函數,此時的 CreateObj 函數執行上下文中的 this 指向了 tempObj 對象

  4. 最後返回 tempObj 對象

js var tempObj = {}; CreateObj.call(tempObj); return tempObj;

這樣,就通過 new 關鍵字構建好了一個新對象,並且構造函數中的 this 其實就是新對象本身。

this 的設計缺陷以及應對方案

1. 嵌套函數中的 this 不會從外層函數中繼承

js var myObj = { name: "極客時間", showThis: function () { console.log(this); function bar() { console.log(this); } bar(); }, }; myObj.showThis();

執行這段代碼後,會發現函數 bar 中的 this 指向的是全局 window 對象,而函數 showThis 中的 this 指向的是 myObj 對象。

可以通過一個小技巧來解決這個問題,比如在 showThis 函數中聲明一個變量 that 用來保存 this,然後在 bar 函數中使用 that。其實,這個方法的的本質是把 this 體系轉換為了作用域的體系。

其實,你也可以使用 ES6 中的箭頭函數來解決這個問題:

js var myObj = { name: "極客時間", showThis: function () { console.log(this); var bar = () => { console.log(this); }; bar(); }, }; myObj.showThis();

這是因為 ES6 中的箭頭函數並不會創建其自身的執行上下文,所以箭頭函數中的 this 取決於它的外部函數。

2. 普通函數中的 this 默認指向全局對象 window

在實際工作中,我們並不希望函數執行上下文中的 this 默認指向全局對象,因為這樣會打破數據的邊界,造成一些誤操作。如果要讓函數執行上下文中的 this 指向某個對象,最好的方式是通過 call 方法來顯示調用。

可以通過設置 JavaScript 的 嚴格模式 來解決(在第一行加上 "use strict";)。在嚴格模式下,默認執行一個函數,其函數的執行上下文中的 this 值是 undefined