深入理解JS:執行上下文中的this(一)

forcheng發表於2020-05-25

目錄

  • 執行上下文與執行上下文棧
  • this
    • 全域性環境
    • 函式環境
  • 總結
  • 參考

1.執行上下文與執行上下文棧

(1)什麼是執行上下文?

在 JavaScript 程式碼執行時,解釋執行全域性程式碼、呼叫函式或使用 eval 函式執行一個字串表示式都會建立並進入一個新的執行環境,而這個執行環境被稱之為執行上下文。因此執行上下文有三類:全域性執行上下文、函式執行上下文、eval 函式執行上下文。

執行上下文可以理解為一個抽象的物件,如下圖:

執行上下文抽象物件

Variable object:變數物件,用於儲存被定義在執行上下文中的變數 (variables) 和函式宣告 (function declarations) 。

Scope chain:作用域鏈,是一個物件列表 (list of objects) ,用以檢索上下文程式碼中出現的識別符號 (identifiers) 。

thisValue:this 指標,是一個與執行上下文相關的特殊物件,也被稱之為上下文物件。


(2)什麼是執行上下文棧?

在全域性程式碼中呼叫函式,或函式中呼叫函式(如遞迴)等,都會涉及到在一個執行上下文中建立另一個新的執行上下文,並且等待這個新的上下文執行完畢,才會返回之前的執行上下文接著繼續執行,而這樣的呼叫方式就形成了執行上下文棧

示例程式碼:

function A() {
  console.log('function A')
  B()
}

function B() {
  console.log('function B')
  C()
}

function C() {
  console.log('function C')
}

A()

上述示例程式碼,當執行到函式 C時,此時的執行上下文棧如下圖:

執行上下文棧


2.this

首先需要清楚,this 是執行上下文的一個屬性,而不是某個變數物件的屬性,是一個與執行上下文相關的特殊物件。由於在開發中不推薦或應儘量避免使用 eval 函式,所以在這裡我們主要討論全域性執行上下文(全域性環境)和函式執行上下文(函式環境)中的 this。


(1)全域性環境

無論是否在嚴格模式下,在全域性環境中(在任何函式體外部的程式碼),this 始終指向全域性物件(在瀏覽器中即 window)。

示例程式碼(瀏覽器中):

console.log(this === window) // true

a = 1;

console.log(window.a) // 1
console.log(this.a === window.a) // true

this.b = "test"

console.log(window.b) // test
console.log(b) //test

(2)函式環境

在大多數情況下,函式的呼叫方式決定了 this 的值。 this 是不能夠在執行期間被賦值修改的,並且在每次函式被呼叫時其 this 可能不同(通過 apply 或 call 方法顯示設定 this 等)。

另外,ES5 引入了 bind 方法來設定函式的 this 值,而不用考慮函式如何被呼叫的。ES6 引入了支援 this 詞法解析的箭頭函式(它在閉合的執行環境內設定 this 的值)。


接下來我們主要分析:函式的呼叫方式是如何決定 this 的值?(對於 bind 方法以及箭頭函式將留於下一篇文章進行詳細分析)

要弄明白這個問題,我們來看看 EcmaScript 5.1標準的規定,瞭解一下 函式呼叫 的規範:

11.2.3 函式呼叫

產生式 CallExpression : MemberExpression Arguments 按照下面的過程執行 :

  1. 令 ref 為解釋執行 MemberExpression 的結果 .
  2. 令 func 為 GetValue(ref).
  3. 令 argList 為解釋執行 Arguments 的結果 , 產生引數值們的內部列表 (see 11.2.4).
  4. 如果 Type(func) is not Object ,丟擲一個 TypeError 異常 .
  5. 如果 IsCallable(func) is false ,丟擲一個 TypeError 異常 .
  6. 如果 Type(ref) 為 Reference,那麼 如果 IsPropertyReference(ref) 為 true,那麼 令 thisValue 為 GetBase(ref). 否則 , ref 的基值是一個環境記錄項 , 令 thisValue 為 GetBase(ref).ImplicitThisValue().
  7. 否則 , 假如 Type(ref) 不是 Reference. 令 thisValue 為 undefined.
  8. 返回撥用 func 的 [[Call]] 內建方法的結果 , 傳入 thisValue 作為 this 值和列表 argList 作為引數列表

產生式 CallExpression : CallExpression Arguments以完全相同的方式執行,除了第1步執行的是其中的CallExpression。

簡單解析:

第1步,令 ref 為 MemberExpression 解釋執行的結果。

11.2 左值表示式 中有提到,MemberExpression 可以是以下五種表示式中的任意一種:

  • PrimaryExpression // 原始表示式
  • FunctionExpression // 函式定義表示式
  • MemberExpression [ Expression ] // 屬性訪問表示式
  • MemberExpression . IdentifierName // 屬性訪問表示式
  • new MemberExpression Arguments // 物件建立表示式

