Javascript|閉包 Closure 的介紹及應用。閉包跟 FP 的關係是什麼?

Javascript 閉包 Closure 介紹、閉包跟 FP 的關係?

Molly M
14 min readApr 21, 2023

✨前言

雖然我一直都是用 functional programming 的概念在撰寫程式,但沒有使用過的閉包,以此篇文章作為學習閉包的紀錄,也希望幫助還沒理解閉包是什麼的讀者們。

至於 functional progamming 在 React 的應用,我們下一篇文章來探討(可以期待是一篇超級實用且知識含量豐富的文章,耶)

✨目錄

- ✨什麼是閉包?
- ✨閉包的特性
- ✨閉包的使用方法: 函式作為返回值 / 將函式作為參數 / 使用 IIFE
- ✨閉包的應用場景
- ✨閉包的注意事項、優點、缺點
- ✨補充: 閉包 Closure 跟 FP 的關係是什麼?用閉包解決這常見的面試考題?
- ✨總結

✨ 什麼是閉包?

閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。(MND) 呼叫函式內的函式。

最常被使用來講解閉包的範例為:

function outer() {
let x = 10;

function inner() {
console.log(x);
}

return inner;
}

const fn = outer();
fn(); // 10

在這個例子中,outer 定義了一個變數 x,然後定義了一個內部函式 inner,並且在最後返回了 inner

在外部,我們創建了一個變數 fn,等於調用 outer 函式後返回的 inner 函式。

最後,我們調用 fn 函式,可以發現它正確地取得 x 變數的值,這是因為 inner 函式與 x 變數綁定在一起,形成了一個閉包

✨閉包的特性

  1. 可以封裝變數:閉包可以將變數封裝在函式內部,從而保護變數的狀態。
  2. 可以訪問外部變數:閉包可以訪問和使用其所在作用域中的外部變數,這樣可以實現更加靈活的功能。
  3. 可以將函式當做值來傳遞:由於閉包將函式和環境綁定在一起,因此可以將閉包當做值來傳遞和使用。

✨閉包的使用方法

閉包的使用方法主要有三種:

1. 函式作為返回值:在一個函式中定義一個內部函式,並在最後返回這個內部函式。

function outerFunction() {
var outerVariable = "Hello";
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}

var myFunction = outerFunction();
myFunction(); // "Hello"

2. 將函式作為參數:將一個函式當做參數傳遞給另一個函式。

function outerFunction() {
var outerVariable = "Hello";
return function innerFunction(callback) {
callback();
}
}

var myFunction = outerFunction();
myFunction(function() {
console.log("Callback function is called!");
}); // "Callback function is called!"

3. 使用 IIFE(Immediately Invoked Function Expression 立即執行函式):定義一個函式,並且立即調用這個函式,這樣定義在這個函式中的變量和函式就可以形成閉包。

var myFunction = (function() {
var counter = 0;
return function() {
counter++;
console.log(counter);
}
})();

myFunction(); // 1
myFunction(); // 2

✨閉包的應用場景

閉包的使用情境:

  1. 緩存 cache:將一些較耗時的計算結果保存在閉包中,從而提高程序的運行效率。閉包可以用來實現簡單的緩存機制,將計算結果暫存起來,以減少重複計算。這種技巧在計算複雜度較高的函式時尤其有用。下面是一個緩存閉包的範例:
function createCache() {
const cache = new Map();

return function (key, compute) {
if (cache.has(key)) {
return cache.get(key);
} else {
const result = compute(key);
cache.set(key, result);
return result;
}
};
}

const computeFibonacci = createCache(function (n) {
if (n < 2) {
return n;
} else {
return computeFibonacci(n - 1) + computeFibonacci(n - 2);
}
});

console.log(computeFibonacci(10)); // 55
console.log(computeFibonacci(11)); // 89
console.log(computeFibonacci(10)); // 55 (cached)

