iT邦幫忙

2021 iThome 鐵人賽

DAY 6
0
Modern Web

舌尖上的JS系列 第 6

D6 - 你不知道 Combo : 主菜 Scope 字彙環境

前言

前一章解釋 Hoising 時說到,JavaScript 一開始會先記下變數名稱 和 Function Declaraion,因此可以做到提前呼叫 function 並印出結果。

但,JavaScript 到底是怎麼追蹤這些資訊? 是怎麼紀錄又紀錄在哪裡呢?

這一切的答案,都是有關 Scope 或稱為 字彙環境 Lexical Environment,來到此系列的主菜
在尋找資料時,看到 你所不知道的JS 這本書以擬人化的方式模擬了引擎解讀 code 的過程,覺得有趣又好理解,因此這篇文的開頭決定也這種方式切入,絕對好入口
各位食客請用餐


程式小劇場

介紹一下即將出場的角色:

🤖 編譯器 、💻 引擎、 Mr. 🐷 Scope、 Miss 🐰 Scope

對話如下:
🤖:在引擎開始上班前我要趕快先做好份內的工作!讓我先掃掃這段程式碼,哦,在第2行發現一個 var 的變數宣告,Mr. 🐷 Scope 請您紀錄一個名為 cookie 的 變數
🐷:好的,記下了

🤖:(繼續掃描) 在第 5 行發現了一個 Function Declaration,Mr. 🐷 Scope 請你記錄一個名為 getCookie 的函數內容
🐷:好的,記下了

🤖:(繼續掃描) 在第6行發現一個 var 的變數宣告, Miss 🐰 Scope 請你記錄一個名為 number 的 變數
🐰:好的,記下了

🤖:搜查完所有的宣告,引擎先生,我的工作結束換你接手囉
💻:各位,現在開始正式執行程式!從第 1 行開始, console.log 要印出 cookie, Mr. 🐷 Scope 請問您知道 cookie 嗎?
🐷:有,我這裡記錄它的值是 undefined
💻:謝謝,第一行印出 undefined。第 2 行賦予變數 cookie 值為字串 'Oreo',Mr. 🐷 Scope 請記錄下這個值
🐷:好的,小本本已修改,cookie 初始化成功,從 undefined 改為 'Oreo'

💻:繼續執行,第 3 行呼叫 getCookie, Mr. 🐷 Scope,請問你知道 getCookie 嗎?
🐷:有,我這裡記錄它是一個 function,位置在第 5 行
💻:好的,前往第 5 行執行 getCookie function,哈囉, Miss 🐰 Scope 來到你的管轄區域囉!
🐰:好的好的
💻:我看到這裡第 6 行是宣告一個變數 number,Miss 🐰 Scope 請去找到這個變數並將它的值紀錄為 2
🐰:好的,小本本已修改,number 初始化成功從 undefined 改為 2

💻:第 7 行使用 console.log 要印出一串字串,需要知道兩個變數的值,Miss 🐰 Scope 請問你知道 number 和 cookie 是什麼嗎?
🐰:我的小本本上紀錄,number 值是 2,但不知道 cookie,你可以問問我的上一層 Mr.🐷 Scope 那有沒有記錄
💻:好的,謝謝。Mr. 🐷 Scope 請問你知道 cookie 是什麼嗎?
🐷:哦哦知道啊,我的小本本紀錄 cookie 的值是 'Oreo'
💻:太棒了,兩個變數的值都拿到,第 7 行成功印出 'Give me 2 pieces of Oreo'
💻:程式碼執行結束,下班!


以上就是模擬 JavaScript 引擎在做的事情,其實隱含了很多底層執行的動作,像是編譯及查找還可以探討很多,但今天先著重在如何取得變數及 function 資訊這件事上。
從對話中發現,引擎在確認值的時候都是詢問 🐰 🐷 ,有著一個小本本紀錄著這些訊息,他們是誰??
可以稱呼他們 Scope , 或是有個更正式的名稱為 Lexical environment(也有人稱 Lexical Scope)

字彙環境 Lexical Environment

故事中的小本本是真實存在的! 在 JavaScript 的每個 Scope (不論區塊、function 或全域),都有一個隱藏的 Object,它的 property 就是上述的那些資訊,但無法讀或取到它,所以它是隱藏的 Object。

Lexical Environment 字彙環境內的資訊可以分成兩部分:

第一部分:該區域內的記錄

記下了該範圍內的變數、function 及參數 parameter,沒錯,連參數的值也會被記錄在這,畢竟參數也可以算是個存有值的容器嘛

第二部分:連結上一層環境的參照 Reference

參照 Reference 是什麼呢? ;
故事中,為什麼在 🐰 小本本內不存在的 cookie 還是印得出來?那不是記錄在 🐷 小本本嗎?

原因就是,連結上一層環境的參照

當 引擎詢問 🐰 cookie 的資訊時,因為 🐰 存有他的上一層環境 🐷 的參照,所以引擎可以往 🐷 的小本本內查找,而因為程式碼層層包裹的結構,構成了內外環境的對應關係,這個就稱為 Nested Scope

巢狀 Nested Scope

故事講完了,我們來改畫圖解說

將這段程式碼的字彙環境畫出來會長得像下面這樣,getCookie function 寫在全域下,所以對 getCookie 來說,全域就是它的外部環境 outer environment,每個字彙環境都存有其外部字彙環境的 參照 reference,如圖中的箭頭指向了上一層的字彙環境,提供路徑般供引擎可以層層往上查找,直到最末端的全域為止,而這一個指向一個的箭頭像個鏈條連起每個 Scope,也就是 Scope Chain 的概念。

