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

目錄html

  • 執行上下文與執行上下文棧
  • this
    • 全局環境
    • 函數環境
  • 總結
  • 參考

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

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

在 JavaScript 代碼運行時,解釋執行全局代碼、調用函數或使用 eval 函數執行一個字符串表達式都會建立並進入一個新的執行環境,而這個執行環境被稱之爲執行上下文。所以執行上下文有三類:全局執行上下文、函數執行上下文、eval 函數執行上下文。算法

執行上下文能夠理解爲一個抽象的對象,以下圖:瀏覽器

執行上下文抽象對象

Variable object:變量對象,用於存儲被定義在執行上下文中的變量 (variables) 和函數聲明 (function declarations) 。app

Scope chain:做用域鏈,是一個對象列表 (list of objects) ,用以檢索上下文代碼中出現的標識符 (identifiers) 。ide

thisValue:this 指針,是一個與執行上下文相關的特殊對象,也被稱之爲上下文對象。函數


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

在全局代碼中調用函數,或函數中調用函數(如遞歸)等,都會涉及到在一個執行上下文中建立另外一個新的執行上下文,而且等待這個新的上下文執行完畢,纔會返回以前的執行上下文接着繼續執行,而這樣的調用方式就造成了執行上下文棧es5

示例代碼:

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步,獲取調用函數的參數列表以及檢測所調用的函數是否合法,不然拋出相應異常。

第六、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(英)

相關文章
相關標籤/搜索