JavaScript 闭包深度解析:原理、应用与常见陷阱

作者:木棉时间:2026-10-29 08:01:36浏览:316

本文深入解析 JavaScript 闭包的核心机制,从变量作用域链出发,阐述闭包如何读取局部变量并使其驻留内存。通过多个代码示例,演示闭包在数据共享、参数传递中的应用,并重点分析循环中闭包导致的变量共享陷阱及解决方案。同时涵盖 this 指向问题、内存泄漏风险及注意事项,帮助开发者正确理解和使用闭包。

JavaScript 闭包基础与原理

理解 JavaScript 闭包之前,首先需要掌握变量作用域链的基本概念。

变量的作用域主要分为全局变量和局部变量两类。未在任何函数中定义的变量属于全局变量;而在函数内部定义的变量则为局部变量。需要注意的是,在函数内部定义变量时必须使用 var 关键字,若省略 var,该变量将自动成为全局变量。

在 JavaScript 中,每段代码都关联着一个作用域链,它是一个对象列表或链表,用于定义该代码“作用域”内的变量。顶层代码的作用域仅包含全局变量;对于不包含嵌套函数的普通函数,其作用域链由两个对象组成:一个是包含函数参数和局部变量的对象,另一个是全局变量对象;而对于嵌套函数,其作用域链则包含三个对象:函数自身的参数和局部变量、外部函数的参数和局部变量,以及全局变量。函数可以访问其作用域链上的对象,因此函数内部可以访问全局变量,但反之不成立,即函数外部无法直接访问函数内部的局部变量。


var a=1;
function wai(){
  alert(a);        
  var m=10;
  n=20;
}

wai();          //=> 1; 函数内部可以访问全局变量
alert(m);        //=> error; 外部访问函数局部变量报错  
alert(n);        //=> 20; 函数内部定义的变量未使用var关键字,所以是全局变量,外部可以访问

接下来探讨如何在外部访问函数内部的局部变量。

当需要在函数外部访问其内部局部变量时,可以利用 JavaScript 作用域的特性进行变通处理。具体做法是在函数内部定义一个子函数,由于子函数可以访问父函数的变量,从而实现了对外部访问内部变量的需求。


function wai(){
 var m=10;
  function nei(){
    alert(m); 
 }
  return nei;
}

var f = wai();
nei();              //=> error; nei()函数是一个局部变量,在外部不能访问
f();               //=> 10;

现在正式引入闭包的概念。

前文示例中的 nei() 函数即为一个闭包。根据前述分析,闭包是指能够读取函数内部局部变量的函数,它是定义在函数内部的函数。从本质上看,闭包可以被视为连接函数内部与外部的桥梁。

闭包主要具备两个作用:

第一,如前所述,它可以读取函数内部的变量。

第二,它能使这些局部变量始终保存在内存中,从而实现变量数据的共享。


function wai(){
  var m=99;
  function nei(){
    alert(m);
    m++;
  }
  return nei;
}
      
var f= wai();
f();         //=> 99;
f();         //=> 100;
f();         //=> 101;

在上述实例中,当 wai() 函数执行完毕后,变量 m 仍保留在内存中。此时通过执行 f() 可以读取 m 的值,但如果直接尝试 alert(m),则会报错,因为 m 并非全局变量。

此外,我们还可以向闭包函数传递参数。如下示例所示,定义一个匿名函数并返回一个闭包函数,该闭包函数将传入的参数与匿名函数中的局部变量 i 相加,并使 i 自增。


var wai=(function(){
  var i=0;
  return function(num){
    num+=i;
    alert(num);
    i++;
 }
})();
wai(1);//1
wai(2);//3
wai(3);//5

闭包的应用与常见陷阱

为了更深入地理解闭包的运行机制,我们来看下面的示例。

假设需要定义一个函数,该函数返回一个数组,且数组的每个元素都是一个函数,每个函数执行时会弹出对应的索引值。

我们可能会写出如下代码:


function box(){
 var arr=[];
  for(i=0;i<5;i++){
    arr[i]=function(){return i;}
  }
 return arr;  
}
var a=box();
alert(a);        //=>包含五个函数体的数组
alert(a[0]());     //=> 5;
alert(a[1]());    //=> 5;

