【克服 JS 】第二章 執行環境與詞彙環境
JavaScript 全攻略:克服 JS 的奇怪部分,第二章 執行環境與詞彙環境 Execution Contexts and Lexical Environments,學習紀錄。
Udemy 課程連結:
JavaScript 全攻略:克服 JS 的奇怪部分
2-1 觀念小叮嚀
- 語法解析器 Syntax parsers
- 詞彙環境 Lexical environments
- 執行環境 Execution contexts
- 名稱/值配的配對與物件 Name/Value pair and Object
1. 語法解析器
將合乎語法的程式,依照規則將其轉換成電腦認識的語言,而這個中介的引擎被稱為直譯器、轉譯器。
2. 詞彙環境
當我們在編輯器存檔,在瀏覽器重新整理時,JS 不會給電腦直接執行,而是透過引擎和語法解析器轉譯、翻譯給電腦執行,而程式語法所寫在的位置,可以影響執行階段時,它對應的記憶體位置,也能影響它和其他變數函式的互動。所以談到詞彙環境就是指開發環境中的程式位置。
3. 執行環境
JavaScript 在執行時,其實並不會完全照我們開發時期寫的 JS 逐字逐行去執行。
我們的程式碼會在執行時,會先依照詞彙環境被解析器轉換,在電腦中被創造並擺到該放的記憶體位置去,最後電腦才執行。尤其 JavaScript 某些特性(例如提升)就是在創造階段產生,因此理解 JavaScript 在執行環境中的狀態是很重要的一件事。
4. 名稱/值的配對與物件
JavaScript 中的物件,就是一堆名稱/值的組合而已,不像其他語言的物件有一堆特性。
var Address = {
Street: 'Main',
Number: 100,
Apartment:{
Floor: 3,
Number: 301
}
}
物件中也可以包含物件,像是上面的 Address 物件包含了一個 Apartment 物件。
2-2 全域環境與全域物件 Global environment and Global Object
JavaScript 一開始在還沒寫任何程式碼時,就會先自動建立一個全域的執行環境,在瀏覽器的執行環境下,會先建立個 window 的全域物件,在全域環境中,他和 this 是相同的。。
全域(Global):程式碼或變數不在一個函式裡,就是全域的(Not inside a function.)。
全域的變數或函式會被放在 window 物件之下。
2-3 執行環境:創造與提升(Hoisting)
在同一個執行環境中,會先將使用到的變數存至記憶體,例如:
b(); // 顯示 Called b!
console.log(a); // 顯示 undefined
var a = 'Hello World!'
function b() {
console.log('Called b!')
}
函式 b
定義的內容會先存至記憶體,所以雖然定義寫在後面,但在前面也可以正確的呼叫
變數 a
的定義寫在後面,雖然可以呼叫,但卻顯示 undefined
。
在使用等號賦值的情況下,只會先將變數放至記憶體,再依執行順序將等號的值填進去。
可以想像程式碼變成這樣:
var a;
function b() {
console.log('Called b!')
}
b(); // 顯示 Called b!
console.log(a); // 顯示 undefined
a = 'Hello World!'
var a
被移到最前面去了,而賦值的地方還是一樣在 console.log(a)
後面,而函數 b
是連內容都移到前面去了。
但其實不是 JS 真的會先把程式碼改成這樣,而是執行的順序會變得像這樣。
CodePen:2-3 執行環境:創造與提升(Hoisting)
提升 Hoisting 的參考資料
2-4 觀念小叮嚀:JavaScript 與 undefined
undefined
和 not defined,字面上來看都是未定義、無定義、沒有定義之類的的。但兩者的意涵完全不相同。
undefined
是 JS 的一種特殊值,代表還沒定義。
當使用 var a
建立一個變數 a
,而沒有賦值時,a
的值就是 undefined
,可以用 ===
來判斷 a
是否為 undefined
。
if(a === undefined) {
console.log('a is undefined')
} else {
console.log('a is defined')
}
另外,undefined
不代表什麼都沒有,在 JS 中undefined
既是個型別,也是一個值,這個型別 undefined
的值就叫 undefined
,既然不代表什麼都沒有,那僅宣告變數自然也會佔據記憶體空間。
也可以用 a = undefined
將 a
的值設定 undefined
,但不建議這麼做,應該讓undefined
就代表一個變數剛建立還沒賦值。
小結
undefined
,是建立後尚未賦值時,預設的值,而這個值表示值還沒被定義。- not defined,是變數、函式等,未被宣告建立、定義,在電腦執行程式碼時找不到這個東西,可以想成在程式與記憶體中未被定義。
CodePen:2-4 JavaScript 與 undefined
2-5 執行環境:程式執行
在 JavaScript 產生執行環境時,會有兩個階段:
- 創造階段(Creation Phase):設定變數與 function 到記憶體中、抬升(hoisting)。
- 執行階段(Execution Phase):一行一行執行你寫的程式碼。
2-6 觀念小叮嚀:單執行緒、同步執行
JavaScript 是一步一步執行,並且在一個時間只會做一件事。
2-7 函式呼叫與執行堆 Function invocation and Execution stack
重點字提示: invocation,他的意思是: 執行或是呼叫函式
所以當我們說 invoke the function 或是 function invocation,意思是執行這個函數。
接著,就來看看,執行函式時,JavaScript 會做什麼:
function b() {
}
function a() {
b()
}
a()
- Global Execution Context:產生全域物件、this、設定變數與 function 的記憶體,然後逐行執行程式碼。
- 第九行執行到後,會產生一個
a()
的 Execution Context,放置到 Execution stack 中,stack 的定義就是後進來的先做,接著,在逐行執行a
中的程式碼。所以,當在 JavaScript 環境下執行到function的時候,就會產生一個Execution Context 放置到 stack 的最頂部,並逐行執行 function 內的程式碼。 - 接著跑到第六行,又碰到了一個 function
b
,所以,把b()
放置到 stack 頂端,接著,逐行執行b
的程式碼。 b
執行完之後,就會 pop 出 stack,回到剛剛呼叫 b 的第 6 行,再往後面執行。- 接著,
a
執行完之後,也會 pop 出 stack。 - 最後,stack 最頂端的是 Global execution context,再往下繼續執行程式碼。
2-8 函式、環境與變數環境
變數環境 Variable environment
定義:Where the variable live ,and how they relate to each other in memory.(描述你創造變數的位置,還有他在記憶體中和其他變數的關係。)
來看看以下程式碼:
function b() {
var myVar;
console.log(myVar)
}
function a() {
var myVar = 2
console.log(myVar)
b()
}
var myVar = 1
console.log(myVar)
a()
- 生成Global execution context,並放置在 stack 最頂端,當程式跑到
myVar = 1
時,賦予給它值。 - 接著,執行
a()
,產生a
的 Execution context,放置在 stack 最頂端,當程式跑到myVar = 2
時,賦予給它值。 - 最後,執行
b()
,產生b
的 Execution context,放置在 stack 最頂端,當程式跑到myVar
時,賦予給它值 undefined。
雖然 myVar 被宣告了 3 次,但因為它都在不同的執行環境中,所以這 3 個其實是不同的變數。
如果我們在剛剛的程式 a()
下面再加一行 console.log(myVar)
執行後回發現重新印出 myVar
仍然顯示 1,因為這個 myVar
就是原本全域的變數 myVar
,代表其實他沒有被覆蓋掉。
2-9 範圍鍊 Scope Chain
但是如果函式 b
沒有宣告變數,如下:
function b() {
console.log(myVar) // 1
}
function a() {
var myVar = 2
b()
}
var myVar = 1
a()
會印出 1 而非 undefined。
當函式中使用到沒有定義的變數時,JavaScript 會接著到外部環境尋找(外部參照),注意這裡的外部環境不是指呼叫這個函式的執行環境,而是指定義宣告這個函式的外部環境。函式 b
是在全域環境定義,在函式 a
中呼叫執行,既然是向定義環境找,所以函式 b
的外部環境就是全域環境,而不是函式 a
,故它就找到全域環境的變數 myVar
,最後印出 1。
如剛剛的範例所示,為了找 myVar
而從內層找到最外層(global)的這條鍊,就稱之範圍鍊。
再看一個例子:
function a() {
function b() {
console.log(myVar) // 2
}
var myVar = 2
b()
}
var myVar = 1
a()
我們此時把 函式 b
放置 函式 a
之中,這個時候再來看看 myVar 是多少,來看看這條 scope chain長甚麼樣子。
此時函式 b
的定義寫在函式 a
的執行環境了,所以當呼叫函式 b
裡的 console.log(myVar)
時,因為函式 b
的執行環境沒有 var myVar
,會到函數b的外部環境尋找,而函式 b
的外部環境為函式 a
的執行環境,所以會顯示在函數a裡設定的 2。
2-10 範圍、ES6 與 let
重點字提示:範圍是變數可以被取用的區域
如果你有兩個相同變數,但它會各有一個自己的執行環境,在記憶體中它們其實是兩個不同的變數。
但到最後 我們知道所有談論到的東西
ECMAScript 2015(ES6),引入新的宣告變數方法:
叫作 let
,他可以像 var
一樣使用,但 var
還是存在可以使用。
但 let
讓 JavaScript 引擎 使用一種東西叫作區塊範圍(block scoping)。
你可以宣告一個變數就像用 var
一樣,它會被創造在執行階段
變數仍會被放入記憶體中,設值為 undefined
,然而直到執行階段,那一行程式被執行, 真的宣告變數後,你才能使用 let
,然而直到執行階段,那一行程式被執行,真的宣告變數後,你才能使用 let
。
if (a > b) {
let c = true
}
在這個例子, 如果你試著在 let c = true
之前取用 c
變數,會回傳錯誤訊息。
2-11 關於非同步回呼 Asynchronous Callbacks
之前有提到,JavaScript 是一個單執行緒、同步的程式,它逐行執行程式碼,並不會非同步的執行程式,這樣它究竟要怎麼去監控(monitor)瀏覽器的一些事件呢?
JavaScript 在瀏覽器執行時依賴引擎來驅動,而瀏覽器不是只有JavaScript 引擎的存在,還有其他引擎在處理瀏覽器的其他狀態、程式。有可以呈現東西到瀏覽器畫面上的渲染引擎(Rendering Engine),也有其他專門處理http請求的東西存在。
所以 JavaScript 要如何處理非同步的事件?
非同步的事情會先放入一個叫做事件佇列(Event Queue) 的東西裡面,JavaScript 會先把所有 Execution context 的事情都做完,才去事件佇列裡面繼續處理這些事情。
JavaScript 在瀏覽器的非同步行為,其實是指將非同步事件放到事件佇列,而自己執行同步程式結束後,才處理事件。如果事件觸發函式,就創造執行環境給對應函式,所以說,當瀏覽器的其他引擎或處理器有事件、狀態發生並與 JavaScript 引擎互動時,這些事件就會被註冊進事件佇列裡。
範例程式:
function waitThreeSeconds() {
var ms = 3000 + new Date().getTime()
while (new Date() < ms){}
console.log('finished function')
}
function clickHandler() {
console.log('click event!')
}
document.addEventListener('click', clickHandler);
waitThreeSeconds();
console.log('finished execution')
執行一個等 3 秒的函式,然後有設定一個 click 事件。
執行此程式,並在 'finished function'
出現前點擊頁面,執行結果為:
'finished function'
'finished execution'
'click event!'
點擊事件只會在 waitThreeSeconds()
執行完成,並且執行完全域的 console.log('finished execution')
之後才會發生。
JavaScript 是用同步的方式(一行一行執行程式碼),來處理非同步的事件(如click event)。也就是說執行時間太長的同步程式可以幹擾非同步事件的處理時間。