?“愛自有天意,天有道自不會讓有情人分離”
大家好,關于閉包問題其實實際上是js作用域的問題,那么js有幾種作用域呢?
作用域類型 | 關鍵字/場景 | 作用域范圍 | 示例 |
---|---|---|---|
全局作用域 | var (無聲明) | 整個程序 | var x = 10; |
函數作用域 | var ?在函數內 | 函數內部 | function foo() { var x; } |
塊級作用域 | let 、const | {} ?代碼塊內 | if (true) { let x; } |
模塊作用域 | ES6 模塊 | 單個模塊文件 | export const x = 1; |
詞法作用域 | 函數定義時 | 定義時的外層作用域鏈 | 閉包 |
我們常見的就是?全局作用域,函數作用域和塊級作用域了。
閉包叫做詞法作用域,我沒聽說過這個詞,總而言之,閉包是一個作用域問題
?什么是閉包??
閉包(Closure)是 JavaScript 中的一個核心概念,它指的是 ??函數能夠記住并訪問其定義時的作用域(詞法環境),即使該函數在其作用域之外執行??。
用人話來講就是:閉包是可以訪問到另一個函數作用域中變量的函數?
在循環嵌套的函數結構中,閉包就很容易理解了。內部函數可以訪問到外部函數中的變量,但是外部函數不能訪問到內部函數中的變量。
我來舉一個例子:
?
function outerFunction(outerParam) {// 外部函數的變量let outerVar = "我是外部變量";const outerConst = "我是外部常量";function innerFunction(innerParam) {// 內部函數的變量let innerVar = "我是內部變量";// 內部函數可以訪問:// 1. 自己的變量console.log("內部函數訪問自己的變量:", innerVar);console.log("內部函數訪問自己的參數:", innerParam);// 2. 外部函數的變量和參數console.log("內部函數訪問外部變量:", outerVar);console.log("內部函數訪問外部常量:", outerConst);console.log("內部函數訪問外部參數:", outerParam);return innerVar;}console.log("\n----- 分割線 -----\n");// 外部函數嘗試訪問內部函數的變量(會失敗)console.log("外部函數可以訪問自己的變量:", outerVar);console.log("外部函數可以訪問自己的參數:", outerParam);// 下面這行如果取消注釋會報錯// console.log("外部函數無法訪問內部變量:", innerVar); // ReferenceError: innerVar is not defined// 調用內部函數const result = innerFunction("內部參數");console.log("只能通過內部函數的返回值來獲取內部變量:", result);return innerFunction;
}// 測試
const innerFn = outerFunction("外部參數");
console.log("\n----- 分割線 -----\n");
innerFn("新的內部參數");
輸出結果:
----- 分割線 -----外部函數可以訪問自己的變量: 我是外部變量
外部函數可以訪問自己的參數: 外部參數
內部函數訪問自己的變量: 我是內部變量
內部函數訪問自己的參數: 內部參數
內部函數訪問外部變量: 我是外部變量
內部函數訪問外部常量: 我是外部常量
內部函數訪問外部參數: 外部參數
只能通過內部函數的返回值來獲取內部變量: 我是內部變量----- 分割線 -----內部函數訪問自己的變量: 我是內部變量
內部函數訪問自己的參數: 新的內部參數
內部函數訪問外部變量: 我是外部變量
內部函數訪問外部常量: 我是外部常量
內部函數訪問外部參數: 外部參數
?這個代碼展示的是:
-
內部函數可以訪問:
- 自己的變量(innerVar)和參數(innerParam)
- 外部函數的變量(outerVar)、常量(outerConst)和參數(outerParam)
-
外部函數只能訪問:
- 自己的變量(outerVar)和參數(outerParam)
- 無法直接訪問內部函數的變量(innerVar)
- 只能通過內部函數的返回值來間接獲取內部變量的值
這就是所謂的"作用域鏈",內部函數可以向上訪問外部作用域的變量,但外部作用域不能訪問內部作用域的變量。
閉包能干什么?
閉包能干的事情有:變量私有化、回調函數、函數柯里化。
變量私有化
什么是變量私有化?
變量私有化是一種編程技術,目的是??限制變量的訪問范圍??,使其只能在特定的作用域或模塊內被訪問和修改,外部代碼無法直接操作。這樣可以提高代碼的安全性、可維護性,并減少命名沖突的風險。
通過閉包實現一下變量私有化
我們來做一個計數器案例,外部不能修改count,只能通過?increment()
?和?getCount()
?操作。
function createCounter() {let count = 0; // 私有變量,外部無法直接訪問return {increment() {count++;},getCount() {return count;},};
}const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(count); // 報錯:count is not defined(無法直接訪問私有變量)
我們利用閉包創建了一個私有變量count,無法在外部訪問,只有通過我們的increment()
?和?getCount()
?操作才能操作和訪問。
回調函數
回調函數想必就不用介紹了,在任何語言中都有出現和應用。
閉包可以讓回調函數記住并訪問其定義時的作用域變量,即使回調在異步操作(如?
setTimeout
、fetch
、事件監聽)中被調用。
介紹一個例子:
setTimeout
?回調??
??問題??:直接使用循環變量?i
?會導致所有回調輸出相同的值(var
?沒有塊級作用域)。
??解決??:用閉包保存每次循環的?i
?值。
// ? 錯誤寫法(輸出 3 個 3)
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 輸出 3, 3, 3}, 100);
}// ? 正確寫法(閉包保存 i 的值)
for (var i = 0; i < 3; i++) {(function(j) { // 立即執行函數(IIFE)創建閉包setTimeout(function() {console.log(j); // 輸出 0, 1, 2}, 100);})(i); // 傳入當前 i 的值
}
監聽事件中的閉包
function setupButtons() {const buttons = document.querySelectorAll('button');for (var i = 0; i < buttons.length; i++) {(function(index) { // 閉包保存當前按鈕的索引let count = 0; // 每個按鈕獨立的計數器buttons[index].addEventListener('click', function() {count++;console.log(`按鈕 ${index} 被點擊了 ${count} 次`);});})(i);}
}setupButtons();
我們發現在回調函數場景中閉包的作用很多是幫我們留下或者說是記住作用域變量,可以讓我們的邏輯更加簡單。
函數柯里化
wow,好高級的詞!
什么是函數柯里化
函數柯里化??(Currying)是一種將 ??多參數函數?? 轉換為 ??一系列單參數函數?? 的技術。
它的核心思想是:??每次只接受一個參數,并返回一個新函數,直到所有參數收集完畢,才執行最終計算??。
總而言之就是:分布傳參。
剛才我們在回調函數中了解到:“我們發現在回調函數場景中閉包的作用很多是幫我們留下或者說是記住作用域變量,可以讓我們的邏輯更加簡單。”
那么:函數柯里化是指將一個多參數函數轉換為一系列單參數函數的過程。那么閉包剛好利用它能記住函數定義時的作用域這一特點就可以實現柯里化;
用閉包做函數柯里化
簡單例子:
// 普通函數(3個參數)
function sum(a, b, c) {return a + b + c;
}// 手動柯里化(閉包實現)
function curriedSum(a) {return function(b) {return function(c) {return a + b + c;};};
}// 調用方式
console.log(curriedSum(1)(2)(3)); // 6
閉包帶來的危害
1. 內存泄漏(Memory Leaks)??
??問題描述??
閉包會長期持有外部函數的變量,阻止垃圾回收(GC),導致內存無法釋放。
??示例??
function createHeavyObject() {const bigData = new Array(1000000).fill("X"); // 占用大量內存的變量return function() {console.log(bigData.length); // 閉包引用 bigData,即使外部函數執行完畢};
}const holdClosure = createHeavyObject(); // bigData 無法被回收!
??解決方法??
- 在不需要閉包時手動解除引用:
holdClosure = null; // 釋放閉包持有的內存
- 避免在閉包中保存不必要的變量(如 DOM 元素、大對象)。
??2. 性能損耗(Performance Overhead)??
??問題描述??
- 閉包會創建額外的作用域鏈,訪問外部變量比訪問局部變量稍慢。
- 在頻繁調用的函數(如動畫、滾動事件)中使用閉包可能導致性能下降。
??示例??
// 每次觸發 scroll 都會訪問閉包變量
window.addEventListener("scroll", function() {const cached = heavyCompute(); // 閉包可能持有 heavyCompute 的結果console.log(cached);
});
??解決方法??
- 對于高頻操作,盡量使用局部變量而非閉包變量。
- 用?
debounce
/throttle
?限制觸發頻率。
??3. 意外的變量共享(Unexpected Shared State)??
??問題描述??
循環中創建的閉包可能共享同一個變量(尤其是用?var
?時)。
??示例??
// ? 錯誤寫法:所有按鈕都輸出 3
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 輸出 3, 3, 3(i 是共享的)}, 100);
}// ? 正確寫法:用 IIFE 或 let 隔離變量
for (let i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 輸出 0, 1, 2}, 100);
}
??解決方法??
- 使用?
let
/const
?替代?var
(塊級作用域)。 - 用 IIFE(立即執行函數)隔離變量:
for (var i = 0; i < 3; i++) {(function(j) {setTimeout(function() {console.log(j); // 正確輸出 0, 1, 2}, 100);})(i); }
??4. 調試困難(Debugging Challenges)??
??問題描述??
閉包的作用域鏈可能讓變量來源難以追蹤,增加調試復雜度。
??示例??
function outer() {const secret = 42;return function inner() {debugger; // 在這里查看作用域鏈,可能有多層閉包console.log(secret);};
}
const mystery = outer();
mystery();
??解決方法??
- 在 Chrome DevTools 中使用 ??Scope?? 面板查看閉包變量。
- 避免過度嵌套閉包,保持函數簡潔。
??5. 閉包與?this
?的混淆??
??問題描述??
閉包中的?this
?可能丟失預期指向(尤其是嵌套函數中)。
??示例??
const obj = {name: "Alice",greet: function() {return function() {console.log(this.name); // ? 輸出 undefined(this 指向全局或 undefined)};}
};
obj.greet()(); // 調用內部函數
??解決方法??
- 使用箭頭函數(繼承外層?
this
):greet: function() {return () => console.log(this.name); // ? 正確輸出 "Alice" }
- 提前綁定?
this
:greet: function() {const self = this;return function() {console.log(self.name); // ? 正確輸出 "Alice"}; }
?
閉包是一把雙刃劍,它既可以:創建私有變量,避免全局變量污染?也會:閉包會導致內存泄漏,如果不銷毀閉包,他引用的外部變量就會一直保存在內存當中,無法被釋放,從而導致內存泄漏 。
就像她對你一樣,既能在戀愛中讓你開心幸福,也會在吵架時讓你痛苦不堪
但是,只要我們珍惜這些幸福,勇敢面對好好處理這些痛苦就能讓我們的感情歷久彌新。閉包也是一樣啊,只要我們利用好它的優點,規避全局變量污染就能讓我們變成大佬。
所以,面對再多的困難,再多的誤會也要拉緊她的手,會幸福的!?