Day15 | JS 的執行環境、作用域、範圍鏈

❒ 執行的錯誤情境 LHS, RHS

在 JavaScript 遇到錯誤記得修正,不然後方的程式碼都不會運行,這時也可以運用 LHS, RHS 提示來進行排除。

// LHS, RHS。出處:六角學院 ↑

範例 1

1
var ming = '小明';
  • ming 在取值時就會稱為 RHS。
  • 小明賦予到左邊變數 ming 上稱為 LHS。

範例 2 | 延續上題

1
2
var ming = '小明';
var man = ming;
  • 右邊的值 ming 就是使用 RHS 取得這個變數,並且透過 LHS 賦予到左邊的變數。
  • 在等號 = 的右邊或是函式取得變數上,都可以稱為 RHS。

範例 3

1
2
1 = true;
console.log(a);

解析:

  • 當左側不是變數時,就無法被賦予值,會顯示錯誤訊息 Uncaught SyntaxError: Invalid left-hand side in assignment
    • 看到 LHS 錯誤時,可以看一下左邊的值是不是沒有辦法被賦予。

答案:

  • 1 = true; 在編譯時產生錯誤 Uncaught SyntaxError: Invalid left-hand side in assignment → LHS 錯誤。
    • console.log(a); 不會在編譯過程產生錯誤,會在執行階段發現變數無法取得而產生錯誤訊息 Uncaught ReferenceError: a is not defined,表示此變數是沒有被定義過的,在 JS 運行過程中也無法找到此變數 → RHS 錯誤 。

❒ 語法作用域 ( Lexical scope )

// 靜態作用域與動態作用域,出處:六角學院 ↑

  • JavaScript 採用語法作用域,也稱靜態作用域。
  • 語法作用域會牽扯到靜態作用域與動態作用域。

靜態作用域

  • 變數的作用域在語法解析時,就已經確定作用域。
  • JavaScript 是直譯式語言或透過直譯器來生成代碼,並運行代碼。語法作用域也稱靜態作用域 ( 語法解析時就已經確定作用域 ),寫 function 時作用域就已經確定 ( 下方範例 )。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 範例一
function callName() {
var Ming = '小明';
console.log(Ming);
}
callName(); // 可印出小明

// 範例二
function callName() {
var Ming = '小明';
}
callName();
console.log(Ming); //是無法呼叫到小明,is not desined
// 因JS作用域在函式內,在內層宣告變數外層是讀不到的
// 也就是函式內的程式碼只在{}中運行,出了{}就被釋放掉了。

動態作用域

  • 變數作用域在函式調用時才會決定它的作用域。

作用域

// JavaScript 的作用域,出處:六角學院 ↑

  • JavaScript 的作用域是一層一層向內的。外層有一層全域作用域 → 內層由函式所包覆。
  • 上圖中兩個 function 的作用域是獨立的,如果作用域內有需要一些變數,但這作用域內沒有特定的變數時會向外層 ( 全域 ) 尋找,如果外層 ( 全域 ) 有可以使用就會直接使用,否則會顯示 ReferenceError: xxx is not defined

作用域 | 範例 1. ( 靜態作用域 )

1
2
3
4
5
6
7
8
9
var value = 1;
function fn1(){
console.log(value);
}
function fn2(){
var value =2;
fn1();
}
fn2();

執行順序 :

  • 運行順序先宣告 value 等於 1 。
  • 執行 fn2(),再把 value 重新宣告等於 2,再執行 fn1()
  • 這時 fn1()value 值會等於 1。

解說 :

  • 因為 JS 是屬於靜態作用域,所以作用域在撰寫 funciton 時就已經確定。因此 var value = 1; 作用域包含全部,所以無論在 fn1()fn2() 都可以讀取到。
  • 雖然在 fn2() 重新賦予 value = 2 的值,但 value = 2 的值作用域只在 fn2() 內。
    所以再往下執行 fn1() 時, fn1() 內的 console.log(value); 就會向外查找 value 等於 1 的值。

▶️ 所以答案會印出 1。

❒ 執行環境與執行堆疊

函式執行環境

// 函式執行環境, 出處:六角學院 ↑

1
2
3
4
5
// 範例
function callName() {
...
}
callName();
  • 上方介紹到函式作用域是限制在 function 內,所以在 function 宣告任何變數,它的作用域就會限制在 function 內。
  • ‼️ 注意 : 如果我們沒有執行這段函式「 callName(); 」,它是不會有任何變數產生。所以是需要執行它「 呼叫 callName(); 」才會產生執行環境,這個執行環境內才會有屬於它的變數。
    另外它「 callName(); 」是可以重複調用的。
  • 會產生一個 this,後方會介紹到。

