JavaScript 深入之從 ECMAScript 規範解讀 this

冴羽發表於2017-04-13

JavaScript深入系列第六篇,本篇我們追根溯源,從ECMAScript5規範解讀this在函式呼叫時到底是如何確定的。

前言

《JavaScript深入之執行上下文棧》中講到,當JavaScript程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。

對於每個執行上下文,都有三個重要屬性

  • 變數物件(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

今天重點講講 this,然而不好講。

……

因為我們要從 ECMASciript5 規範開始講起。

先奉上 ECMAScript 5.1 規範地址:

英文版:es5.github.io/#x15.1

中文版:yanhaijing.com/es5/#115

讓我們開始瞭解規範吧!

Types

首先是第 8 章 Types:

Types are further subclassified into ECMAScript language types and specification types.

An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.

我們簡單的翻譯一下:

ECMAScript 的型別分為語言型別和規範型別。

ECMAScript 語言型別是開發者直接使用 ECMAScript 可以操作的。其實就是我們常說的Undefined, Null, Boolean, String, Number, 和 Object。

而規範型別相當於 meta-values,是用來用演算法描述 ECMAScript 語言結構和 ECMAScript 語言型別的。規範型別包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

沒懂?沒關係,我們只要知道在 ECMAScript 規範中還有一種只存在於規範中的型別,它們的作用是用來描述語言底層行為邏輯。

今天我們要講的重點是便是其中的 Reference 型別。它與 this 的指向有著密切的關聯。

Reference

那什麼又是 Reference ?

讓我們看 8.7 章 The Reference Specification Type:

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

所以 Reference 型別就是用來解釋諸如 delete、typeof 以及賦值等操作行為的。

抄襲尤雨溪大大的話,就是:

這裡的 Reference 是一個 Specification Type,也就是 “只存在於規範裡的抽象型別”。它們是為了更好地描述語言的底層行為邏輯才存在的,但並不存在於實際的 js 程式碼中。

再看接下來的這段具體介紹 Reference 的內容:

A Reference is a resolved name binding.

A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag.

The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1).

A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

這段講述了 Reference 的構成,由三個組成部分,分別是:

  • base value
  • referenced name
  • strict reference

可是這些到底是什麼呢?

我們簡單的理解的話:

base value 就是屬性所在的物件或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一種。

referenced name 就是屬性的名稱。

舉個例子:

var foo = 1;

// 對應的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};複製程式碼

再舉個例子:

var foo = {
    bar: function () {
        return this;
    }
};

foo.bar(); // foo

// bar對應的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};複製程式碼

而且規範中還提供了獲取 Reference 組成部分的方法,比如 GetBase 和 IsPropertyReference。

這兩個方法很簡單,簡單看一看:

1.GetBase

GetBase(V). Returns the base value component of the reference V.

返回 reference 的 base value。

2.IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.

簡單的理解:如果 base value 是一個物件,就返回true。

GetValue

除此之外,緊接著在 8.7.1 章規範中就講了一個用於從 Reference 型別獲取對應值的方法: GetValue。

簡單模擬 GetValue 的使用:

var foo = 1;

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

GetValue(fooReference) // 1;複製程式碼

GetValue 返回物件屬性真正的值,但是要注意:

呼叫 GetValue,返回的將是具體的值,而不再是一個 Reference

這個很重要,這個很重要,這個很重要。

如何確定this的值

關於 Reference 講了那麼多,為什麼要講 Reference 呢?到底 Reference 跟本文的主題 this 有哪些關聯呢?如果你能耐心看完之前的內容,以下開始進入高能階段:

看規範 11.2.3 Function Calls:

這裡講了當函式呼叫的時候,如何確定 this 的取值。

只看第一步、第六步、第七步:

1.Let ref be the result of evaluating MemberExpression.

6.If Type(ref) is Reference, then

  a.If IsPropertyReference(ref) is true, then

      i.Let thisValue be GetBase(ref).

  b.Else, the base of ref is an Environment Record

      i.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).複製程式碼

7.Else, Type(ref) is not Reference.

  a. Let thisValue be undefined.複製程式碼

讓我們描述一下:

1.計算 MemberExpression 的結果賦值給 ref

2.判斷 ref 是不是一個 Reference 型別

2.1 如果 ref 是 Reference,並且 IsPropertyReference(ref) 是 true, 那麼 this 的值為 GetBase(ref)

2.2 如果 ref 是 Reference,並且 base value 值是 Environment Record, 那麼this的值為 ImplicitThisValue(ref)

2.3 如果 ref 不是 Reference,那麼 this 的值為 undefined複製程式碼

具體分析

讓我們一步一步看:

  1. 計算 MemberExpression 的結果賦值給 ref

什麼是 MemberExpression?看規範 11.2 Left-Hand-Side Expressions:

MemberExpression :

  • PrimaryExpression // 原始表示式 可以參見《JavaScript權威指南第四章》
  • FunctionExpression // 函式定義表示式
  • MemberExpression [ Expression ] // 屬性訪問表示式
  • MemberExpression . IdentifierName // 屬性訪問表示式
  • new MemberExpression Arguments // 物件建立表示式

舉個例子:

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

foo(); // MemberExpression 是 foo

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

foo()(); // MemberExpression 是 foo()

var foo = {
    bar: function () {
        return this;
    }
}

foo.bar(); // MemberExpression 是 foo.bar複製程式碼

