深入 JavaScript 的 this

語言: CN / TW / HK

歡迎關注微信公眾號:前端閱讀室

前言

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

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

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

今天我們來講解下 this

關於 this 的介紹可以參考如下規範:

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

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

而規範型別相當於 meta-values,是用來用演算法描述 ECMAScript 語言結構和 ECMAScript 語言型別的。

Reference 型別就是一種規範型別。它與 this 有著密切的關聯。

Reference

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

Reference 由三個部分組成,分別是:

  • base value
  • referenced name
  • strict reference

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

referenced name 就是屬性的名稱。

舉個例子:

```js var foo = 1;

// 對應的Reference是: var fooReference = { base: EnvironmentRecord, name: "foo", strict: false }; ```

再舉個例子:

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

foo.bar(); // foo

// bar 對應的 Reference是: var BarReference = { base: foo, propertyName: "bar", strict: false }; ```

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

GetBase 方法返回 reference 的 base value。

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

GetValue

GetValue 是用於從 Reference 型別獲取對應值的方法。

簡單模擬 GetValue 的使用如下:

```js var foo = 1;

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

GetValue(fooReference); // 1; ```

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

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

如何確定 this 的值

由於 Reference 和 this 息息相關,所以我們才花了這麼大的篇幅講解。知道了 Reference,接下來我們就可以確定 this 的取值規則了。

當函式呼叫的時候,this 的取值規則如下:

  1. 計算 MemberExpression 的結果賦值給 ref
  2. 判斷 ref 是不是一個 Reference 型別

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

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

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

具體分析

讓我們一步一步來分析:

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

MemberExpression :

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

舉個例子:

```js 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 型別。

this 取值規則舉例

我們最後來看個例子,看看例子中各個 this 的取值。

``` 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()

規範 11.2.1 說明 Property Accessors 是一個 Reference。

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

js var Reference = { base: foo, name: "bar", strict: false };

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

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

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

這個時候我們就可以確定 this 的值為 GetBase(ref)。

GetBase 方法返回 reference 的 base value,所以 this 的值就是 foo ,示例 1 列印的結果是 2。

(foo.bar)()

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

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

(foo.bar = foo.bar)()

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

Let rval be GetValue(rref).

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

按照之前講的判斷邏輯:如果 ref 不是 Reference,那麼 this 的值為 undefined

在非嚴格模式下,this 的值為 undefined 時,其值會被隱式轉換為全域性物件,所以列印的結果是 1.

(false || foo.bar)()

檢視規範 11.11 Binary Logical Operators:

Let lval be GetValue(lref).

因為使用了 GetValue,所以返回的不是 Reference 型別,this 為 undefined,列印的結果是 1。

(foo.bar, foo.bar)()

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

Call GetValue(lref).

因為使用了 GetValue,所以返回的不是 Reference 型別,this 為 undefined,列印的結果是 1。

例子列印結果

所以例子的列印結果如下:

```js 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 就會報錯。

補充

最後,補充一個最普通的情況:

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

foo(); ```

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

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

由於 IsPropertyReference(ref) 的結果為 false,進入 2.2 判斷:

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

檢視規範 10.2.1.1.6:ImplicitThisValue 方法始終返回 undefined。

所以最後 this 的值是 undefined。

總結

我們可以簡單地理解成 this 為呼叫函式的物件,不過只有結合規範在一些情況下我們才能更好地判斷 this。

比如下面這個例子,大家覺得會打印出什麼值呢?

```js var value = 1;

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

歡迎關注微信公眾號:前端閱讀室