全域執行環境與堆疊

// 全域執行環境, 出處:六角學院 ↑

  • 除了函式,全域也有屬於自己的全域執行環境。
  • 全域執行環境是在網頁一開啟或是後端的 Node.js 一開啟時,它的執行環境就已經建立了。它在建立時會同時產生一個 window 變數 ( 使用瀏覽器開啟 ) 或是 global 變數 ( 使用 Node JS 開啟 ) 。
  • 會產生一個 this,等於 window 或 global。
    • 但注意 this 會隨著它的執行環境而有所不同,後方章節會介紹到。
1
2
3
4
5
6
7
8
// 範例
function sayHi(name) {
// ...
}
function doSomething() {
sayHi();
}
doSomething();

運行順序 :

  1. 上方兩個函式,先運行 doSomething()
  2. doSomething() 裡面再運行 sayHi();

解說 :

  • 在執行環境堆疊的狀態,會先看到上方運行順序。
  • 網頁一開啟就會產生全域執行環境
    1. 呼叫 doSomething(),出現 doSomething() 的執行環境,並且堆疊在全域的執行環境上。
    2. doSomething() 內去呼叫 sayHi(); 這個函式,sayHi(); 會堆疊在 doSomething() 這個函式上。
  • 所以這個執行環境是一層一層堆疊起來的,和函式在宣告的時候沒有關聯性,而是與呼叫的位置有關係。
    離開時也是一層一層的離開 sayHi(); 執行完就會先離開 → doSomething() 完成後也會離開 → 最後回到全域的執行環境。

❒ 範圍鍊

解說範圍鍊

  • 當函式的本身沒有這個變數時,它就會向外層來做尋找,這尋找的過程與執行環境是沒有關聯性的。
  • 所以下方範例中,無論是函式 fn1 或函式 fn2 的範圍鍊都是指向外層的全域環境。
    // 函式 fn1 或函式 fn2 的範圍鍊都是指向外層的全域環境, 出處:六角學院 ↑

範圍鍊範例 1

1
2
3
4
5
6
7
8
9
var value = 1;
function fn1(){
console.log(value);
}
function fn2(){
var value =2;
fn1();
}
fn2();
  1. 一開始會產生全域執行環境。
  2. 執行 fn2(); 的執行環境 → 執行 fn2() 內的 fn1(); ,雖然在 fn2() 重新賦予 value = 2 的值,但 value = 2 的值作用域只在 fn2() 內。
    所以再往下執行 fn1() 時, fn1() 內的 console.log(value); 就會向外查找 value 等於 1 的值。
  3. ‼️ 注意 : 函式 fn1 在往外層尋找 value 時並不會跟執行環境有任何的關聯性。因為 JS 是語法作用域,它在程式碼撰寫時就已經確定它的作用域。
    1. 函式 fn1 沒有 value 這個變數時會向外層全域來做尋找,這尋找過程與執行環境 ( 執行環境也就是 function fn1(){} 內 ) 是沒有關聯性的。

範圍鍊範例 02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = '老媽';
function sayHi() {
console.log(`hi ${person}`);
}
function doMorningWork() {
var person = '老爸';
function meetAuntie() {
var person = '漂亮阿姨';
console.log(`嗨囉~ ${person}`);
}
sayHi();
meetAuntie();
}
sayHi();
doMorningWork();
  • 答案

    hi 老媽、hi 老媽、哈囉~漂亮阿姨

    • doMorningWork() 內的 sayHi(); 會忽視裏面的變數 person 老爸,向外層尋找變數 value 老媽。因為變數 person 老爸作用域只在 doMorningWork() 內。
      另外函式 sayHi 向外尋找變數 person 老媽是因為它裡面本身沒有此變數。
    • doMorningWork() 內宣告一個 meetAuntie() ,所以現在執行的不是外層的 sayHi 而是內層的 meetAuntie(),而內層的 meetAuntie() 本身就有一個 person 變數漂亮阿姨,所以會印出 哈囉~漂亮阿姨
      • 如果註解掉 var person = '漂亮阿姨'; 就會向外尋找變數印出 哈囉~老爸
    • 結論 : 當函式的本身沒有這個變數時,它就會向外層來做尋找,這尋找的過程與執行環境是沒有關聯性的。

參考資訊

  • 六角學院 - JavaScript 核心篇