运行上述代码会发现,弹出的值均为 5,而非预期的 0, 1, 2, 3, 4。这是因为 i 是存在于内存中的局部变量,当执行 a[0]() 时,i 的值已经变为 5。在整个 box() 函数运行过程中,i 的值不断自增,导致所有闭包共享同一个 i 变量。

解决该问题的方法是利用闭包的特性进行修正。


function box(){
var arr=[];
    for(var i=0;i<5;i++){

         arr[i]=(function(num){
           return function(){return num;}
         })(i);

     }
return arr;     
}

var arr=box();

for(var i=0;i<5;i++){

  alert(arr[i]());//0,1,2,3,4
}

闭包使用的注意事项

在使用闭包时,还需注意以下几点事项。

首先,由于闭包会导致函数中的变量始终保存在内存中,从而造成较大的内存消耗。因此,不能滥用闭包,否则可能引发网页性能问题,在 IE 浏览器中甚至可能导致内存泄露。解决方法是在退出函数之前,将不再使用的局部变量手动删除。

其次,闭包会在父函数外部改变父函数内部变量的值。因此,如果将父函数当作对象(object)使用,将闭包当作其公用方法(Public Method),将内部变量当作其私有属性(private value)时,务必小心,避免随意修改父函数的变量值。

闭包进阶:this 指向与内存管理

最后,通过几个关于闭包的思考题来检验对闭包运行机制的理解。

如果能正确理解以下代码的运行结果,说明已经掌握了闭包的运行机制。

JavaScript 代码示例如下:


var name = "The Window"; 
var object = { 
  name : "My Object", 
  getNameFunc : function(){ 
    return function(){ 
      return this.name;       //=>嵌套函数的this为全局变量或undefined,不会继承父函数的this
    }; 
  } 
}; 
alert(object.getNameFunc()()); //The Window

上述代码输出结果为 “The window”,原因在于嵌套函数中的 this 不会继承父函数的 this,其值为全局变量或 undefined(在 ECMAScript 5 标准下),因此返回的是全局对象的 name 变量。若要使其返回 object 的 name 属性,可参考以下代码:


var name = "The Window"; 
   var object = { 
    name : "My Object", 
    getNameFunc : function(){ 
      var cur=this;
      return function(){ 
        return cur.name;
      }; 
    } 
  }; 
  alert(object.getNameFunc()()); //=》My Object

上述修正代码将父函数 object 的 this 赋值给 cur 变量,使得嵌套函数可以通过 cur 变量访问其属性。

------------------------------------------------------------------------------------------------------ JavaScript 闭包例子


function outerFun()
{
var a=0;
function innerFun()
{
a++;
alert(a);
} 
}
innerFun();  //=>error

上面的代码是错误的。innerFun() 的作用域位于 outerFun() 内部,因此在 outerFun() 外部直接调用它是非法的。

修改后的代码如下,即通过返回内部函数实现闭包:

JavaScript 代码示例:


function outerFun()
{
   var a=0;
  function innerFun()
  {
    a++;
    alert(a);
  }
  return innerFun; //注意这里
}
var obj=outerFun();
obj(); //结果为1
obj(); //结果为2
var obj2=outerFun();
obj2(); //结果为1
obj2(); //结果为2

什么是闭包?

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的闭包。如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存中不会被释放,因为闭包需要它们。

--------------------------------------------------------------------------------------------------------

再来看一个例子。

JavaScript 代码示例:


function outerFun()
{
   var a =0;
   alert(a); 
}
var a=4;
outerFun();   //=> 0
alert(a);      //=> 4

运行结果为 0, 4。因为在函数内部使用了 var 关键字,维护了 a 的作用域在 outFun() 内部。

再看下面的代码:

JavaScript 代码示例:


function outerFun()
{
//没有var 
a =0;
alert(a); 
}
var a=4;
outerFun();    //=> 0
alert(a);      //=> 0

结果为 0, 0。这看起来有些奇怪,原因何在?

作用域链是描述一种路径的术语,沿着该路径可以确定变量的值。当执行 a=0 时,由于没有使用 var 关键字,赋值操作会沿着作用域链查找并修改 var a=4 的值,从而改变了外部变量的值。

作者最新文章