簡單理解 MemberExpression 就是呼叫一個函式的()左側的部分。

第2~5步,獲取呼叫函式的引數列表以及檢測所呼叫的函式是否合法,否則丟擲相應異常。

第6、7步,就是決定函式呼叫的 this 的值的關鍵步驟,翻譯一下,如同下面的虛擬碼:

var thisValue = getThisValue(ref)

function getThisValue(ref) {
  // 判斷 ref 的型別是否是 Reference,如果不是,直接返回 undefined
  if(Type(ref) !== Reference) return undefined
  
  // 是否是 Object, Boolean, String, Number
  if(IsPropertyReference(ref)) {
    return GetBase(ref)
  } else {
    // 是一個環境記錄項(Environment record),呼叫其 ImplicitThisValue 方法
    return GetBase(ref).ImplicitThisValue()
  }
}

關於 GetBase 和 IsPropertyReference 方法:

  • GetBase(V), 返回引用值 V 的基值 (Reference 的基值 base,詳見下面提到的 Reference 的組成)。
  • HasPrimitiveBase(V), 如果基值是 Boolean, String, Number,那麼返回 true。
  • IsPropertyReference(V), 如果基值是個物件或 HasPrimitiveBase(V) 是 true,那麼返回 true;否則返回 false。

而對於 ImplicitThisValue 方法,其屬於環境記錄項(Environment record)的方法。而環境記錄項分為兩種:

  • 宣告式環境記錄項:每個宣告式環境記錄項都與一個包含變數和(或)函式宣告的 ECMA 指令碼的程式作用域相關聯。宣告式環境記錄項用於繫結作用域內定義的一系列識別符號。其 ImplicitThisValue 永遠返回 undefined。

  • 物件式環境記錄項:每一個物件式環境記錄項都有一個關聯的物件,這個物件被稱作 繫結物件 。物件式環境記錄項直接將一系列識別符號與其繫結物件的屬性名稱建立一一對應關係。其 ImplicitThisValue 通常返回 undefined,除非其 provideThis 標識的值為 true。具體如下:

    1. 令 envRec 為函式呼叫時對應的宣告式環境記錄項。
    2. 如果 envRec 的 provideThis 標識的值為 true,返回 envRec 的繫結物件。
    3. 否則返回 undefined。

    物件式環境記錄項可以通過配置的方式,將其繫結物件合為函式呼叫時的隱式 this 物件的值。這一功能用於規範 With 表示式(12.10 章 )引入的繫結行為。該行為通過物件式環境記錄項中布林型別的 provideThis 值控制,預設情況下,provideThis 的值為 false。(只有使用了 with 表示式,才會將 provideThis 標識的值為 true)


而上面提到了兩種新的型別: 引用規範型別 (Reference)與 環境記錄項(Environment record)都是屬於ECMAScript 的規範型別,相當於 meta-values,是用來用演算法描述 ECMAScript 語言結構和 ECMAScript 語言型別的。

而與規範型別相對於的就是語言型別:就是開發者直接使用的型別,即Undefined, Null, Boolean, String, Number, 和 Object。(ECMAScript的型別分為語言型別和規範型別)


從上面的虛擬碼中可以看到 thisValue 的值與 ref 是否是引用規範型別(Reference)有直接關聯,即呼叫一個函式時,其()左側的部分的解釋執行的結果的型別是不是 Reference 型別,將直接影響 thisValue 的值。

EcmaScript 5.1標準中的 Reference 的規範:

8.7 引用規範型別 (Reference)

Reference 型別是用來說明 delete,typeof,賦值運算子這些運算子的行為。

一個 Reference 是個已解決的命名繫結。其由三部分組成, 基值 (base) , 引用名稱(referenced name) 和布林值 嚴格引用 (strict reference) 標誌。

基值是 undefined, Object, Boolean, String, Number, Environment record 中的任意一個。基值是 undefined 表示此引用可以不解決一個繫結。引用名稱是一個字串。嚴格引用標誌表示是否在嚴格模式下解釋執行的程式碼。

而引用規範型別(Reference)會被用在識別符號解析中,識別符號執行的結果總是一個 Reference 型別的值。

EcmaScript 5.1標準中的 識別符號解析 的規範:

10.3.1 識別符號解析

識別符號解析是指使用正在執行的執行環境中的詞法環境,通過一個 識別符號 獲得其對應的繫結的過程。在 ECMA 指令碼程式碼執行過程中,PrimaryExpression : Identifier 這一語法產生式將按以下演算法進行解釋執行:

  1. 令 env 為正在執行的執行環境的 詞法環境 。
  2. 如果正在解釋執行的語法產生式處在 嚴格模式下的程式碼 中,則僅 strict 的值為 true,否則令 strict 的值為 false。
  3. 以 env,Identifier 和 strict 為引數,呼叫 GetIdentifierReference 函式,並返回撥用的結果。

解釋執行一個識別符號得到的結果必定是 Reference 型別的物件,且其引用名屬性的值與 Identifier 字串相等。

