iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0

🔔 閉包就是內層函式可以取得外層函式作用域的變數
Closure Flow

閉包的定義


MDN文件:閉包

閉包是什麼?
閉包是 JavaScript 中的一種特性,它允許函式訪問其外部作用域中的變數,即使函式在外部作用域之外被執行。閉包是由函式及其相關的作用域鏈組成的。

以下範例,createCounter() 函式返回了一個內部函式,內部函式能夠訪問 createCounter() 中的 count 變數,就算 createCounter() 已經執行過了,還是可以訪問,這就是閉包的效果。

function createCounter() {
  let count = 0;
  return function() {
    count += 1;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

閉包的運作
當一個函數在 JavaScript 中建立時,它帶有一個名為 [[Environment]] 的隱藏屬性,該屬性保留對其詞法環境(即定義該函數的位置)的引用,這種機制使 JavaScript 函數能夠「記住」它們的原始環境。

到現在還是沒有找到直接看 [[Environment]] 的方法,但是可以用 debugger 和 chrome 的開發者工具觀察函數內部的作用域和變量。

參考文章:[筆記]-JavaScript 閉包(Closure)是什麼?關於閉包的3件事词法环境(Lexical Environment)

Lexical Environment

閉包的應用


數據封裝和私有變量
在 JavaScript中,每創建函式,閉包就會在函式創建的同時被創建出來,作為函式內部與外部連接起來的一座橋樑。

任何閉包的使用場景都離不開這兩點:

  1. 建立私有變數
  2. 延長變數的生命週期

私有方法可以增加函式的可用性,主要在回傳的函式中可以執行多種方法,只要在 return 的地方用物件的方式包裝起來就可以了!

function createCounter() {
    let count = 0; // 建立私有變數

    return {
        increment: () => {
            count++;
            return count;
        },
        decrement: () => {
            count--;
            return count;
        },
        getCount: () => count
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
function storeMoney(initMoney = 1000) {
  const myMoney = initMoney;

  return {
    increase: function(price) {
      myMoney += price;
    },
    decrease: function(price) {
      myMoney -= price;
    },
    value: function(price) {
      return myMoney;
    }
  }
}

const kukuMoney = storeMoney(1000);
kukuMoney.increase(5000);
kukuMoney.decrease(1000);
console.log(kukuMoney.value()); // 5000 

函數工廠
閉包可以透過不同的變數,讓他做相同的事情,也稱為函式工廠。

function makeMultiplier(multiplier) {
  return function(x) {
    return x * multiplier;
  };
}

// 使用閉包分別產生兩個作用域
const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

🔔 使用閉包需要注意:
如果不是某些特定任務需要使用閉包,在函式中創建函式是不明智的,因為閉包在處理速度和記憶體消耗方面對腳本效能有負面影響,簡單來說,閉包使用過程需要保留對外部作用域變數的參考,所以會增加記憶體使用量。

作用域鏈和作用域的提升


🔔 有空可以看一下的相關資料:
You Don't Know JS Yet: Scope & Closures - 2nd Edition

第 4 天:函式 function提過函數作用域與閉包,這裡就要來好好說什麼是作用域,要做什麼的呢?

作用域鏈是指在執行上下文中,JavaScript 引擎如何查找變數和函式的過程,一個函式內部訪問變數時,JavaScript 引擎會按照以下順序查找變數:

  1. 內部作用域:首先,在當前執行上下文中查找變數。
  2. 外部作用域:如果在內部作用域中找不到,則查找上層作用域(即函式的外層或全局作用域)。

這樣的查找過程形成了一個鏈條,稱為作用域鏈。每個作用域都可以訪問其自身的變數,以及其外部作用域中的變數,直到全局作用域為止。

以下範例:
inner 函式能夠訪問 innerVarouterVarglobalVar,這是因為它們依次位於 inner 函式的作用域鏈中。

const globalVar = 'global';

function outer() {
    const outerVar = 'outer';
    function inner() {
        const innerVar = 'inner';
        console.log(innerVar);  // 'inner'
        console.log(outerVar);  // 'outer'
        console.log(globalVar); // 'global'
    }
    inner();
}

outer();

以下是面試曾經被問過類似的題目,請說明 console.log 出來陸續是什麼呢?為什麼?

function getArray() {
    const arr = [];

    for (var i = 0; i < 3; i++) {
        arr.push(function () {
            console.log(i);
        });
    }

    return arr
}

const fn = getArray();
fn[0]();
fn[1]();
fn[2]();

console.log
連續輸出都是 3 因為是執行外層函式作用域的變數。
其實實戰中,都是 push 變數什麼的,push 一個函數很少見,也不知道面試主管想要做什麼,所以往上看到 var 大概猜到他想要考 ES6 block scope...

外層作用域變數會隨著閉包概念不斷的去控制他,所以執行 for 迴圈的時候已經不斷的被累加,所以每次執行閉包的時候,只會取到目前作用域下的數字。

再說簡單些因為他是用 var第 2 天:基本語法和資料類型的宣告變數 - varletconst (letconst 在 ES6 出現)提過 var 只有 function scope,而不是 block scope,也就是在整個 getArray() 裡面都是可見的,而不是只有在定義的 for 循環塊內,所以當 for 循環完成時,i 的值是 3。

🤔 那跟閉包有什麼關係?
var 在整個函數內部都是可見的,所以所有閉包都引用同一個變量,console.log 出來的 i 就都會是最後一個 3。

🤔 怎麼印出 0, 1, 2?

  1. 立即函式:
    因為立即函式有一個很重要的功能:限制作用域
    每次 for 循環迭代時,IIFE 都會被立即呼叫,然後創建一個新的作用域來保存 i 的當前值,所以作用域是獨立的!
    function getArray() {
        const arr = [];
    
        for (var i = 0; i < 3; i++) {
            arr.push(((i) => () => console.log(i))(i));
        }
    
        return arr;
    }
    
    const fn = getArray();
    fn[0]();
    fn[1]();
    fn[2]();
    
  2. ES6 的 letconst
    這樣 for 迴圈的變數作用域就只會在整個迴圈中,不會受到外層影響,外層也無法取得!
    function getArr() {
        const arr = [];
    
        for (let i = 0; i < 3; i++) {
            arr.push(() => console.log(i));
        }
    
        return arr
    }
    
    const fn = getArr();
    fn[0]();
    fn[1]();
    fn[2]();
    

上一篇
第 17 天:關於 this 這件事
下一篇
第 19 天:常見的記憶體洩漏問題和解決方法
系列文
30天 JavaScript 提升計畫:從零到精通結合2024年的創新功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言