全面了解 Javascript 閉包(Closure)
閉包是 JavaScript 中一個重要的概念,也是一個比較難理解的概念之一。本篇技術筆記將詳細解釋閉包的概念、優點、缺點、實現方式和技巧,以及閉包在不同程式語言中的異同。希望通過本篇技術筆記的介紹,讀者可以更好地理解和應用閉包。
什麼是閉包?
閉包是指一個函式與其引用的外部變數形成的一個環境,也可以理解為函式和其引用的外部變數的一個包裹。下面是一個簡單的閉包例子:
function outer() {
const outerVar = 'Hello'; // 真正要執行的函式內要用到的變數
// 真正要執行的函式
function inner() {
const innerVar = 'World';
console.log(`${outerVar} ${innerVar}`);
}
return inner; // 把內部函數回傳出來
}
const fn = outer();
fn(); // output: "Hello World"
看到一個 function 內 return 了另一個 function,通常就是有用到閉包的概念。
閉包的優點和應用
閉包的優點在於可以保存狀態、實現私有變數和方法、實現函式柯里化、實現延遲計算等。
保存狀態
閉包可以保存狀態,也就是說,當我們執行閉包時,它可以記住之前的狀態。這一點在一些場景下非常有用,例如實現計數器、緩存等。
下面是一個實現計數器的例子:
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const fn = counter();
fn(); //output: "1"
fn(); //output: "2"
fn(); //output: "3"
在上面的例子中,counter 函式返回一個閉包,閉包中保存了 count 變數的狀態,每次執行閉包時 count 變量會增加 1,然後輸出 count 的值。當我們連續執行 fn() 時,閉包會記住 上一次 count 的值,因此 count 會持續增加。
實現私有變數和方法
閉包可以實現私有變數和方法,這是因為閉包中的變數和方法只能在閉包內部訪問,無法從閉包外部訪問。
下面是一個實現私有變數和方法的例子:
function createPerson(name) {
let age = 0;
function grow() {
age++;
}
function getAge() {
return age;
}
return {
getName: function () {
return name;
},
getAge: getAge,
grow: grow,
};
}
const person = createPerson('John');
person.grow();
console.log(`${person.getName()} is ${person.getAge()} years old.`);
在上面的例子中,createPerson 函式返回一個物件,物件中包含了 getName、getAge 和 grow 三個方法。其中 getName 方法返回 name 參數,getAge 方法返回 age 變數,grow 方法自增 age 變數。由於 age 變數和 grow 方法只在閉包中被定義,因此無法從閉包外部訪問,從而實現了私有變數和方法。
實現函式柯里化 Currying
閉包可以實現函式柯里化(Currying),也就是將一個接受多個參數的函式轉換為一系列只接受一個參數的函式,並且返回一個新的函式,新的函式接受一個參數,並且返回一個新的函式,直到最後一個函式返回結果為止。柯里化的作用在於提前接收部分參數,延遲執行,不立即輸出結果。
下面是一個實現函式柯 里化的例子:
function add(a, b, c) {
return a + b + c;
}
function curry(fn) {
const len = fn.length;
let args = [];
return function curryFn(...curryArgs) {
args = [...args, ...curryArgs];
if (args.length >= len) {
const result = fn.apply(this, args);
args = [];
return result;
} else {
return curryFn;
}
};
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // output: 6
console.log(curriedAdd(1, 2)(3)); // output: 6
console.log(curriedAdd(1, 2, 3)); // output: 6
在上面的例子中,add 函式接受三個參數,返回三個參數的和。curry 函式接受一個函式 fn,返回一個新的函式 curryFn
。curryFn
接受一個參數 curryArgs
,將 curryArgs
添加到 args 中,如果 args 的長度大於等於 fn 的參數個數 len,則執行 fn,返回結果;否則,返回 curryFn
。這樣,就可以實現將一個接受多個參數的函式轉換為一系列只接受一個參數的函式。
在這個例子中,我們使用了閉包保存了 args 變數和 curryFn 方法,因此 curryFn 方法可以持續保存 args 變量的值,直到最後一個函式返回結果為止。
閉包的缺點和限制
閉包雖然有很多優點和應用,但也存在一些缺點和限制。以下是其中幾個值得關注的方面。
1. 記憶體消耗:
閉包會記錄外部函式的環境,這意味著閉包會佔用額外的記憶體。當閉包中包含大量的變數時,可能會導致記憶體消耗過大,影響性能。
2. 內存洩漏:
閉包中的變數會被一直保留在內存中,直到閉包被銷毀。如果不小心使用閉包,可能會導致內存洩漏,進而影響應用程序的性能。
3. 參數傳遞:
閉包中的函數只能訪問當前閉包被創建時的變數值,而不能直接訪問當前變數的值。因此,在閉包中使用外部函數的變數時,需要注意變數的值是否已被修改。
閉包的實現方式和技巧
閉包的實現方式和技巧非常多樣,下面是一些常用的方法。