上述的例子只有提到兩個 Scope,分別是 全域 Scope 及 function Scope,但在 JavaScript 還存有另一個更小的 Scope 範圍 - 區塊 Block Scope

區塊 Block Scope

來看看另一個更複雜的結構,直接將各個 console.log 印出的結果備註在後面,比較一下跟你心裡想的答案是不是一樣?

開始來看看每個 Scope ,第 2 行的 cookie 可以成功在全域的字彙環境中找到,順利印出 Oreo,沒問題

再來進入到 countCookie function 的範圍內,首先是第 6 行,如果以為會印出 Oreo 那就不對了!你可能會混亂以為這時引擎會往上查找到全域 Scope,但別忘了,這個 function 區域也有個 cookie 宣告,編譯器會留意到這個宣告,在執行前將它存入 countcookie 的字彙環境中,因此不需要往外查找,只是尚未初始化前 countcookie Scope 只知道 cookie 的名字但不知道它的值(忘了請看上一章),這時要印出它當然拋出錯誤!

經過第 7 行賦值初始化成功後,第 8 行就可以印出 Ritz 了。
繼續往下,這時碰到 for loop

它是 function Scope 內的 Block Scope,將第 6 行的解釋同理套用到第 11 行的錯誤訊息,此時 Block Scope 字彙環境內的 cookie 尚未初始化,一樣也是拋出錯誤。

好的,讓我們將會產生錯誤訊息的 code 拿掉然後將字彙環境畫出來


注意:i:1 為迴圈剛開始的值,隨著每一圈重新賦與新的數字

這樣對照著看是不是就很清楚每個 scope 內可以取到什麼值
那,若今天的所有宣告改為 var 有什麼不一樣嗎?

var 宣告下的區塊 Block Scope

大家來找碴!直接幫你圈出哪裡不一樣

  1. 第 6 行印出 undefined
  2. 第 11 行不是拋出錯誤訊息,隨著迴圈第一次印出 Ritz,第二次後印出 Lays
  3. 第 15 行印出 Lays
  4. 字彙環境圖中的 Block Scope 內容跑到 Function Scope 內了

var 就是這麼的出奇不意,第 6 行同剛剛 let 的解釋一樣,因為在 function scope 內也有 cookie 變數宣告,因此執行到第 7 行的賦值前,字彙環境記錄的值還是 undefined。
那第 11 行呢? 為什麼不是 undefined,而是可以印出值呢?

答案就是:var 無視 Block Scope,它的宣告只會存在最近的 function Scope 內或是全域 Scope

所以當程式執行到第 11 行時,在 Block Scope 內查無 cookie 這個變數,便會循著 refernce 往外層的 countCookie function Scope 查找,此時的 cookie 值是 Ritz,因此第一次便會印出 Ritz,接下來程式執行到第 12 行,cookie 被 重新賦值為 Lays,此時 countCookie 的字彙空間修改 cookie 的值為 Lays,所以第 13 行以及隨著迴圈重新來到的第 11 行,都會印出 Lays

而 第 15 行也是一樣的結論,由於第 12 行的關係,countCookie Function Scope 的 cookie 重新賦值為 Lays,所以跟 let 舉例的結果不同,這裡會印出 Lays。

整理了 Block Scope 字彙環境下不同的宣告差異:

  • letconst 宣告的變數會存在最近的 Scope
  • var 宣告的變數不會存在 Block Scope,而是最近的 function Scope 或 全域 Scope

結論

參考了很多文章和書籍,很怕講的不完整,有很多不同的切入點做解釋,每個知識點都細探分享的話這篇就不是主菜而是滿漢全席,所以最終拍板決定這篇文著重在 Scope 字彙環境的部分,編譯和 Execution Context的部分留到下一餐!

希望這樣的講解能讓各位食客們好下嚥
/images/emoticon/emoticon25.gif

Reference:

書籍:
[你所不知道的JS] by Kyle Simpon
[忍者 JavaScript 開發技巧探秘第二版] by John Resig, Bear Bibeault, Josip Maras

文章:
How Lexical Environments affect JavaScript Variables, Hoisting & Closures
Hoisting in JavaScript
我知道你懂 hoisting,可是你了解到多深?
Hoisting in JavaScript


上一篇
D5 - 你不知道 Combo : 前菜 Hoisting
下一篇
D7 - 你不知道Combo: 第二主菜 Execution Context
系列文
舌尖上的JS30
2
Hooo
iT邦新手 5 級 ‧ 2021-09-21 15:36:08

快被 emoji 搞瘋!!! 不敢再編輯怕又轉換失敗只好自己留言崩潰一下

1
Chiahsuan
iT邦新手 5 級 ‧ 2021-09-21 16:03:45

圖文並茂超級用心的好文章~~/images/emoticon/emoticon24.gif

Hooo iT邦新手 5 級 ‧ 2021-09-22 14:37:15 檢舉

挖了一個坑快逼死自己/images/emoticon/emoticon02.gif

0
南國ㄟ安迪
iT邦新手 5 級 ‧ 2021-09-22 16:00:09

圖文並茂超級用心的好文章~~/images/emoticon/emoticon24.gif

Hooo iT邦新手 5 級 ‧ 2021-09-22 17:05:58 檢舉

不要複製貼上

我要留言

立即登入留言