深入理解JavaScript闭包:原理、应用与内存机制

作者:纸飞机

时间:2026-10-26

浏览:490

本文深入解析JavaScript闭包的核心概念,从变量作用域出发,阐述闭包作为读取内部变量的桥梁机制。详细探讨闭包在保持变量持久化、实现私有属性及方法中的应用,并剖析其背后的执行环境、活动对象及垃圾回收机制,帮助开发者掌握闭包原理以编写更安全的代码。

文章目录

变量作用域基础

一、变量作用域概述

理解闭包的前提是掌握JavaScript独特的变量作用域机制。

变量作用域主要划分为全局变量与局部变量两类。

JavaScript允许函数内部直接访问全局变量。


var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

反之,函数外部的代码无法直接访问函数内部的局部变量。


  function f1(){
    var n=999;
  }

  alert(n); // error

在函数内部声明变量时必须使用var命令,否则将意外创建全局变量。


  function f1(){
    n=999;
  }

  f1();

  alert(n); // 999

从外部读取局部变量的方法

二、外部读取局部变量的方法

虽然常规情况下无法从外部获取局部变量,但可通过特定技巧实现。

该方法的核心是在函数内部定义另一个函数。


  function f1(){

    n=999;

    function f2(){
      alert(n); // 999
    }

  }

当函数f2嵌套于f1内部时,f1的所有局部变量对f2可见,反之则不可见,这构成了JavaScript特有的“链式作用域”(chain scope)。

子对象可逐级向上查找父对象的变量,因此父对象变量对子对象可见,但子对象变量对父对象不可见。

若将f2作为f1的返回值,即可在f1外部访问其内部变量。


  function f1(){

    n=999;

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

闭包的概念与本质

三、闭包定义

前述代码中的f2函数即为闭包的实例。

专业文献对“闭包”(closure)的定义抽象难懂,通俗理解即为能读取其他函数内部变量的函数。

鉴于仅函数内部的子函数能读取局部变量,闭包可简单定义为“定义在函数内部的函数”。

本质上,闭包是连接函数内部与外部的桥梁。

闭包的主要用途

四、闭包的主要用途

闭包主要有两大用途:一是读取函数内部变量,二是使变量值持续保留在内存中。

具体原理可通过以下代码示例理解。


  function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

示例中result为闭包f2,两次运行结果分别为999和1000,证明f1的局部变量n始终存在于内存中,未被调用后清除。

因f1是f2的父函数,且f2被赋给全局变量,导致f2常驻内存,进而依赖f1也常驻内存,避免被垃圾回收机制(garbage collection)回收。

代码中“nAdd=function(){n+=1}”未使用var,故nAdd为全局变量,其值为匿名函数(anonymous function)。

该匿名函数本身也是闭包,使nAdd充当setter,允许在外部操作内部局部变量。

使用闭包的注意事项

五、使用闭包的注意事项

1)闭包导致变量常驻内存,消耗大量内存,滥用可能引发网页性能问题或IE内存泄露。解决策略是在退出函数前删除不使用的局部变量。

2)闭包会修改父函数内部变量。若将父函数视为对象、闭包视为公共方法(Public Method)、内部变量视为私有属性(private value),需谨慎操作。

避免随意改变父函数内部变量的值。

闭包运行机制思考

六、思考题解析

理解以下代码的运行结果,即掌握闭包运行机制。


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

JavaScript闭包示例分析


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

原代码错误在于innerFun()的作用域位于outerFun()内部,在外部调用属于非法操作。

修正后的代码体现了闭包的正确用法。


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

闭包的概念解析

当内部函数在定义它的作用域外部被引用时,即创建闭包。若内部函数引用了外部函数的变量,外部函数调用完毕后,这些变量因闭包需求而不会释放。

进一步示例分析


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

输出结果为0,4,因函数内部使用var关键字将a的作用域限制在outFun()内部。

另一段代码示例


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

输出结果为0,0,原因分析如下。

作用域链是确定变量值的路径术语。执行a=0时,因未使用var,赋值操作沿作用域链至var a=4并修改其值。

深入理解闭包原理

