---
# System prepended metadata

title: 閉包

---

# 閉包


**詞法作用域(lexical scope)=靜態作用域**
JavaScript 的作用域範圍由程式碼所定，所以程式碼在撰寫完成的同時，就已經先確立了作用域，運行的過程中都不會改變其作用域

```
function fn1() {
  console.log(a);
}
function fn2() {
  var a = 1;
  fn1();
}
fn2();
```
兩個函式的作用域完全獨立(a is not defined)
![](https://i.imgur.com/RbHBA5H.png)

變數的作用域是獨立的，但函式內的函式則可以取用外層作用域的變數=>**作用域鏈（scope chain）**:子物件會一級一級地向上尋找所有父物件的變數

```
function sayHi() {
  var name = '小明';
  
  function addString() {         // 內部函式、閉包
    console.log(`${name} 你好`);  // 取用外層的變數
  }
  addString();
}
sayHi();
```
![](https://i.imgur.com/QxinGUh.png)


在巢狀函式中，如果內層的函式沒有可以取用的特定變數則會向外查找，此時內部的函式就可以稱為閉包。閉包是內部的函式可以取用外部作用域變數/參數的概念

addString可以讀取sayHi中的局部變數=>把addString作為返回值=>外部讀取內部變數

閉包發生的時機是在函式建立的時候，每當新的函式被建立出來，**它會紀錄它所在的位置的*執行環境*，並記錄外層的*作用域鏈*。**

**執行環境（Execution Context，EC）**
js底層在程式準備執行時，針對「全域」及「函式」所建立的物件

1. 全域執行環境（Global Execution Context）:
創造階段（creation phase）------------------------->執行階段（execution phase）
* 建立全域物件（Global Object）--------------------一行一行（line by line）執行code
* 建立 this，並將它指向window
* 建立scope chain，並設為 null（因為他自己就是最頂層）
* hoisting


2. 函式執行環境（Function Execution Context）
創造階段（creation phase）------------------------->執行階段（execution phase）
* 建立執行物件（Activation Object）----------------一行一行（line by line）執行code
* 建立 this，並把它指向呼叫此函式的 caller
* 建立Scope chain並把它指向此函式的外層
* hoisting

```
var a = 0
function b() {
  var a = 10
  function c() {
    console.log(a)
  }
  return c
}
var func = b()  //變數 func =return的函式c
func() // 10
```
![](https://i.imgur.com/A18OFeV.png)

js底層運作流程:
1. 建立 Global EC:預留了變數 a、func 的空間，及函式 b 的作用域鏈
2. 執行 Global EC:變數 a 賦值 a = 0，呼叫函式 b()
3. 準備函式 b:建立 function b() EC，預留了區域變數 a 的空間，及函式 c 的作用域鏈
4. 執行 b():區域變數 a 賦值 a = 10，回傳函式 c
5. 函式 b 執行結束:消除 function b() EC
=>function b() EC 內的 a 被回傳的函數 c 閉包了
6. 變數 func 賦值成函式 b() 的執行結果 - return的函式 c
7. 呼叫 func():準備函式 c，建立 function c() EC，並依照作用域鏈找到 function b() EC 中的區域變數 a
8. 執行 func():執行 console.log(a)；印出 a=10
9. func() 執行結束，消除 function c()的EC，程式執行結束

由於閉包，在 b() 執行結束時，其中的區域變數 a 並未跟著 function b() EC 一起消失，而是留給了 function c() EC 的參照使用，變數a因為還會持續維持參考，所以不會被釋放記憶體，因此成為了 func() 函式的**私有變數**（僅存與此函式中，無法透過其它方式調整）


**私有變數**
任何在函式中定義的變數，不能在函式外部訪問的變數，都可以認為是私有變數。包括函式的參數，局部變數和在函式內部定義的其他函式


私有變數=>優點:隱藏那些不應該被直接修改的資料

EX:一個簡易的後台系統
```
var commodities = [
  { name: "iphone", stock: 10 },
  { name: "htc", stock: 5 },
];

function add(name, number) {
  commodities.push({ name: name, stock: number });
}

function changeStock(id, amount) {
  commodities[id].stock += amount;
}

add("乖乖", 20);
changeStock(0, -5);
commodities[0].stock += 10;
```
問題?
全部商品的資料暴露在外，有心人士能夠直接操作裡面的資料。

解決?=>引入閉包
```
function createstore() {
  var commodities = [
    { name: "iphone", stock: 10 },
    { name: "htc", stock: 5 },
  ];
  return {
    all: function () {
      return commodities;
    },
    add: function (name, number) {
      commodities.push({ name: name, stock: number });
    },
    changeStock: function (id, amount) {
      commodities[id].stock += amount;
    },
  };
}

var myStore = createstore();

myStore.add("乖乖", 20);
console.log(myStore.all());

myStore.changeStock(0, -5);
console.log(myStore.all());

commodities[0].stock += 10; //reference error
```
上面的函式回傳一個物件，裡面包含的三個函式。因為閉包的關係，所以commodities這個陣列會被鎖在函式中，而且持續可被也僅可被後面三個函式(**特權方法**)所存取，但外界是看不到這個變數的，因為已經是作用域以外的世界。

**特權方法**︰有權訪問私有變數的公有方法

疑點?
外加個函式屬性給myStore，是不是也能存取到commodities？
```
'use strict'

...

myStore.removeAll = function () {
  commodities = [];
};
myStore.removeAll(); //Uncaught ReferenceError
```
就算外加屬性給已經包裝好的物件，也沒辦法存取到原先的閉包，因為閉包一旦創建完成，就具有不可侵犯性。

**結論:**

Q1解釋何謂閉包?

1. 所有函式在建立時都會產生閉包。
2. 閉包不是只會產生在巢狀(內部)函式的回傳時，巢狀(內部)函式是一種最常利用閉包結構的樣式，因為它可以重覆使用閉包中的記憶環境。
3. 閉包所記憶的環境，其原理是來自作用域連鎖的設計，內部函式可以看(獲取)到外部函式的變數值與傳入參數值。
4. 閉包中的變數是私有的，只有閉包函式才有許可權訪問它。不會被外面的變數和方法給汙染。

Q2為什麼要使用閉包?

如果你希望「別人要依據你規範好的方法操作狀態」:
透過應用閉包私有變數的特性，外部僅能通過指定的特權方法訪問該變數，防止變數汙染，確保資料安全性與穩定性。

**題目:**
```
var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name;
    };
  },
};
alert(object.getNameFunc()());
```
```
var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};
alert(object.getNameFunc()());
```

-----
參考資料:
[JS 核心觀念筆記 - 閉包基本認識、工廠模式與私有方法](https://hsuchihting.github.io/javascript/20200812/3642687092/)
[閉包，原來這就是閉包啊！](https://ithelp.ithome.com.tw/articles/10244348)
[閉包包什麼？探索JS中的作用域與Closure](https://medium.com/%E7%8B%97%E5%A5%B4%E5%B7%A5%E7%A8%8B%E5%B8%AB/%E9%96%89%E5%8C%85%E5%8C%85%E4%BB%80%E9%BA%BC-%E6%8E%A2%E7%B4%A2js%E4%B8%AD%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E8%88%87closure-javascript%E9%8D%9B%E9%8D%8A%E6%97%A5%E8%A8%98-f7b1a2ac1e2a)
[題目的解釋連結!](https://www.imooc.com/wenda/detail/352112)
