前言:
總結前三篇的內容,可以將學習路線圖歸納爲以下:
變量與標識符 —> 詞法環境與作用域 —> 詞法環境與環境記錄 —> 詞法環境與執行上下文
執行上下文與執行棧 —> 執行棧與任務序列 —> 事件循環入門 。
從這個路線中可以看出,Javascript進階的基本脈絡了。每個人進階的方法都不盡相同,不過儘可能避免知識斷層或是概念套路化。否則真的,不論你嘗試進階多少次,到頭來可能還是竹籃打水一場空。進階的大部分資料都來自github大佬博客、ES8標準翻譯以及ES2020,博文編撰時也借鑑了不少,的確發發博客的確更容易堅持下來,哎。
言歸正傳,請不要忽略Javascript中的迷惑行爲不要停止調戲Javascript, 舉個例子,暫存死區和變量提升,這兩個看起來無關緊要的概念,但實際上卻涉及了非常非常重要的底層知識——環境記錄,諸如此類數不勝數。 總的來說就是一定要注意避免概念套路化,如果把這些迷惑行爲視作死規則、死套路,你可能錯過許多重要內容。
如果還沒有意識到這些概念影響到什麼,可以去參考比較底層的JS源代碼,看不懂沒關係,主要是用所學儘可能解釋這些行爲,解釋不了自然就意味着自己進階時不小心遺漏什麼關鍵概念。
下面我列舉了兩個特殊情形,記住這種感覺。
左值
何謂左值? 這些還是要從標識符解析開始…… 簡單來說它代表了內存中的可操作空間,但是Javascript中的左值稍微特別,因爲它記作Lref
,事實上,在Javascript較底層操作中,標識符和變量是真正意義上的引用關係,正因爲如此,任一標識符都能夠成爲左值(事實上它也能夠成爲右值)。
見以下代碼:
var a = {a:111}
var b = a;
a.prop = a = {b:2222}
console.log(a.prop); // undefined
console.log(b.prop); // object
這裏出現一個很迷惑的情形: a.prop
爲undefined
。
這裏主要考察的是環境記錄、標識符解析以及賦值操作這三個基礎知識點。
首先回顧一下基礎知識點:
- 變量是指存儲區,或是直接理解爲左值,它必須是可賦值的。
- 標識符即變量名,它是一個字符串名稱,通過它能夠找到並引用指定的變量進行操作, 補充一點:屬性名也是一種標識符(相反則不然)。
- 標識符和變量的映射將被綁定到環境記錄中。
換言之,將詞法環境中解析成一個變量的前提條件有二:
- 它是一個標識符
- 它是一個屬性名
在賦值過程中:
A = B
只看起到關鍵作用的前兩步即可:
- 首先找到標識符
A
所引用的變量
,設爲LRef
- 然後找到標識符
B
所引用的變量
,取出其中數據,設爲RVal
。
……
(上述順序參考自規範,其實顛倒過來,左側也必須是LRef
,右側也必須是RVal
, 個人理解)
回到上面的代碼:a.prop = a = {b:2222}
它可以寫成下面的僞代碼:
<<a.prop>> = <<a>> = {b:2222}
// <<a>>表示標識符a所引用的變量
代入優先級:
<<a.prop>> = ( <<a>> = {b:2222} )
// part A : <<a.prop>>
// part B: ( <<a>> = {b:2222} )
根據賦值順序,即:
- 找到
a.prop
的所引用的變量,設爲<<a.prop>>
- 找到
a
所引用的變量,設爲<<a>>
- …
{b:2222}
本身就是數據,所以……
到這裏務必要注意:
<<a.prop>>
和 <<a>>
已經被替換成了左值,換言之不會再有查找變量的過程了。
所以後續發展就是:
<<a>>
的數據完全被替換成了{b:2222}
- 再將
{b:2222}
賦給<<a.prop>>
…
那麼爲什麼b
會有prop
呢?
這是因爲標識符b
引用的也是<<a>>
這個左值。
畫成圖就這:
備註:
- 這也算是一個經典問題,也挺讓人津津樂道了。
- 其實這裏應該必須加入Reference Specification Type(RST)的概念,但我認爲沒有必要再繼續深入了,所以就略過了。
- 總而言之,必須明確左值的概念(Google百度就可以,還有許多案例)。事實上還涉及了LHS和RHS操作,只是限於篇幅,不做贅述了。
爲何我要如此強調這點? 目的在於左值和值的差別對Javascript的影響頗深,例如this的綁定結果等等。也可以不做深入,但請牢記一點: 左值指的是可賦值的事物而值僅僅代表了數據, 數據本身是無任何屬性的。
This綁定
this的值範圍大致上可以確定爲:
- 如果是函數調用,那麼
this
爲window
,嚴格模式下爲undefined
- 如果是作爲一個對象的方法,那麼
this
指向該對象。 - 如果是
new F()
創建新實例,那麼this
指向這個實例。
其他的情形例如不能爲this賦值什麼的,this是運行時概念等等。
大部分情況下,這些規則都是正確的。但只有一個前提,函數必須是一個左值。
見以下代碼:
var o = {}
var f = function(){
if(this === window)
console.log('window');
else if(this === o)
console.log('object');
}
o.f = f;
然後:
o.f() // object
f(); // window
//開始調戲行爲:
(f=o.f)(); // window
(o.f=f)(); // window
(o.f)(); // object
(1&&o.f)(); // window
(o.f=o.f)(); // window
圓括號主要用於表達式分組,如果沒有操作符,那麼就什麼也不做,所以(o.f)()
的this
仍然爲object
的原因。
但是在其他語句中,圓括號都有一個左值變數據的過程,雖然函數作爲數據依然能被調用,但與左值也有了本質區別。
甚至:(,o.f)()
也會返回window
。
備註:
- 因爲左值和數據的區別往往會導致很多惱人的問題,所以不得不強調一下。
- 說這些,其實對實踐沒有幫助,只是橫練基本功而已,不過對源代碼解讀上貌似很有幫助。
- 其實上面的解釋都不夠規範,詳細請參考解讀MemberExpression、RST的文章,要解釋這些,僅幾千字是不行的,可以在進階後參考。
最後:進階的必要性
沒辦法了,閉包放在下一篇吧……我順便解釋一下爲什麼要進階。
這章有點水了,權當過渡吧。如果從普世角度將,就算不進階,想必你也可以負責前端所有業務;但是爲什麼說進階是必要的呢,因爲開發人員必須擁有深度解讀源代碼的能力,這樣才能快速適應潮流我已經不想再學了,所以我就直接看源代碼,畢竟你不能出現一個框架,就要再經歷一次學習過程吧。
想要深度解讀源代碼,只有對知識的廣度認知是完全不夠的(可惜這點少有人意識到)。簡單來說廣度就是你在相關領域的知識量,而深度則是指在你所知內有多少是涉及底層的。因此進階的主要目的就是深入底層實現,而不是重頭複習鞏固,大部分都以爲重學Javascript就可以進階,事實上是不正確的。
深度解讀源代碼可以幫助你極快的跟進潮流,這也不是憑空捏造,有某個大佬就不願意看Vue編排的文檔直接就啃源代碼……那水平真的。這也是在程序員所必學知識逐漸增多時唯一的應對手段了。Javascript的進階是漫長的,但不是說很困難,因爲只要你找到了貼合規範的讀物(例如:阮一峯大佬的ES6入門教程),就能增加理解上的深度。
Javascript的進階另一個難點就是容易走彎路,這是沒辦法規避的; 但無論如何也不要重新看遍網課,網上課程帶領你入門,卻沒有繼續讓你深入的義務,但是也可以去看看大佬是怎麼解讀源代碼的,學習其中的方法也很重要。那麼祝各位學有所成吧,謝君來此一覽。