---
# System prepended metadata

title: Scope
tags: [作用域, JavaScript]

---

Scope
===
規範變數在程式碼中的取用規則，Scope 主要分成兩種類型：

- Lexical Scope ( 語彙範疇 )
- Dynamic Scope ( 動態範疇 )

JavaScript 與大多數語言採用 Lexiacal Scope。

Lexical Scope （語彙範疇）
---
代表區塊間的包裹關係，被包裹在內層的區塊可以保護自己的變數不被外層取用，相反的，外層區塊的變數仍能被內層區塊使用。

```jsx
let outer = '此處是外層';
function innerFun() {
  var inner = '此處是內層';
  console.log(outer);    // "此處是外層"
  console.log(inner);    // "此處是內層"
}
console.log(outer);    // "此處是外層"
console.log(inner);    // Uncaught ReferenceError: inner is not defined
```

- 函式參數屬於內層 Scope，只有在函式內能夠使用
![](https://i.imgur.com/PIXZ7Gu.png)
- 巢狀 Scope，規則相同：外層 Scope 無法取用內層變數，但內層 Scope 可以取用外層變數
![](https://i.imgur.com/BCn2afh.png)

全域作用域 Global Level Scope
---
每個執行 JavaScript 的環境，都有一個全域物件（Global Object）：
- 在 HTML 裡，全域物件是 `window` object。
- 在 Node.js 裡，全域物件是 `global` object。

存放在全域物件裡的變數，無論在哪裡宣告，效力遍及整個程式。因此不應該隨便產生不需要的全域變數，造成額外的程式風險，以及變數控管上的困擾。

使用 var 才會是 Global Level Scope，若使用 let、const 不全然是 Global Level Scope。詳細見[此](https://hackmd.io/9jqUIElFRcavjEriaCBDCg?view#%E4%B8%8D%E6%9C%83%E7%94%A2%E7%94%9F-Global-Scope-%E8%AE%8A%E6%95%B8)。


### 賦值給未宣告的變數
賦值給未宣告的變數，自動產生全域變數，一般應該避免這種寫法。

```jsx
function myFunc(){
    n1 = "OneJar";  // 賦值給未宣告的變數，自動產生全域變數
    console.log(n1); // OneJar
    console.log(this.n1); // OneJar
    console.log(window.n1); // OneJar
}

myFunc();
console.log(n1); // OneJar
```


函式作用域 Function Level Scope
---
> 同時也是 Block Level Scope，只是為了方便區分，才特此分出來。

只有在限定範圍內有效果，形成 Scope 的基本單位：
- Function

JavaScipt 不是為每個 `{}` 產生 Scope，因此 `if() {}`、`for() {}` 內層的變數仍能被外層取用。

區塊作用域 Block Level Scope
---
只有在限定範圍內有效果，形成 Scope 的基本單位：
- try/catch 中的 `catch`、`with`
- ES6：`let`、`const`

### ES6：`let`、`const`

在 ES5 以前，用 var 關鍵字宣告的變數，只會有 Global Level 和 Function Level 兩種等級的作用域，因此`{}` 不會產生 Scope，只有 Function 才會。

ES6 導入新的變數宣告關鍵字：let 和 const，增加了 Block Scope 的用途。使用 let、const 進行變數宣告，將會在 `{}` 產生 Scope。

```jsx
{
   let x = 2;
   {
        let x = 10;
        console.log(x); // 10
   }
   console.log(x); // 2
}
```
> [詳細的變數運用](https://hackmd.io/9jqUIElFRcavjEriaCBDCg?view)

Scope Lookup（查找）
---
由當下的 Scope 往外層 Scope 查找，一旦找到就停止。若是一直沒有找到變數，JavaScript 就會由當下的 Scope 一路查找到最外層的 Global Scope。

此規則下，可能衍生的兩種問題：

- ### 直到 Global Scope 都沒有找到該變數

    再細分情境，查找這個變數，是為了要「取值」or「賦值」。
    
    - 在取值情境，會發出 Reference Error：`Uncaught ReferenceError: XXX is not defined`。
    
    - 在賦值情境，會新建變數 or 發出錯誤：
    
        - 通常情況，JavaScript 會一路查找到 Global Scope，發現連 Global Scope 都沒有時，便會建立新的 Global 屬性。
        
        - 然而在 `'use strict'`（[JavaScript 的嚴格模式](https://hackmd.io/FZI5Jq1SRBWwJGWMqN-cHA?view)）時，在賦值時便會報錯。在 `'use strict'`，JavaScript 不接受對不存在變數賦值。

            ```jsx
            'use strict';
            function assignToNonExist() {
              notDeclaredVar = 'I am is not Declared';　
            // Uncaught ReferenceError: notDeclaredVar is not defined
            }
            assignToNonExist();
            ```
- ### Shadowing （遮蔽）

    如果同時有兩個同名變數宣告在不同層的 Scope，內層 Scope 的變數會 Shadow（遮蔽）外層的同名變數。
    
    ![](https://i.imgur.com/aAEYMm8.png)


    由內層 Scope 往外層 Scope 找，馬上找到自己的 Scope 內有一個 myString，於是停止查找，而外層的 myString 理所當然被 Shadow（遮蔽）。
    
    ```jsx
    var myString = "hello global";

    function testShadowing(){
      var myString = "hello scope";
      console.log(myString); // "hello global"
      console.log(window.myString); // "hello global"
    }
    ```
    
    然而，如果在內層透過如 `window.n1` 的方式，明確表明「我要取的是 Global 變數的 n1，而不是 Local 的 n1」，就會是 Global 變數發揮效用。
    
實作建議
---

- 雖然有 Shadowing 這個特性，仍盡量不要重複宣告同名變數。
    - 宣告同名變數，會造成語意不清，難以維護。
- 屬性存取是藉由屬性存取規則（Prototype）去查找的，並不是 Scope 的工作。
- 可以使用 [eval](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) 脫離 Lexical Scope，但是盡量別用。
    - eval 可以輸入字串，編譯器會在執行時期將它轉成程式碼，也就是脫離 Lexical Scope 在編譯時期就定好 Scope 的概念。
    - 使用 eval 程式碼難以 Debug，也會造成效能低落，因為編譯器無法預先知道執行的程式是什麼，無法最佳化 eval 述句與相關程式碼的效能。