GetIdentifierReference 函式就是返回一個 Reference 型別的物件,類似如下物件:

var valueOfReferenceType = {
    base: <base object>, // Identifier 所處的環境(Environment Record)或者 Identifier 屬性所屬的物件
    propertyName: <property name>, // 與 Identifier 字串相等
    strict: <boolean>
};

因此,我們可以來看一些相關的示例程式碼。

第一組:非嚴格模式和嚴格模式的全域性函式

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

function bar() {
  'use strict'
  console.log(this)
}

foo() // global
bar() // undefined

// foo 識別符號對應的 Reference
var fooReference = {
  base: EnvironmentRecord,
  propertyName: 'foo',
  strict: false
}

// bar 識別符號對應的 Reference
var barReference = {
  base: EnvironmentRecord,
  propertyName: 'bar',
  strict: true
}

上述程式碼中,對於 fooReference,根據函式呼叫規範可知其 this = getThisValue(fooReference) = GetBase(fooReference).ImplicitThisValue() = undefined,而 barReference 也是一樣。

但為什麼 foo() 輸出的是 global 全域性物件而不是 undefined 呢?這是因為在非嚴格模式下, 當 this 的值為 undefined 時,會被隱式轉換為全域性物件。而在嚴格模式下,指定的 this 不再被封裝為物件。


第二組:物件的屬性訪問

var foo = {
  bar: function () {
      console.log(this)
  }
}

foo.bar() // foo

// foo 的 bar 屬性對應的 Reference
var barReference = {
  base: foo,
  propertyName: 'bar',
  strict: false
}

上述程式碼中,對於 barReference,根據函式呼叫規範可知 this = getThisValue(barReference) = GetBase(barReference) = foo

foo.bar()中,MemberExpression 計算的結果是 foo.bar,為什麼它是一個 Reference 型別呢?

EcmaScript 5.1標準中的 屬性訪問 的規範:

11.2.1 屬性訪問

  1. 返回一個 Reference 型別的值,其基值為 baseValue 且其引用名為 propertyNameString, 嚴格模式標記為 strict.

這裡只引用了最後一步,屬性訪問最終返回的值是一個 Reference 型別。


第三組:非 Reference 型別的函式呼叫

首先,需要j簡單瞭解一下 GetValue 方法,其作用是獲取 Reference 型別具體的值,返回結果不再是一個 Reference。例如:

var foo = 1

// foo 識別符號對應的 Reference
var fooReference = {
  base: EnvironmentRecord,
  propertyName: 'foo',
  strict: false
}

GetValue(fooReference) // 1

示例程式碼:

value = 1

var foo = {
  value: 2,
  bar: function () {
    console.log(this.value)
  }
};

foo.bar();   // 2
(foo.bar)(); // 2

(false || foo.bar)();   // 1
(foo.bar = foo.bar)();  // 1
(foo.bar, foo.bar)();   // 1

在上述示例程式碼中:

  1. 對於 (foo.bar),foo.bar 被 () 包住,使用了分組運算子,檢視規範 11.1.6 分組操作符,可知分組表示式不會呼叫 GetValue 方法, 所以 (foo.bar)仍舊是一個 Reference 型別,因此 this 為 Reference 型別的 base 物件,即 foo。
  2. 對於 (false || foo.bar),有邏輯與演算法,檢視規範 11.11 二元邏輯運算子,可知二元邏輯運算子呼叫了 GetValue 方法,所以false || foo.bar不再是一個 Reference 型別,因此 this 為 undefined,非嚴格模式下,被隱式轉化為 global 物件。
  3. 對於 (foo.bar = foo.bar),有賦值運算子,檢視規範 11.13.1 簡單賦值,可知簡單賦值呼叫了 GetValue 方法,所以foo.bar = foo.bar不再是一個 Reference 型別,因此 this 為 undefined,非嚴格模式下,被隱式轉化為 global 物件。
  4. 對於 (foo.bar, foo.bar),有逗號運算子,檢視規範 11.14 逗號運算子,可知逗號運算子呼叫了 GetValue 方法,所以foo.bar, foo.bar不再是一個 Reference 型別,因此 this 為 undefined,非嚴格模式下,被隱式轉化為 global 物件。

3.總結

  1. 在全域性環境(全域性執行上下文)中(在任何函式體外部的程式碼),this 始終指向全域性物件
  2. 在函式環境(函式執行上下文)中,絕大多數情況,函式的呼叫方式決定了 this 的值,這與呼叫函式的()左側的部分 MemberExpression 的解釋執行的結果的型別是不是 Reference 型別直接關聯。

4.參考

this 關鍵字 - JavaScript | MDN - Mozilla

深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)

深入理解JavaScript系列(13):This? Yes,this!

JavaScript深入之從ECMAScript規範解讀this

ECMAScript5.1中文版

ECMAScript 5.1 pdf(英)

相關文章