精讀Javascript系列(四)過渡篇:左值與This綁定

前言:

總結前三篇的內容,可以將學習路線圖歸納爲以下:
變量與標識符 —> 詞法環境與作用域 —> 詞法環境與環境記錄 —> 詞法環境與執行上下文

執行上下文與執行棧 —> 執行棧與任務序列 —> 事件循環入門

從這個路線中可以看出,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.propundefined

這裏主要考察的是環境記錄標識符解析以及賦值操作這三個基礎知識點。

首先回顧一下基礎知識點:

  1. 變量是指存儲區,或是直接理解爲左值,它必須是可賦值的
  2. 標識符即變量名,它是一個字符串名稱,通過它能夠找到並引用指定的變量進行操作, 補充一點:屬性名也是一種標識符(相反則不然)。
  3. 標識符和變量的映射將被綁定到環境記錄中。

換言之,將詞法環境中解析成一個變量的前提條件有二:

  • 它是一個標識符
  • 它是一個屬性名

在賦值過程中:

 A = B

只看起到關鍵作用的前兩步即可:

  1. 首先找到標識符A所引用的變量,設爲LRef
  2. 然後找到標識符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} ) 

根據賦值順序,即:

  1. 找到a.prop的所引用的變量,設爲<<a.prop>>
  2. 找到a所引用的變量,設爲<<a>>
  3. {b:2222}本身就是數據,所以……

到這裏務必要注意
<<a.prop>><<a>>已經被替換成了左值,換言之不會再有查找變量的過程了。

所以後續發展就是:

  1. <<a>>的數據完全被替換成了{b:2222}
  2. 再將{b:2222}賦給<<a.prop>>

那麼爲什麼b會有prop呢?
這是因爲標識符b引用的也是<<a>>這個左值。

畫成圖就這:
在這裏插入圖片描述
備註:

  • 這也算是一個經典問題,也挺讓人津津樂道了。
  • 其實這裏應該必須加入Reference Specification Type(RST)的概念,但我認爲沒有必要再繼續深入了,所以就略過了。
  • 總而言之,必須明確左值的概念(Google百度就可以,還有許多案例)。事實上還涉及了LHS和RHS操作,只是限於篇幅,不做贅述了。

爲何我要如此強調這點? 目的在於左值的差別對Javascript的影響頗深,例如this的綁定結果等等。也可以不做深入,但請牢記一點: 左值指的是可賦值的事物僅僅代表了數據數據本身是無任何屬性的

This綁定

this的值範圍大致上可以確定爲:

  1. 如果是函數調用,那麼thiswindow,嚴格模式下爲undefined
  2. 如果是作爲一個對象的方法,那麼this指向該對象
  3. 如果是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

備註:

  1. 因爲左值數據的區別往往會導致很多惱人的問題,所以不得不強調一下。
  2. 說這些,其實對實踐沒有幫助,只是橫練基本功而已,不過對源代碼解讀上貌似很有幫助。
  3. 其實上面的解釋都不夠規範,詳細請參考解讀MemberExpression、RST的文章,要解釋這些,僅幾千字是不行的,可以在進階後參考。

最後:進階的必要性

沒辦法了,閉包放在下一篇吧……我順便解釋一下爲什麼要進階。

這章有點水了,權當過渡吧。如果從普世角度將,就算不進階,想必你也可以負責前端所有業務;但是爲什麼說進階是必要的呢,因爲開發人員必須擁有深度解讀源代碼的能力,這樣才能快速適應潮流我已經不想再學了,所以我就直接看源代碼,畢竟你不能出現一個框架,就要再經歷一次學習過程吧。

想要深度解讀源代碼,只有對知識的廣度認知是完全不夠的(可惜這點少有人意識到)。簡單來說廣度就是你在相關領域的知識量,而深度則是指在你所知內有多少是涉及底層的。因此進階的主要目的就是深入底層實現,而不是重頭複習鞏固,大部分都以爲重學Javascript就可以進階,事實上是不正確的。

深度解讀源代碼可以幫助你極快的跟進潮流,這也不是憑空捏造,有某個大佬就不願意看Vue編排的文檔直接就啃源代碼……那水平真的。這也是在程序員所必學知識逐漸增多時唯一的應對手段了。Javascript的進階是漫長的,但不是說很困難,因爲只要你找到了貼合規範的讀物(例如:阮一峯大佬的ES6入門教程),就能增加理解上的深度。

Javascript的進階另一個難點就是容易走彎路,這是沒辦法規避的; 但無論如何也不要重新看遍網課,網上課程帶領你入門,卻沒有繼續讓你深入的義務,但是也可以去看看大佬是怎麼解讀源代碼的,學習其中的方法也很重要。那麼祝各位學有所成吧,謝君來此一覽。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章