闭包,每次准备面试就要来背背,争取一篇文章讲清楚。
目录
闭包的概念
闭包(Closure)是指有权访问另一个函数作用域中变量的函数。
闭包的形成通常发生在嵌套函数中,内层函数可以访问外层函数的变量,即使外层函数已经执行完毕。
闭包的核心是词法作用域(Lexical Scope),即函数在定义时就能记住其所在的上下文环境。
上述概念里出现了函数作用域和词法作用域,先细讲一下作用域。
作用域
即变量(上下文)或者函数生效(能被访问)的区域或者集合,换句话说,作用域决定了代码区块中变量和其他资源的可见性。
我们一般将作用域分成:
- 全局作用域
- 函数作用域
- 块级作用域
全局作用域
全局作用域是 JavaScript 中最外层的作用域,变量或函数在全局作用域中声明时,可以在代码的任何地方访问。全局变量会成为全局对象(如浏览器中的 window
)的属性。
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,这些变量可以在程序的任何位置访问
var globalVar = "I'm global";
console.log(window.globalVar); // 输出: "I'm global"
函数作用域
函数作用域是指在函数内部声明的变量或函数,只能在函数内部访问。使用 var
声明的变量具有函数作用域。
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,她就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
function test() {
var funcVar = "I'm inside a function";
console.log(funcVar); // 输出: "I'm inside a function"
}
console.log(funcVar); // 报错: funcVar is not defined
块级作用域
块级作用域由 let
和 const
声明引入,变量仅在 {}
定义的代码块内有效。
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号之外不能访问
let和const没有变量提升(var
声明的变量和函数声明会被提升至作用域顶部,但赋值不会提升。let
和const
存在暂时性死区(TDZ),声明前访问会报错)
if (true) {
let blockVar = "I'm block-scoped";
console.log(blockVar); // 输出: "I'm block-scoped"
}
console.log(blockVar); // 报错: blockVar is not defined
词法作用域(静态作用域)
JavaScript 采用词法作用域,变量的作用域由代码的书写位置决定,而非运行时调用位置。
变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,Javascript遵循的就是词法作用域
// 由于Javascript遵循词法作用域,相同层级的foo和bar就没有办法访问到彼此函数作用域中的变量
var a = 1;
function foo(){
console.log(a)
}
function bar(){
var a = 2
foo()
}
bar(); //输出1
作用域链
作用域链是 JavaScript 查找变量的机制,从当前作用域开始逐级向外层作用域查找,直至全局作用域。
如果在全局作用域里仍然找不到该变量,它会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
var global = "Global";
function outer() {
var outerVar = "Outer";
function inner() {
console.log(outerVar); // 输出: "Outer"
console.log(global); // 输出: "Global"
}
inner();
}
outer();
闭包是函数与其词法作用域的组合,即使函数在定义的作用域之外调用,仍能访问其词法作用域的变量。
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const closureFn = outer(); // 形成闭包
closureFn(); // 输出 1
closureFn(); // 输出 2
闭包的作用
- 保存状态:闭包可以保存函数执行时的变量状态,实现类似私有变量的效果。
- 模块化:通过闭包封装私有变量和方法,避免全局污染。
- 延迟执行:闭包可以延迟变量的生命周期,例如在事件回调或异步任务中使用。
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在的词法环境依然存在,以达到延长变量的生命周期。
闭包的常见应用场景
- 计数器:通过闭包实现计数器的状态保存。
- 私有变量:模拟面向对象中的私有成员。
- 函数工厂:动态生成具有特定行为的函数。
// 示例
function createCounter() {
let privateCount = 0;
return {
increment: () => privateCount++,
getCount: () => privateCount,
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出 1
闭包的注意事项
- 内存泄漏:闭包会长期持有对外部变量的引用,可能导致内存无法释放。
- 性能影响:过度使用闭包可能增加内存消耗和执行时间。
- 变量共享:在循环中创建闭包时,需注意变量共享问题(通常用 IIFE 或
let
解决)。
// 循环中的闭包问题及解决
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
}
// 解决方案:使用 IIFE 或 let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出 0, 1, 2
}
闭包的底层原理
闭包的实现依赖于 JavaScript 的作用域链机制。当函数被创建时,会生成一个包含其所在作用域链的闭包对象(Closure)。即使外层函数执行完毕,其变量对象仍被内层函数引用,因此不会被垃圾回收。
通过理解闭包的概念、应用和注意事项,可以更高效地利用闭包解决实际问题,同时避免常见的陷阱。