換個角度看 JavaScript 中的 (this) => { 整理 (JavaScript 深入之從 ECMAScript 規範解讀 this ) }

dendoink發表於2018-12-21

前言

這篇文章的產生,是基於冴羽大大的JavaScript 深入之從 ECMAScript 規範解讀 this的思考,這是對應掘金鍊接,文中詳細的論述了前因後果,建議各位都可以去了解一下,很有幫助,並且這篇文章在寫作時,也有冴羽大大的幫助,再次表示感謝~

文中的 ES5 規範是參考 顏海鏡大大 的譯本,也在這裡表示感謝。

那為什麼還有這篇文章呢?因為很多的同學在冴羽大大的部落格下評論沒有看懂,我也是其中的一員,於是我決定要弄明白為什麼,現在也把我的一些整理分享出來,希望對大家也有幫助。

再囉嗦一句,對於知道了各種情況下 this 如何判斷的同學來說,這篇文章並不會告訴你如何進行 this 指向的判斷,更多的是知道為什麼這樣判斷,不滿足於知其然,更知其所以然。

一. 從 Reference Type (引用型別)開始:

Reference Type :引用型別。在 ES5 文件標準中,將Reference 描述為 a resolved name binding

顏大的 ES5 譯本 中,譯為已解決的命名繫結。

  1. resolved 翻譯為 已完成

  2. name binding 翻譯為 命名繫結 沒有任何問題,如果有後端語言經驗的同學可能更好理解。

那我們再解釋下命名繫結:繫結是有雙方的,把 命名 ,也就是 我們取的名字 ,要繫結在 某個東西 上面,換言之,就是用 名字 來描述了一個什麼 東西

1.為什麼需要用一個名字來描述,它沒有自己本身的名字嗎?

舉個例子: 現在我們有一個物件:time ,然後他有三個屬性:

 time {
   second: 32,
   minute: 12,
   hour: 10
 }
複製程式碼

2.這個物件是存在什麼地方的?

我們定義完成後,它必須存在於某一個地方,才能在後面的程式碼中獲取到它。

存在哪由 time 本身的特性來決定,因為它是一個物件,內部的屬性是可以新增也可以減少的,換言之,它的大小並不固定。所以我們把它存在了 裡面。

如果它的大小固定呢?例如 JavaScript 中的 6 種基本型別的值 :nullundefinedBooleanNumberStringSymbol,既然大小固定,我們就可以放在 裡面。


3.堆疊是什麼?為什麼不同型別的資料要分開放呢?

  • :程式執行時系統分配的一小塊記憶體,大小在編譯期時由編譯器引數決定。
  • :可以理解為當前可以使用的空閒記憶體,其大小是需要程式碼編寫的人員自己去申請和釋放。(在 JS 中,V8 下有自動垃圾回收機制不需要我們自己操作) [這裡只做簡單解釋,有需要可以自行 Google 更多資訊]

4.引用型別存在堆中,和例子有什麼關係?

Okay。如果你已經理解了我們的 time 是存在堆中的,那就很好理解了。現在我要用到 time 裡面 second屬性的值, 我們都知道用 time.second 就可以拿到,但是為什麼 time.second 或者 time[’second’] 就可以訪問到 second 屬性的值呢?

看起來這個問題很蠢是不是,哈哈,但是仔細想想,按理來說:這個值是在記憶體裡面的一小塊上面,那我們需要找到這一塊記憶體,才能取到這個值啊。

現在就很好理解了。那其實 time.second 或者 time[’second’] 他們是和記憶體裡面的真正存放 second 的值那個記憶體位置 是繫結在一起的。只要你用到了 time.second 或者 time[’second’],那編譯器就找到,哦,這就是存在xxxxx 地址裡面的值也就是 32

二. Reference Typethis 有什麼關係?

thisJavascript 中一直是一個初學者難以理解的點,有一些甚至寫了 2 年的專案也沒搞明白為什麼 this 有這樣那樣的不同。

這裡我們先不從使用的場景上來看 this 的指向,還是迴歸到本源。

站在編譯器的角度,是怎麼樣去理解 this 指向呢?因為this 的指向的判斷,常常發生於函式的呼叫中,那我們就來看看ES5 文件標準中的 11.2.3 Function Calls (函式呼叫)。

一共分為 8 個步驟:


1. Let ref be the result of evaluating MemberExpression.

2. Let func be GetValue(ref).

3. Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).

4. If Type(func) is not Object, throw a TypeError exception.

5. If IsCallable(func) is false, throw a TypeError exception.

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 Implicit This Value concrete method of GetBase(ref).

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

    a.Let thisValue be undefined.

8. Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.
複製程式碼

我就不翻譯了,因為就算翻譯出來你可能也讀得很累,那麼我們用圖來看下這個流程會更加直觀。