在這個範例中,createCache 函式返回了一個閉包,該閉包包含了一個 Map 對象 cache,用於保存計算結果。當閉包被調用時,它會檢查 cache 中是否已經有了對應的計算結果,如果有的話直接返回,否則計算新的結果,並將其保存到 cache 中。

2. 迭代器 iterator:將迭代器的狀態保存在閉包中,從而實現迭代器的功能。閉包可以用來實現迭代器,讓函式可以按照特定的方式迭代數組或對象,從而簡化對數據的操作。下面是一個簡單的迭代器閉包的範例:

function createIterator(array) {
let index = 0;

return function () {
if (index < array.length) {
const value = array[index];
index++;
return value;
} else {
return undefined;
}
};
}

const iterator = createIterator([1, 2, 3]);

console.log(iterator()); // 1
console.log(iterator()); // 2
console.log(iterator()); // 3
console.log(iterator()); // undefined

在這個範例中,createIterator 函式返回了一個閉包,該閉包包含了一個變量 index,用於追蹤數組的索引位置。當閉包被調用時,它會返回數組中的下一個元素,直到全部遍歷完畢。

3. 異步 asynchronous:將回調函式保存在閉包中,從而實現異步操作的功能。閉包可以用來實現異步操作,將回調函式保存在閉包中,以便在需要的時候調用。下面是一個使用閉包實現異步操作的範例:

function downloadFile(url, callback) {
setTimeout(function () {
const content = `This is the content of ${url}`;
callback(content);
}, 1000);
}

function processFile(url) {
downloadFile(url, function (content) {
console.log(`Processing file: ${url}`);
console.log(`Content: ${content}`);
});
}

processFile('https://example.com/file1');
processFile('https://example.com/file2');

在這個範例中,downloadFile 函式模擬了一個網絡下載操作,它接收一個 URL 和一個回調函式,當下載完成後調用回調函式,並將下載的內容作為參數傳遞給它。processFile 函式使用 downloadFile 函式下載指定 URL 的文件,並在下載完成後對文件進行處理。

這個範例中使用了閉包技巧,將回調函式保存在 downloadFile 函式的內部,從而實現了異步操作。

4. 私有變數 private variable:將變數定義在閉包的內部,從而隱藏變數的內部實現細節。閉包可以實現私有變量,即變量不會被外部訪問和修改,只能通過函式內部的方法進行操作。這樣可以保護變量的安全性,避免外部的不當修改。

function createCounter() {
let count = 0;
return {
increment() {
count++;
},
decrement() {
count--;
},
getCount() {
return count;
}
};
}

const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1
counter.decrement();
console.log(counter.getCount()); // 0
console.log(counter.count); // undefined,無法訪問私有變量

在這個例子中,createCounter 函式返回了一個包含三個方法的對象,其中 count 是一個私有變量,外部無法直接訪問和修改。通過 incrementdecrement 方法可以分別增加和減少 count 的值,而 getCount 方法可以返回當前的值。外部只能通過 counter 對象來訪問這些方法,而無法訪問 count 變量。

5. 模組化 Modular:將模組的內部狀態保存在閉包中,對外只暴露必要的接口。閉包可以實現模組化,即將一個複雜的程式分成多個模塊,每個模塊只暴露必要的方法和變量,隱藏內部的實現細節,提高代碼的可讀性和可維護性。

const module = (function() {
let privateVar = '私有變量';
let privateFunc = function() {
console.log('私有方法');
};
return {
publicVar: '公共變量',
publicFunc: function() {
console.log('公共方法');
console.log(privateVar);
privateFunc();
}
};
})();

console.log(module.publicVar); // 公共變量
module.publicFunc(); // 公共方法 私有變量 私有方法
console.log(module.privateVar); // undefined,無法訪問私有變量
module.privateFunc(); // TypeError,無法訪問私有方法

在這個例子中,module 是一個匿名函式,用來實現模組化。

6. 單例模式 Singleton Pattern:將對象的實例保存在閉包中,每次調用時返回同一個實例。單例模式是一種設計模式,通過該模式可以確保一個類只有一個實例,並且這個實例可以被全局訪問。這種模式通常用於需要全局訪問的資源管理器,例如網頁上的購物車。

