JavaScript 闭包原理、应用场景与内存管理详解

作者:小括号

时间:2026-10-22

浏览:298

本文深入解析 JavaScript 闭包的核心概念,阐述其作为函数、使用外部变量及存在于定义作用域内的三大特征。通过 setTimeout 异步执行经典案例,演示如何利用闭包保存变量状态。同时重点分析闭包导致的内存占用与 DOM 循环引用问题,提供将变量置 null 等内存管理最佳实践。

文章目录

闭包的基本定义

闭包的定义

ECMAScript规范指出,闭包是指包含未绑定变量的函数表达式,即函数能够访问其外部作用域中定义的变量。

该定义可能令人困惑,接下来进行解析。

  • 闭包本质上是一个函数
  • 闭包能够访问其外部定义的变量
  • 闭包必须存在于定义这些变量的作用域内

上述特征已初步清晰,具体指代外部变量需结合变量作用域概念理解。

变量作用域与生存周期

变量作用域机制

变量分为全局与局部两类。全局变量具有全局作用域,可在JS代码任意位置访问;在函数内使用var声明的变量为局部变量,其作用域仅限于该函数内部,外部无法访问。


var func = function(){
  var a = 'linxin';
  console.log(a);     // linxin
}
func();
console.log(a);       // Uncaught ReferenceError: a is not defined

作用域概念相对基础,此处重点探讨与闭包密切相关的变量生命周期。

变量生命周期管理

全局变量生命周期永久有效;局部变量在定义它的函数执行结束后,会被垃圾回收机制销毁。再次调用该函数时,将重新创建新的局部变量实例。


var func = function(){
  var a = 'linxin';
  console.log(a);
}
func();

变量a作为局部变量,在func函数调用结束后即被销毁。


var func = function(){
  var a = 'linxin';
  var func1 = function(){
    a += ' a';
    console.log(a);
  }
  return func1;
}
var func2 = func();
func2();          // linxin a
func2();          // linxin a a
func2();          // linxin a a a

观察发现,首次调用func2后,func内部的变量a值变为'linxin a'且未被销毁。这是因为func1形成了闭包,从而延长了变量a的生命周期。

闭包的三大特征

至此,闭包的概念已较为明确。

  • 闭包是一个函数,例如前述的func1函数
  • 闭包引用其他函数定义的变量,防止其被销毁,如func1调用变量a
  • 闭包存在于定义该变量的作用域中,既然变量a存在于func的作用域,那么func1也必然位于此作用域内。

综上所述,满足上述三个条件的即为闭包。

闭包经典应用案例

接下来通过一个经典示例深入理解闭包。


for (var i = 0; i < 4; i++) {
  setTimeout(function () {
    console.log(i)
  }, 0)
}

直观预期控制台输出0 1 2 3,实际却输出4 4 4 4。原因在于setTimeout为异步执行,当回调函数执行时,for循环已结束,此时变量i的值为4。因此,function() { console.log(i) } 查找变量i时,只能获取到最终值4。

回顾前例,闭包保存了变量a的值,此处亦可利用闭包保存0 1 2 3。


for (var i = 0; i < 4; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, 0)
  })(i)
}

当i=0时,将0作为参数传入匿名函数,此时function(i){}内部的i值为0。待setTimeout执行时,沿作用域链向外查找i,即可获取到0。循环往复,即可得到期望的0 1 2 3结果。

闭包与内存管理

内存管理机制

在闭包中调用局部变量,会导致该局部变量无法及时销毁,类似于全局变量持续占用内存。如需释放这些变量占用的内存,可手动将相关变量设置为null。

然而,使用闭包时易形成JavaScript对象与DOM对象的循环引用,进而引发内存泄露。浏览器垃圾回收机制规定,若两个对象相互引用形成循环,则均无法被回收。


function func() {
  var test = document.getElementById('test');
  test.onclick = function () {
    console.log('hello world');
  }
}

在前例中,func函数通过匿名函数创建闭包。变量test为JavaScript对象,引用id为test的DOM对象;DOM对象的onclick属性引用闭包;闭包又可调用test,从而形成循环引用,导致两对象均无法回收。解决此问题只需将循环引用中的变量设为null。


function func() {
  var test = document.getElementById('test');
  test.onclick = function () {
    console.log('hello world');
  }
  test = null;
}

若在func函数中不通过匿名函数创建闭包,而是引用外部函数,则不会引发循环引用问题。


function func() {
  var test = document.getElementById('test');
  test.onclick = funcTest;
}
function funcTest(){
  console.log('hello world');
}

本文内容至此结束,希望对大家的学习或工作有所助益,欢迎继续支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章