這篇文章將會介紹 Execution Context,然後也會在了解的過程中一併認識 Hoisting、Scope Chain。
Execution Context 是一種抽象的概念,在 JS 程式碼被編譯解析後建立,用來執行程式碼的環境,Execution Context 分成兩種:
JavaScript 預設的 Execution Context,只會有一個,裡面會建立全域物件(window)。
當函式呼叫時,會形成函式自己的 Execution Context。
而在一個 Execution Context 裡面,還有 Lexical Environment 和 Variable Environment,下面一起來了解它們吧!
首先我們先寫一段程式碼:
console.log(dogName);
console.log(showAge);
console.log(showWeight);
var dogName = 'Lucky';
var showAge = function() {
return '5 years old';
}
function showWeight() {
return '20 kg';
}
function introMyDog(name, age, weight) {
var introduction = `My dog's name is ${dogName}. It is ${age} and weight ${weight}.`
return introduction;
}
var message = introMyDog(dogName, showAge(), showWeight());
console.log(message);
接著我們一步步分析這段程式碼的執行過程。
推薦讀者使用雙螢幕閱讀,一邊放程式碼一邊放說明的內容就不用上下頻繁滾動,或是把程式碼貼在瀏覽器 devTools 並配著看也可以!
編譯階段完成時,產生全域的 Execution Context,並且放在 Call Stack 裡面。
此時程式裡面宣告的變數和函式,也在 Memory Heap 裡面分配了記憶體位置,不過這時變數尚未被賦值,在記憶體為 undefined,像 dogName、showAge 等變數/函數表達式就印出 undefined,但整個函式會被存放到記憶體內,所以 showWeight、introMyDog 能印出完整函式。
從程式碼的角度看,它們就好像移動到作用域的頂端一樣,這個就是 hoisting。
進入執行階段時,所有變數被賦值。
接著就依序執行程式碼,到達var message = introMyDog(dogName, showAge(), showWeight());
這行程式碼時,showAge 函式和 showWeight 會先呼叫,它們回傳的結果再當成參數傳入 introMyDog。
這個階段兩個函式內部沒啥程式碼,簡單帶過。
先執行 showAge 函式,執行完後其 Execution Context 移出 Stack。
showWeight 執行完後 Execution Context 也移出 Stack。
首先會建立 introMyDog 的 Execution Context,並且和全域 Execution Context 時一樣,分為編譯函式內的程式碼和執行兩個階段。
函式內部一樣也有 hoisting
最後就可以將 introMyDog 函式回傳的值賦予給 message 變數了,然後一樣將 introMyDog 的 Execution Context 移出 Stack。
接著延續前面提到的程式碼範例,我們來看作用域 & 作用域鏈。
作用域指的是我們宣告的變數、常數、函式可以被存取得到的範圍,分為三種:
例如在 introMyDog 的函式作用域中可以取用到 introduction 變數,而如果在 introMyDog 函式內要取到 dogName 這個全域變數,就需要往外層的全域作用域查找,一層層往外層的作用域找直到找到為止,這種特性就叫作用域鏈。
補充: JS 屬於靜態作用域,也就是在函式定義時決定其作用域。與之相對的是動態作用域,也就是在函式執行時才決定其作用域
以下範例重複宣告了三次 height,讀者可以思考看看最後 showHeight 函式印出的值是什麼?
var height = 170;
function showHeight() {
console.log(height); // ?
}
function setHeight160() {
var height = 160;
showHeight();
}
function setHeight180() {
var height = 180;
setHeight160();
}
setHeight180();
讀完程式碼後,這段程式執行到呼叫 showHeight 函式時的樣子會如下圖:
在 showHeight 函式內找不到 height 變數,所以往外層作用域查找,最後印出全域 execution context 的 170。
為什麼 showHeight 找不到變數不是往 setHeight160 的 execution context 查找? 因為 showHeight 的外層作用域就是全域!
我們來看下面範例來做比較。
接著我們把範例程式碼調整成這樣:
var height = 170;
function setHeight180() {
var height = 180;
function setHeight160() {
var height = 160;
function showHeight() {
console.log(height);
}
showHeight();
}
setHeight160();
}
setHeight180();
這樣的話就會印出 160,showHeight 函式找不到 height 變數,就會向外層作用域找,所以找到 setHeight160 函式的變數。
紅色箭頭代表程式碼找到 setHeight160 函式就停止,若一直找不到就會往外繼續查。
這篇介紹了 Execution Context、Hoisting、Scope 等概念,下一篇我會繼續延伸,加入 let/const、Block Scope(區塊作用域) 等概念和本篇文章的概念做整合。