前言
這篇文章的產生,是基於冴羽大大的JavaScript 深入之從 ECMAScript 規範解讀 this的思考,這是對應掘金鍊接,文中詳細的論述了前因後果,建議各位都可以去了解一下,很有幫助,並且這篇文章在寫作時,也有冴羽大大的幫助,再次表示感謝~
文中的 ES5
規範是參考 顏海鏡大大 的譯本,也在這裡表示感謝。
那為什麼還有這篇文章呢?因為很多的同學在冴羽大大的部落格下評論沒有看懂,我也是其中的一員,於是我決定要弄明白為什麼,現在也把我的一些整理分享出來,希望對大家也有幫助。
再囉嗦一句,對於知道了各種情況下 this
如何判斷的同學來說,這篇文章並不會告訴你如何進行 this
指向的判斷,更多的是知道為什麼這樣判斷,不滿足於知其然,更知其所以然。
一. 從 Reference Type
(引用型別)開始:
Reference Type
:引用型別。在 ES5 文件標準中,將Reference
描述為 a resolved name binding
顏大的 ES5 譯本 中,譯為已解決的命名繫結。
-
resolved
翻譯為已完成
-
name binding
翻譯為命名繫結
沒有任何問題,如果有後端語言經驗的同學可能更好理解。
那我們再解釋下命名繫結:繫結是有雙方的,把 命名
,也就是 我們取的名字
,要繫結在 某個東西
上面,換言之,就是用 名字
來描述了一個什麼 東西
。
1.為什麼需要用一個名字來描述,它沒有自己本身的名字嗎?
舉個例子:
現在我們有一個物件:time
,然後他有三個屬性:
time {
second: 32,
minute: 12,
hour: 10
}
複製程式碼
2.這個物件是存在什麼地方的?
我們定義完成後,它必須存在於某一個地方,才能在後面的程式碼中獲取到它。
存在哪由 time
本身的特性來決定,因為它是一個物件,內部的屬性是可以新增也可以減少的,換言之,它的大小並不固定。所以我們把它存在了 堆
裡面。
那如果它的大小固定呢?例如 JavaScript
中的 6 種基本型別的值 :null
,undefined
,Boolean
,Number
,String
, Symbol
,既然大小固定,我們就可以放在 棧
裡面。
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 Type
和 this
有什麼關係?
this
在 Javascript
中一直是一個初學者難以理解的點,有一些甚至寫了 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.
複製程式碼
我就不翻譯了,因為就算翻譯出來你可能也讀得很累,那麼我們用圖來看下這個流程會更加直觀。
我已經把最關鍵的幾個步驟都標紅了,如果在第三步返回的 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
:
test()
的Ref
就是test
引用,它關聯到在記憶體中儲存了test()
的某一片段。- 判斷
test()
是否為引用型別 =>true
- 判斷
Ref
是否是屬性引用型別 =>false
,它並沒有定義在某個引用型別的內部。 - 進入到圖中的第九個步驟:
this = ImplicitThisValue(Ref)
,在Environment Records
下返回undefined
,而在非嚴格模式下,瀏覽器會把this
指向window
說起來很麻煩,其實理解起來很簡單。
2.在物件內部呼叫
function test() {
console.log(this.a);
}
let parent = {
a: 's',
test: test
};
parent.test(); // s
複製程式碼
parent.test()
的Ref
就是parent.test
引用,它關聯到在記憶體中儲存了test()
的某一片段。- 判斷
parent.test()
是否為引用型別 =>true
- 判斷
Ref
是否是屬性引用型別 =>true
- 進入到圖中的第八個步驟:
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 上的解釋,明確的指出了
- 一個繼承自 Foo.prototype 的新物件被建立。
- 使用指定的引數呼叫建構函式 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
不為 arguments
, super
, this
或 new.target
定義本地繫結。 對 ArrowFunction
中的 arguments
, super
, this
或 new.target
的任何引用都必須解析為詞法作用域中的繫結。"
也就是說,箭頭函式內部不會定義 this
,都是由它外部的詞法作用域來決定的,也就是說,箭頭函式的外部的 this
指向的是誰,那箭頭函式內部的 this
指向的也是誰。
回到這個例子,我們知道至始至終,無論你套多少層箭頭函式,this
都是指向 Foo
裡面的 this
,那Foo
裡面的 this
根據我們之前的例子可以知道,就是指向了 window
。
四.最後
歡迎大家關注我的掘金專欄,後期也會更新更多優質的內容~ 有任何問題,歡迎理性和友好的討論~
題圖來自 unsplash