闭包
developer.mozilla.org/zh-CN/docs/...
一、基本概念
1. 定义
闭包是指能够访问其他函数内部变量的函数 ,或者说函数与其相关的引用化解组合而成的实体。
2. 核心特征
l 函数嵌套函数
l 内部函数可以访问外部函数的变量和参数
l 外部函数的变量会持久保存在内存中
二、创建闭包的典型场景
1. 函数返回函数
2. 函数作为参数传递
3. IIFE(立即执行函数表达式)
三、闭包的核心原理
1. 作用域链机制
作用域链基本概念
作用域链是一个由多个变量对象(VO)或活动对象(AO)组成的链表,它决定了函数在查找变量时的搜索顺序。
作用域链的组成
- 当前函数的变量对象(AO - Activation Object)
- 闭包所在的父级函数变量对象
- 更外层的变量对象(直到全局对象)
闭包作用域链的形成过程
-
函数定义时:
- 函数会保存其被创建时的作用域链(称为
[[Scope]]
属性) - 这个
[[Scope]]
包含了函数定义时可访问的所有变量对象
- 函数会保存其被创建时的作用域链(称为
-
函数调用时:
- 创建新的执行上下文
- 创建新的活动对象(AO)用于存储局部变量
- 将AO添加到
[[Scope]]
前端,形成完整的作用域链
示例分析
javascript
function outer() {
let outerVar = 'outer';
return function inner() {
let innerVar = 'inner';
console.log(outerVar + innerVar);
};
}
const closure = outer();
closure();
作用域链形成过程
-
当
outer
函数定义时:outer.[[Scope]]
= [全局变量对象]
-
当
outer
函数执行时:outer
的执行上下文作用域链 = [outer的AO, 全局变量对象]
-
当
inner
函数定义时(在outer
内部):inner.[[Scope]]
= [outer的AO, 全局变量对象]
-
当
closure()
执行时:inner
的执行上下文作用域链 = [inner的AO, outer的AO, 全局变量对象]
变量查找机制
当闭包访问一个变量时,JavaScript引擎会按照以下顺序查找:
- 首先检查当前函数的活动对象(AO)
- 如果没找到,沿着作用域链向外查找
- 直到找到该变量或到达全局对象
闭包作用域链的特点
-
静态作用域(词法作用域):
- 作用域链在函数定义时就已确定,而不是调用时
- 这是闭包能够"记住"外部变量的关键
-
持久性:
- 即使外部函数已执行完毕,其变量对象仍被闭包引用
- 只有当所有引用都消失时,这些变量才会被回收
-
层级性:
- 多层嵌套函数会形成多层作用域链
- 每个闭包都有自己的作用域链副本
2. 内存模型
当闭包形成时,内存中会发生以下变化:
text
[栈内存]
|
|-- 执行上下文 (创建闭包时的上下文)
| |-- 变量对象 (VO)
| | |-- 局部变量1
| | |-- 局部变量2
| | └-- ...
| └-- 作用域链
|
└-- [闭包对象] (存储在堆内存中)
|-- [[Scope]] (指向创建时的作用域链)
|-- 函数代码
└-- 可能的其他属性 (如name, length等)
关键特点
- 堆内存存储:闭包本身存储在堆内存中,而不是栈内存
- 引用保持:闭包会保持对其外部变量的引用,阻止垃圾回收
- 独立环境:每次函数调用都会创建一个新的闭包环境
示例分析
javascript
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
内存模型:
- 调用
outer()
时,创建执行上下文,count
变量存储在变量对象中 - 返回
inner
函数时,形成闭包,inner
的[[Scope]]
属性指向outer
的作用域链 - 即使
outer
执行完毕,其变量对象仍被inner
的闭包引用,不会被回收
四、闭包的实际应用
1. 数据封装和私有变量
javascript
function createCounter() {
let count = 0; // 外部变量
return function() { // 闭包
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
2. 模块模式
javascript
const module = (function() {
let privateVar = "I'm private";
return {
getVar: function() {
return privateVar;
},
setVar: function(newVal) {
privateVar = newVal;
}
};
})();
console.log(module.getVar()); // "I'm private"
module.setVar("Updated");
console.log(module.getVar()); // "Updated"
3. 函数工厂
javascript
function makeMultiplier(x) {
return function(y) {
return x * y;
};
}
const double = makeMultiplier(2);
console.log(double(5)); // 10
const triple = makeMultiplier(3);
console.log(triple(5)); // 15
4. 事件处理和回调
javascript
function delayLog(message, delay) {
setTimeout(function() {
console.log(message); // 闭包记住message
}, delay);
}
delayLog("Hello after 1s", 1000);
五、闭包的优缺点
优点
- 数据封装(私有变量)
闭包可以创建私有变量,避免全局污染,实现模块化编程。
javascript
function createCounter() {
let count = 0; // 外部无法直接访问
return {
increment: function() { return ++count; },
getCount: function() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
// console.log(count); // Error: count未定义(私有变量)
- 保持状态
闭包可以记住外部函数的变量,即使外部函数已经执行完毕。
javascript
function memoize(fn) {
const cache = {}; // 缓存数据
return function(arg) {
if (cache[arg]) return cache[arg];
cache[arg] = fn(arg);
return cache[arg];
};
}
- 函数工厂
可以动态生成不同行为的函数,提高代码复用性。
javascript
function makeGreeting(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = makeGreeting("Hello");
console.log(sayHello("Alice")); // "Hello, Alice!"
- 回调函数 & 事件处理
闭包可以保留上下文,适用于异步操作(如 setTimeout、Promise、事件监听)。
javascript
function delayLog(message, delay) {
setTimeout(function() {
console.log(message); // 闭包记住message
}, delay);
}
delayLog("Hello after 1s", 1000);
缺点
- 内存泄漏风险
闭包会长期持有外部变量的引用,导致垃圾回收(GC)无法释放内存。
示例(意外内存泄漏):
场景1:DOM 元素与闭包
javascript
function setupButton() {
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
// 这个闭包隐式引用了button
console.log('Button clicked');
});
}
场景2:大对象意外保留
javascript
function outer() {
const bigData = new Array(1000000).fill("data"); // 大数据
return function() {
console.log("Closure keeps bigData in memory!");
};
}
const closure = outer(); // bigData 不会被释放
解决方案:
当 JavaScript 引擎创建闭包时,它会保留整个外部函数的词法环境,而不仅仅是闭包实际使用的变量。
如果你确实不需要保留 bigData,可以手动释放:
javascript
function outer() {
const bigData = new Array(1000000).fill("data");
const result = function() {
console.log("Closure");
};
// 手动释放不再需要的大数据
bigData.length = 0; // 或者 bigData = null;
return result;
}
- 性能影响
l 闭包访问外部变量比访问局部变量稍慢(需查找作用域链)。
l 过度使用闭包可能影响执行效率。
- 变量共享问题
在循环中使用闭包可能导致变量共享问题 (如 for 循环中的 var)。
错误示例:
javascript
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 全部输出3(i是共享的)
}, 1000);
}
解决方法( 使用 let 或 IIFE):
javascript
for (let i = 0; i < 3; i++) { // let 创建块级作用域
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 1000);
}