[課程筆記]克服JS的奇怪部分-CH1 執行環境與詞彙環境
本篇筆記目錄:
觀念小叮嚀:名稱/值配對與物件
U1 全域環境、全域物件
U2 執行環境:創造與提升
觀念小叮嚀:JS 與 undefined
觀念小叮嚀:單執行序、同步執行
U3 函數呼叫與執行堆
U4 函數、環境與變數環境
U5 範圍鏈
U6 範圍、ES6 與 let
U7 關於非同步回呼
觀念小叮嚀:語法解析器、執行環境與詞彙環境
- Syntax parsers 語法解析器:
當你的語法是有效時,能夠讀取程式碼,並決定做何動作的一個程式。
其他人也寫了一個程式來轉換你的 JS 程式碼成為電腦懂的東西。
- Lexial enviorments 詞彙環境= execution context 執行派絡
程式碼在程式中的實際所在位置。
- Execution contexts 執行環境
一個包裹,幫助管理(驗證、執行)正在執行的程式。
我們有許多的詞彙環境,但真正在執行的會被執行環境所管理,執行環境包含的不只是你所寫的程式碼,下面會有完整的介紹。
觀念小叮嚀:名稱/值配對與物件
- Name/Value Pair 名稱/值配對
代表一個名稱會對應到唯一的值
名稱可能會被定義很多次,但如果在一種執行環境下,它只能有一個值。
也就是說一段正在執行的程式碼,同樣名稱只會有一個,而一個名稱只能被一個值定義。
不過,這個值可以是另一個名稱/值的配對—>「物件」
- 物件
名稱/值配對的組合
其他的程式語言物件可能有更多複雜的概念,但在 JS 中,物件就是這樣而已。
U1 全域環境、全域物件
不論何時執行JS程式,你的程式碼都被包在執行環境裡。
Global context 全域環境 = Basic context 基本執行環境
全域表示不再函數中,我們可以在任何地方取用它,只要執行 JS 檔,執行環境就會被創造出來,全域執行環境 JS 會幫我們創造幾件事情:
(1) Global Object 全域物件
瀏覽器裡的全域物件是 window,每個視窗都有自己的執行環境和自己的全域物件。
舉例:當我們執行以下程式
var a='Hello world!'
function b(){
}
我們可以在 window 這個全域物件中找到 a 和 b,
也就是說我們輸入 a
或 window.a
可以得到一樣的結果,
(2) this
執行環境可以決定 this 裡面是什麼。在用瀏覽器的情況下,this 就等於 window
(3) Outer Enviroment 外部環境
當你在全域等級執行,你沒有外部環境(已經在最外面了),所以這時外部環境為 null 。
(4) 你的程式
最後,執行環境在執行你的程式碼了!