所以簡單理解 MemberExpression 其實就是()左邊的部分。

2.判斷 ref 是不是一個 Reference 型別。

關鍵就在於看規範是如何處理各種 MemberExpression,返回的結果是不是一個Reference型別。

舉最後一個例子:

var value = 1;

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

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());複製程式碼

foo.bar()

在示例 1 中,MemberExpression 計算的結果是 foo.bar,那麼 foo.bar 是不是一個 Reference 呢?

檢視規範 11.2.1 Property Accessors,這裡展示了一個計算的過程,什麼都不管了,就看最後一步:

Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

我們得知該表示式返回了一個 Reference 型別!

根據之前的內容,我們知道該值為:

var Reference = {
  base: foo,
  name: 'bar',
  strict: false
};複製程式碼

接下來按照 2.1 的判斷流程走:

2.1 如果 ref 是 Reference,並且 IsPropertyReference(ref) 是 true, 那麼 this 的值為 GetBase(ref)

該值是 Reference 型別,那麼 IsPropertyReference(ref) 的結果是多少呢?

前面我們已經鋪墊了 IsPropertyReference 方法,如果 base value 是一個物件,結果返回 true。

base value 為 foo,是一個物件,所以 IsPropertyReference(ref) 結果為 true。

這個時候我們就可以確定 this 的值了:

this = GetBase(ref),複製程式碼

GetBase 也已經鋪墊了,獲得 base value 值,這個例子中就是foo,所以 this 的值就是 foo ,示例1的結果就是 2!

唉呀媽呀,為了證明 this 指向foo,真是累死我了!但是知道了原理,剩下的就更快了。

(foo.bar)()

看示例2:

console.log((foo.bar)());複製程式碼

foo.bar 被 () 包住,檢視規範 11.1.6 The Grouping Operator

直接看結果部分:

Return the result of evaluating Expression. This may be of type Reference.

NOTE This algorithm does not apply GetValue to the result of evaluating Expression.

實際上 () 並沒有對 MemberExpression 進行計算,所以其實跟示例 1 的結果是一樣的。

(foo.bar = foo.bar)()

看示例3,有賦值操作符,檢視規範 11.13.1 Simple Assignment ( = ):

計算的第三步:

3.Let rval be GetValue(rref).

因為使用了 GetValue,所以返回的值不是 Reference 型別,

按照之前講的判斷邏輯:

2.3 如果 ref 不是Reference,那麼 this 的值為 undefined

this 為 undefined,非嚴格模式下,this 的值為 undefined 的時候,其值會被隱式轉換為全域性物件。

(false || foo.bar)()

看示例4,邏輯與演算法,檢視規範 11.11 Binary Logical Operators:

計算第二步:

2.Let lval be GetValue(lref).

因為使用了 GetValue,所以返回的不是 Reference 型別,this 為 undefined

(foo.bar, foo.bar)()

看示例5,逗號操作符,檢視規範11.14 Comma Operator ( , )

計算第二步:

2.Call GetValue(lref).

因為使用了 GetValue,所以返回的不是 Reference 型別,this 為 undefined

揭曉結果

所以最後一個例子的結果是:


var value = 1;

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

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1複製程式碼

注意:以上是在非嚴格模式下的結果,嚴格模式下因為 this 返回 undefined,所以示例 3 會報錯。

補充

最最後,忘記了一個最最普通的情況:

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

foo();複製程式碼

MemberExpression 是 foo,解析識別符號,檢視規範 10.3.1 Identifier Resolution,會返回一個 Reference 型別的值:

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};複製程式碼

接下來進行判斷:

2.1 如果 ref 是 Reference,並且 IsPropertyReference(ref) 是 true, 那麼 this 的值為 GetBase(ref)

因為 base value 是 EnvironmentRecord,並不是一個 Object 型別,還記得前面講過的 base value 的取值可能嗎? 只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一種。

IsPropertyReference(ref) 的結果為 false,進入下個判斷:

2.2 如果 ref 是 Reference,並且 base value 值是 Environment Record, 那麼this的值為 ImplicitThisValue(ref)

base value 正是 Environment Record,所以會呼叫 ImplicitThisValue(ref)

檢視規範 10.2.1.1.6,ImplicitThisValue 方法的介紹:該函式始終返回 undefined。

所以最後 this 的值就是 undefined。

多說一句

儘管我們可以簡單的理解 this 為呼叫函式的物件,如果是這樣的話,如何解釋下面這個例子呢?

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}
console.log((false || foo.bar)()); // 1複製程式碼

此外,又如何確定呼叫函式的物件是誰呢?在寫文章之初,我就面臨著這些問題,最後還是放棄從多個情形下給大家講解 this 指向的思路,而是追根溯源的從 ECMASciript 規範講解 this 的指向,儘管從這個角度寫起來和讀起來都比較吃力,但是一旦多讀幾遍,明白原理,絕對會給你一個全新的視角看待 this 。而你也就能明白,儘管 foo() 和 (foo.bar = foo.bar)() 最後結果都指向了 undefined,但是兩者從規範的角度上卻有著本質的區別。

此篇講解執行上下文的 this,即便不是很理解此篇的內容,依然不影響大家瞭解執行上下文這個主題下其他的內容。所以,依然可以安心的看下一篇文章。

下一篇文章

《JavaScript深入之執行上下文》

深入系列

JavaScript深入系列目錄地址:github.com/mqyqingfeng…

JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

相關文章