閉包可以用來實現單例模式,透過閉包可以保證在同一個執行環境下只會存在一個實例。以下是一個使用閉包實現單例模式的範例:

const Singleton = (function() {
let instance;

function createInstance() {
const object = new Object('I am the instance');
return object;
}

return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

在上面的範例中,Singleton 函式返回一個物件,該物件包含一個 getInstance 方法,用於獲取唯一的實例。透過閉包,instance 變數只會被創建一次,因此 getInstance 方法總是返回同一個實例。這樣就確保了只有一個全局可訪問的實例存在。

在使用單例模式時,需要注意保證實例的唯一性,並且在設計時也要考慮到後續擴展的可能性。

✨閉包的注意事項

使用閉包時需要注意以下幾點:

  1. 閉包中的變數是一直存在的:直到閉包被銷毀為止。因此,在使用閉包時需要注意內存泄漏的問題。
  2. 閉包中的變數是共享的:多個閉包共用一個變數時需要特別注意。
  3. 閉包中的變數可以被修改:因此需要適當地設置權限和約束,防止閉包中的變數被意外修改。

優點:

  1. 可以實現私有變數和保護變數:通過閉包,可以實現封裝和隱藏變數,使得變數只能通過暴露的接口進行訪問,從而增強了代碼的安全性和可維護性。
  2. 可以減少全域變數的污染:使用閉包可以將變數限定在函式的作用域內,減少了對全域命名空間的污染,從而增強了代碼的可讀性和可維護性。
  3. 可以實現某些高級功能:閉包可以用來實現某些高級功能,例如模組化程式碼、緩存、迭代器等。

缺點:

  1. 可能導致內存泄漏:由於閉包可以持續引用外部環境中的變數,因此如果不恰當地使用閉包,可能會導致內存泄漏問題。
  2. 可能導致代碼難以理解和維護:閉包可以讓程式碼變得複雜和難以理解,特別是在多層嵌套的情況下,因此開發者需要適當地使用閉包,以保持代碼的可讀性和可維護性。
  3. 可能會影響效能:閉包會增加函式的複雜度和運行時間,因此在需要高效率運行的場景下,閉包可能會影響程式碼的效能。

✨補充

Q: 閉包 Closure 跟 FP 的關係是什麼

為什麼介紹閉包會提到 FP(functional progamming)呢,因為閉包能夠幫助我們在程式碼確保以下幾點:

  • decoupling 低耦合性,每個 function 之間要能獨立開發,不能因為要改一個功能,全部都要跟著大改。
  • immutable 不可變性,保在記憶體中的變數是不可被外部更改的。
  • reusability 重複使用性,每個 function 要盡可能達到重複使用,以降低開發時間及成本。

而這些概念就符合 FP 的基本觀念,可以說閉包就是函式語言程式設計的核心概念之一。

Q: 用閉包解決這常見的面試考題:將這段程式碼的輸出調整為 0,1,2,3,4

for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}

作法 — 閉包

 for (var i = 0; i < 5; i++) {
((i) => {
setTimeout(() => {
console.log(i)
}, i * 1000)
})(i)
}

其他做法 — setTimeout 傳入參數

for (var i = 0; i < 5; i++) {
setTimeout((i) => {
console.log(i)
}, i * 1000,i)
}

其他做法— 用 Let

for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}

✨總結

閉包的概念大概就是這樣,但我覺得要真的實際用在專案上才會比較熟悉,不論是閉包應該注意的點或是優缺點,都需要真的在專案上跑一跑才會知道,光是用簡單的範例只能理解概念。

✨碎碎念

最近迎來了第 300 個 followers,每月平均觀看也將近 6000 次,謝謝大家的愛戴 ( ´•̥̥̥ω•̥̥̥` )

--

--

Molly M

Molly — Software Developer / 職稱是軟體研發工程師。 因為健忘所以用 Medium 紀錄,持續ㄉ慢慢前進(ง•̀_•́)ง ❤