U2 執行環境:創造與提升
在大部分的程式語言中,下面的語法會出現錯誤,因為 a 還沒有被定義,
b();
console.log(a)
var a='Hello World';
function b(){
console.log('Called B!');
}
但在 JS 中,實際上會出現:
Called b!
undefined
錯誤解釋:JS 的變數和函數被移動到最前面
正確解釋:因為執行環境的建立會被分成兩階段:
- Creation 創造階段:建立執行環境(全域物件、this、外部物件)
- Hoisting 提升階段:在逐行執行程式碼前,設定變數和函數在記憶體裡
提升階段時,JS 先為b()
空出記憶體空間,所以當我們可以呼叫b()
,然而,a
變數的情況不太一樣,雖然 JS 為a
空出記憶體空間時,它不知道的會是什麼值(undefined),直到被執行。雖然技術上來說可以,但依賴提升不是個好方法!
觀念小叮嚀:JS 與 undefined
Undefined 和 NotDefined 是不同的東西
var a;
console.log(a)
//undefined
console.log(b)
//notdefined
-undefined :當執行環境被創造時,JS 會設定所有變數都是 undefinded
-notdefined :變數從未被宣告,記憶體裡沒有這個東西
永遠不要將變數設值為 undefined,很難 debug
觀念小叮嚀:單執行序、同步執行
Javascipt 一次只會發生一件事
- Singled threaded 單執行序
一次執行一個指令
在程式碼裡會有許多指令,而單執行序表示我們一次只能執行一個指令,當我們在使用瀏覽器時,要知道 JS 不是瀏覽器唯一在執行的東西,因此當我們說 JS 是單執行序的時候,不是指瀏覽器,而是從我們的角度看,JS 一次只做一件事。
- Synchronous execution 同步執行
一次一個
程式碼會依照出現的順序,一次執行一行。
U3 函數呼叫與執行堆
Invocation 函數呼叫
函數呼叫:執行或呼叫函數
以下面的程式為例:
function b(){
}
function a(){
b();
}
a()
執行過程:
- 語法解析器逐行分析程式內容,編譯器編譯程式
- Creation 創造階段:建立全域執行環境(創造全域物件、this、window),並放進執行堆中
- Hoisting 提升階段:將函式、變數放進記憶體中
- 程式逐行執行
- 遇到
a()
時:呼叫a()
- Creation 創造階段:新的執行環境被創造,並放進執行堆中
- 逐行執行
- 遇到
b()
時:呼叫b()
- 停止執行
a()
,執行b()
- Creation 創造階段:新的執行環境被創造,並放進執行堆中
- 停止執行
- 當
b()
執行完畢後:-
b()
離開執行堆 - 因為
a()
沒有要執行的東西,所以a()
也離開執行堆
-
Execution stack 執行堆:一個一個堆起來,誰在最上面就是正在執行的東西
另一個範例
function a(){
b();
var c;
}
function (){
var d;
}
a();
var d;
執行過程:
- 語法解析器逐行分析程式內容,編譯器編譯程式
- 創造新的執行環境
- Creation 創造階段:建立全域執行環境(創造全域物件、this、window),並放進執行堆中
- Hoisting 提升階段:將函式、變數放進記憶體中
- 程式逐行執行
- 遇到
a()
時:呼叫a()
- 創造新的執行環境、放進執行堆
- 逐行執行
- 遇到
b()
時:呼叫b()
- 停止執行
a()
,執行b()
- 新的執行環境被創造,並放進執行堆中
- 逐行執行:var d
- 停止執行
-
b()
執行完畢後:- b() 離開執行堆,繼續執行
a()
- 逐行執行:var c
- b() 離開執行堆,繼續執行
-
a()
執行完畢後:-
a()
離開執行堆 - 逐行執行:var d
-
U4 函數、環境與變數環境
Variable Environment 變數環境
描述創造變數的位置還有他在記憶體中和其他變數的關係
function b(){
var myVar;
}
function a(){
var myVar=2;
b();
}
var myVar=1;
a();
myVar 的值會是多少?先了解執行過程:
執行過程:
- 創造:建立全域執行環境,並放進執行堆中,提升:
myVar
、b()
、a()
被放進記憶體中 - 逐行執行:遇到 var myVar=1 ,記憶體中的變數得到 1 的值
- 逐行執行:遇到
a()
:- 創造新的執行環境
- Creation 創造階段:建立全域執行環境,並放進執行堆中
- Hoisting 提升階段:遇到 var myVar=2,將myVar放進記憶體中,得到 2 的值
- 逐行執行:var myVar=2
- 創造新的執行環境
- 遇到
b()
時:呼叫b()
- 停止執行
a()
,執行b()
- 創造新的執行環境
- Creation 創造階段:建立全域執行環境,並放進執行堆中
- Hoisting 提升階段:遇到 var myVar,將
myVar
放進記憶體中,得到 undefined 的值
- 停止執行
- b() 執行完畢後, b() 離開執行堆,繼續執行 a()
- a() 沒有要執行的, a() 離開執行堆
Scope:表示我們可以在哪裡看到變數
這裡的每個變數都被定義在自己的執行環境,彼此沒有關聯!
U5 範圍鏈
Reference to the outer enviroment 外部環境的參照
當我們需要使用變數時,JS 不只會在目前的執行環境的變數環境中尋找,當你需要某個執行環境內的程式碼的變數,而他無法找到變數,它就會到外部環境尋找變數,每個執行環境都有一個參照到它的外部環境參照。
function b(){
console.log(myVar);
}
function a(){
var myVar=2;
b();
}
var myVar=1;
a();
//1
為什麼答案會是1呢?因為外部環境的參照!
以上面的程式碼為例,b()的外部環境是全域執行環境,a()也是。
執行環境取得的變數環境,不必是正下方的執行環境,反之,它會是某個我們已討論的東西-「詞彙環境」
「Lexial enviorments 詞彙環境」
JS 很特別它很注重詞彙環境,詞彙環境是程式碼實際所在的位置。b()的位置不在 a()裡,而是在全域環境,所以我們在 b()找不到 mayVar 時,他會往它的外部環境:全域環境做尋找。
往下搜索的鏈子,就是我們所說的「範圍鏈」,「範圍」代表我能夠取用這個變數的地方,「鏈」是外部環境參照的連結。
接著我們嘗試改變 b()的位置:
function a(){
var myVar=2;
function b(){
console.log(myVar);
}
}
var myVar=1;
a();
//2
從上面的程式碼來看,我們可以知道b()
被我們改建立在 a()
裡面,而不是全域環境,所以 b()
的外部環境就是 a()
。
當我們改變b()的
詞彙環境,表示我改變了他的物理位置,他現在在 a()
裡面
所以 JS 會認定他的外部參照是 a!函數的位置決定了它的外部環境,我們也可以想成因為 b() 在 a() 的執行環境創造階段被創造了,所以 a() 就是它的外部環境
。
當然,依照詞彙環境來看程式碼實際所在位置是最快的!
範圍、ES6 與 let
- Scope 範圍
範圍是變數可以取用的區域
如果你有兩個相同變數,他在記憶體中會是兩個不一樣的變數
- Let
ES6 中引入新的宣告變數方法:let,let 讓 JS 引擎使用「區塊範圍 Block scoping」
兩個重點:
(1) 宣告變數後我們才能使用 let
if(pa>b){
console.log(c)
let c= true;
//error
}
c會被創造在執行階段,變數仍會被放入記憶體中,設值為undefined,然而,直到執行階段那一行程式被執行,真的宣告變數時,我們才能使用 let,他仍然在記憶體中,但引擎不讓我們使用!
(2) 當變數被宣告在區塊裡面,它只能在裡面被取用
當你有一個迴圈正在執行相同的程式,但你有個 let,每次進行迴圈時,你的變數在記憶體中都是不同的
U6 關於非同步回呼
Asynchrounous 非同步
一個時間點不止執行一個程式
Javascript引擎本身,不是獨自存在的,例如:瀏覽器,它還有其它引擎,像是呈現引擎(rendering engine)、處理瀏覽器的HTTP請求,它們會與 JS 引擎互動,但這些可能都是非同步執行,只有 JS 引擎是同步的。
JS 引擎內的等待列稱為事件佇列(event queue),這裡面都是事件(如:click 、HTTP Request)
當執行堆是空的 JS 才會注意到事件佇列,如果事件佇列有東西,它才會看是否有函數會被這個事件處發,如果事件觸發函數,會出現在執行堆(依據事件發生的順序處理)。
所以這不是真正的非同步,JS 用同步的方式處理非同步事件,把瀏覽器非同步東西放到事件佇列,但原本的程式仍然繼續一行行執行。
我們可以來看範例:
//long running function
function waitThreeSeconds(){
var ms = 3000+new Date().getTime();
while(new Date()<ms){}
console.log('finished function');
}
function clickHandler(){
console.log('click event')
}
//listen for the click event
document.addEventListener('click',clickHandler);
waitThreeSeconds();
console.log('finished execution');
當我們在 3 秒內點擊畫面,會出現:
finished function
finished execution
click event
為什麼呢?
因為 JS 引擎直到執行堆是空的才會看事件佇列,這表示長時間函數可以干擾事件,但這就是 JS 如何同步處理在瀏覽器別處非同步的事件發生。當全部完成後,它會不斷繼續看事件佇列的迴圈,這稱為持續檢查(continous check),