iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0

閉包無所不在,在你還沒知覺到的時候,你已經寫了一個閉包,像是這樣:

const outVar = "out variable";
const displayVar = function () {
  console.log(outVar);
};
displayVar();

所以閉包在哪裡?閉包到底是什麼?
也許有人會說,騙人這哪有閉包,這就scope chain啊,沒錯!這是scope chain,但!也是閉包!(忍者說的,請參閱v2 p104)。
因為outVar和displayVar都在全域宣告,所以只要程式還在執行,scope就不會消失,看起來好像不是閉包,所以這也是為什麼要把閉包和他的小夥伴scope chain一起寫的原因。

先從scope chain開始

  • 外星語版本
    每次呼叫一個函式便會產生一個新的字彙環境(lexical environment),程式執行時,可存取外部環境的變數(outer environment),這就是scope chain
  • 翻譯蒟篛版本
    scope chain簡單說就是一種javascript查找變數的機制,當一段code執行時,若找不到執行所需的變數,會往外層查找,一直尋找到全域(global scope),若還是沒有才會報錯。需注意的是,哪些變數可以被找到,與變數宣告的方式有關。

像是這樣:
呼叫outer()時,會先執行黃色框內的code,其中定義了變數outerVar和函式innerFn(),並呼叫innerFn(),此時會執行紅色框,發現紅框內找不到outerVar,往外層有找到,可印出outerVar字串

那閉包又是什麼?

  • 外星語版本
    當定義函數時,無論執行背景空間(execution context)存在與否,都可讀取到的鄰近變數環境(variable environment)
  • 翻譯蒟篛版本
    當函式建立時,一個像是包住函式定義&函式scope chain的泡泡,它可以確保當函式執行時,無論當初的scope是否存在,都可以依循scope chain找到所需的變數。

閉包的例子

  • 一開始用callback來理解看看,以下是一個setTimeOut的例子:
    delayText已經執行完之後,setTimeOut的callback過兩秒才執行,但還是可以讀到text變數
const delayText = function () {
  const text = "this is a delay message";
  setTimeout(() => console.log(text), 2000);
};

delayText();
  • 來一個function內return function的例子:
function countDogs() {
  let dogNum = 0;
  return function () {
    dogNum++;
    console.log(`${dogNum} dog(s)`);
  };
}

const myFunc = countDogs();
myFunc(); //1 dog(s)
myFunc(); //2 dog(s)
myFunc(); //3 dog(s)
  1. 首先,在全域(global scope)我們有一個函式(countDogs)
  2. 接著宣告了變數myFunc並呼叫countDog()儲存進去,有一個新的execution context產生了,裡面宣告了一個變數(dogNum)並定義了一個匿名函式
  3. 因回傳這個匿名函式,所以這個execution context結束工作,從call stack消失了
  4. 接著我們呼叫了myFunc函式,這時又一個新的execution context產生,但裡面沒有任何變數,若依scope chain只能找到全域的countDog和myFunc,但!!實際上卻可以因為閉包,從當初建立匿名函式的位置,依當初的scope chain找到dogNum變數。登愣~
  • 把一開始的scope chain例子改寫一下:
const outerFn = function () {
  const outerVar = "I am from outer function";

  const innerFn = function () {
    console.log(outerVar);
  };

  return innerFn;
};

const myClosure = outerFn();
myClosure(); //  I am from outer function

事情是這樣發生的:
我們把outerFn裡本來直接執行的innerFn改成傳出來,並存在另一個變數myClosure,當執行myClosure時,當初宣告innerFn的outerFn已經執行完,它的execution context早就消失了,但因為閉包的特性,innerFn還是可以從當初宣告的位置,依循scope chain找到需要的outerVar印出來

  • 最後來個可能在網路上都寫到爛經典的小考題二連發,請問執行結果是什麼,請作答:
function pushFunc() {
  var arr = [];
  for (var i = 0; i < 3; i++) {
    arr.push(function () {
      console.log(i);
    });
  }
  return arr;
}

var resultArr = pushFunc(); 

resultArr[0]();
resultArr[1]();
resultArr[2]();
function pushFunc() {
  var arr = [];
  for (let i = 0; i < 3; i++) {
    arr.push(function () {
      console.log(i);
    });
  }
  return arr;
}

var resultArr = pushFunc(); 

resultArr[0]();
resultArr[1]();
resultArr[2]();

第一題答案是3 3 3,第二題答案是0 1 2。
主要差別在於for迴圈裡,變數i的宣告方式,第一題是用var;第二題是用let。

  1. 全域裡有宣告函式pushFunc和變數resultArr
  2. 將pushFunc執行並儲存於變數resultArr時,一個execution context產生,裡面有變數arr(是一個空array)和一個匿名函式(內容是印出i),最後return一個arr陣列後,從call stack消失。此時resultArr是一個內含三個匿名函式的陣列,像這樣:[fn0, fn1, fn2]
    console.log(functionArr[0]); 
    
    //ƒ () {
    //      console.log(i);
    //    }
    
  3. 接著我們在全域分別呼叫resultArr的element(也就是fn0之類的),並且執行,會執行的就是那個匿名函式。
  4. 執行結果分兩個部分來說。
    一個是把for當中的i用var宣告的第一題,因為var是function scope,故此時i的scope是所以每個匿名函式透過閉包找i值時,都會找到的是for迴圈跑完的時候,i=3的狀態。
    而第二題的i用let宣告,是屬於block scope,此時i的scope為每跑一圈就會抓到一個i。

今日結論

其實推幾次之後,好像會建立一種類直覺的感受,就可以比較快判斷出來了!
tl;dr 閉包是一個包住函式定義與其scope chain的泡泡,它可以確保當函式執行時,無論當初的scope是否存在,都可以依循scope chain找到所需的變數。

ref
JS 原力覺醒 Day08 - Closures
所有的函式都是閉包:談 JS 中的作用域與 Closure


上一篇
js的模組化:Common js & ES module
下一篇
currying柯里化
系列文
超低腦容量學習法遇到javascript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言