換個角度看 JavaScript 中的 (this) => { 整理 (JavaScript 深入之從 ECMAScript 規範解讀 this ) }

我已經把最關鍵的幾個步驟都標紅了,如果在第三步返回的 func 無法通過 5 的判斷的話,根本就沒有討論 this 指向的必要。

所以我們重點看:這個裡面最關鍵的點,第 2 , 6 , 7 步驟:

  • 2 步:計算 MemberExpression 的值並且賦值給 Ref: 也就是計算 () 左邊的內容的結果,並賦值給 Ref。換句話說: Ref 就是對於 () 左邊的內容進行計算之後的引用。

  • 6 步:判斷 ref 是否為 Reference 型別: 這個沒什麼好說的。

  • 7 步:判斷 ref 是否是屬性引用型別: 官方解釋: 通過執行IsPropertyReference(V)來判斷的,如果基值是個物件或 HasPrimitiveBase(V) 是 true,那麼返回 true;否則返回 false。 HasPrimitiveBase(V):如果基值是 Boolean, String, Number,那麼返回 true。 換成大白話,取決於Ref這個引用是基於誰的? 如果它基於一個物件 或者 Boolean, String, Number那就返回 true 否則返回 false

OK 看到這裡,估計你也有些累,但是最關鍵的部分在下面。

三. 回過頭來看 this 的 N 種情況

1.直接呼叫

let a = 'm';
function test() {
  console.log(this.a);
}
test(); // m
複製程式碼

我們用剛剛所看到的 3 個步驟來判斷下 this:

  1. test()Ref 就是 test引用,它關聯到在記憶體中儲存了test()的某一片段。
  2. 判斷 test() 是否為引用型別 => true
  3. 判斷 Ref 是否是屬性引用型別 => false,它並沒有定義在某個引用型別的內部。
  4. 進入到圖中的第九個步驟:this = ImplicitThisValue(Ref) ,在 Environment Records 下返回 undefined ,而在非嚴格模式下,瀏覽器會把 this 指向 window

說起來很麻煩,其實理解起來很簡單。

2.在物件內部呼叫

function test() {
  console.log(this.a);
}
let parent = {
  a: 's',
  test: test
};
parent.test(); // s
複製程式碼
  1. parent.test()Ref 就是 parent.test引用,它關聯到在記憶體中儲存了test()的某一片段。
  2. 判斷 parent.test() 是否為引用型別 => true
  3. 判斷 Ref 是否是屬性引用型別 => true
  4. 進入到圖中的第八個步驟:this = GetBase(Ref) 那這個test() 方法是基於誰呢?很明顯就是 parent,所以 this 指向 parent

3.new 關鍵字

let a = 'k';
function Foo() {
  console.log(this);
}
let c = new Foo();
c.a = 's'
複製程式碼

new 關鍵字呼叫,區別於一般的函式呼叫,大家可以看下MDN 上的解釋,明確的指出了

  1. 一個繼承自 Foo.prototype 的新物件被建立。
  2. 使用指定的引數呼叫建構函式 Foo ,並將 this 繫結到新建立的物件

如果你仍舊想從規範的角度來解釋,建議你讀一下ES5 規範:11.2.2 The new Operator 以及關聯的 ES5 規範:8.7.1 GetValue (V) 我反覆了讀了很多遍,但是沒有發現如何從規範的角度去解釋 this 的指向問題,最後也是請教了冴羽大大才知道 new 可能在底層有明確指定 this的過程,不適合用這樣的方式解讀,但是,如果你有了更好的答案,很歡迎一起討論~

既然明白了this 指向的是 c 那麼輸出的是 {a : 's'}

4.箭頭函式

function Foo() {
  return () => {
    return () => {
      console.log(this);
    };
  };
}
console.log(Foo()()());
複製程式碼

new 一樣箭頭函式也是一個特例,但是箭頭函式同樣可以從對應的規範中找到 this 的答案:

建議參考ES6 規範-箭頭函式-evaluation裡面的一段話:

“An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. ”

直譯為:"ArrowFunction 不為 argumentssuperthisnew.target 定義本地繫結。 對 ArrowFunction 中的 argumentssuperthisnew.target 的任何引用都必須解析為詞法作用域中的繫結。"

也就是說,箭頭函式內部不會定義 this ,都是由它外部的詞法作用域來決定的,也就是說,箭頭函式的外部的 this 指向的是誰,那箭頭函式內部的 this 指向的也是誰。

回到這個例子,我們知道至始至終,無論你套多少層箭頭函式,this 都是指向 Foo 裡面的 this,那Foo 裡面的 this 根據我們之前的例子可以知道,就是指向了 window

四.最後

歡迎大家關注我的掘金專欄,後期也會更新更多優質的內容~ 有任何問題,歡迎理性和友好的討論~

題圖來自 unsplash

相關文章