若对JavaScript闭包理解不足,可参考转载文章:(转载:http://www.felixwoo.com/archives/247)

一、闭包定义详解

官方定义:闭包是包含变量及绑定环境的表达式(通常为函数),变量亦是其一部分。该定义过于学术,通俗而言:JavaScript中所有function均为闭包,嵌套function产生的闭包更为强大且常见。示例代码如下:


function a() { 
 var i = 0; 
 function b() { alert(++i); } 
 return b;
}
var c = a();
c();

该代码具备两个特征:

1、函数b嵌套于函数a内部;

2、函数a返回函数b。

引用关系图示如下:

变量作用域基础

执行var c=a()后,变量c指向函数b,执行c()弹出窗口显示i值(首次为1)。此过程创建了闭包,因函数a外的变量c引用了函数a内的函数b。

当函数a的内部函数b被函数a外的变量引用时,即创建闭包。

更透彻的解释:在构造函数体内定义目标对象的方法函数,该方法函数引用外层函数体中的临时变量。只要目标对象存在,其方法即可间接保持原构造函数体中的临时变量值。即使构造函数调用结束,临时变量名消失,仍可通过方法访问该值。再次调用构造函数生成新对象和方法,新临时变量对应新值,与旧值独立。

二、闭包的作用

简言之,闭包作用在于函数a执行返回后,阻止JavaScript垃圾回收机制(GC)回收a的资源,因内部函数b的执行依赖a中的变量。此描述虽不严谨,但直观体现了闭包作用,理解需循序渐进。

示例中,因闭包存在,函数a返回后,a中的i始终存在,每次执行c(),i自加1后alert输出。

若a不返回函数b,则a执行完后,b未被外界引用,仅被a引用,a又被b引用,二者互相引用且不被外界引用,将被GC回收。(关于JavaScript垃圾回收机制后续详述)

闭包内部的微观世界

三、闭包内部微观机制

深入理解闭包及函数a与嵌套函数b的关系,需引入以下概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行过程为例阐述。

  1. 定义函数a时,JavaScript解释器将a的作用域链(scope chain)设置为定义时a所在的“环境”,若a为全局函数,则scope chain仅含window对象。
  2. 执行函数a时,a进入相应的执行环境(excution context)。
  3. 创建执行环境时,首先为a添加scope属性,即a的作用域,值为第1步中的scope chain。即a.scope等于a的作用域链。
  4. 随后执行环境创建活动对象(call object)。活动对象是具有属性的对象,无原型且不能通过JavaScript代码直接访问。创建后,将活动对象添加至a的作用域链顶端。此时a的作用域链包含两个对象:a的活动对象和window对象。
  5. 下一步在活动对象上添加arguments属性,保存调用函数a时传递的参数。
  6. 最后将函数a的所有形参和内部函数b的引用添加至a的活动对象。此步完成函数b的定义,故如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。

至此,函数a从定义到执行的步骤完成。a返回函数b的引用给c,又因函数b的作用域链包含对函数a的活动对象的引用,即b可访问a中定义的所有变量和函数。函数b被c引用,b又依赖a,因此a在返回后不会被GC回收。

函数b执行时亦遵循上述步骤。因此,执行时b的作用域链包含3个对象:b的活动对象、a的活动对象和window对象,如下图所示:

变量作用域基础

如图所示,在函数b中访问变量时,搜索顺序为:

  1. 先搜索自身的活动对象,若存在则返回,否则继续搜索函数a的活动对象,依次查找直至找到。
  2. 若函数b存在prototype原型对象,则在查完自身活动对象后先查找自身的原型对象,再继续查找。此即JavaScript中的变量查找机制。
  3. 若整个作用域链上均无法找到,则返回undefined。

小结:本段提及两个重要概念:函数的定义与执行。文中指出函数的作用域在定义函数时已确定,而非执行时确定(参看步骤1和3)。用代码说明此问题:


function f(x) { 
 var g = function () { return x; }
 return g;
}
var h = f(1);
alert(h()); 

该代码中变量h指向f中的匿名函数(由g返回)。

  • 假设函数h的作用域在执行alert(h())时确定,此时h的作用域链为:h的活动对象->alert的活动对象->window对象。
  • 假设函数h的作用域在定义时确定,即h指向的匿名函数在定义时已确定作用域。则执行时,h的作用域链为:h的活动对象->f的活动对象->window对象。

若第一种假设成立,输出值为undefined;若第二种假设成立,输出值为1。

运行结果证明第2个假设正确,说明函数的作用域确实在定义函数时已确定。

闭包的实际应用场景

四、闭包的应用场景 保护函数内变量安全。以最初例子为例,函数a中i仅函数b可访问,无法通过其他途径访问,从而保护i的安全性。

  1. 在内存中维持变量。如前例,因闭包,函数a中i一直存在于内存中,故每次执行c(),均给i自加1。
  2. 通过保护变量安全实现JS私有属性和私有方法(不能被外部访问) 私有属性和方法在Constructor外无法被访问
  3. 
    function Constructor(...) { 
     var that = this; 
     var membername = value; 
     function membername(...) {...}
    }

以上3点为闭包最基本的应用场景,许多经典案例均源于此。

JavaScript垃圾回收机制

五、JavaScript的垃圾回收机制

在JavaScript中,若对象不再被引用,则该对象将被GC回收。若两个对象互相引用且不再被第3者引用,则这两个互相引用的对象也将被回收。因函数a被b引用,b又被a外的c引用,故函数a执行后不会被回收。

六、结语

理解JavaScript的闭包是迈向高级JS程序员的必经之路,理解其解释和运行机制方能写出更为安全和优雅的代码。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章