JavaScript闭包原理与实例详解

作者:异步漫步

时间:2026-10-29

浏览:148

本文深入解析JavaScript闭包的概念、实现原理及常见应用场景。通过具体代码示例,阐明闭包如何保存函数局部变量,并探讨其在Singleton模式、循环变量隔离及私有成员封装中的实际应用,帮助开发者理解闭包机制及其在ECMAScript规范中的底层逻辑。

文章目录

JavaScript闭包概念解析

什么是闭包

闭包(Closure)是JavaScript中一个独特且重要的特性,静态类型语言通常不具备这一概念。尽管听起来复杂,但闭包的核心概念并不晦涩,简而言之,它指的是:

闭包本质上是函数内部局部变量的集合,这些变量在函数执行返回后依然保留在内存中,并未被销毁。

另一种理解方式是,当函数返回后,其内部的“堆栈”并未释放,而是分配在堆内存中。通常在函数内部定义另一个函数时,就会形成闭包。上述第二种定义是对第一种定义的补充,若提取第一定义的主干——“闭包是函数的局部变量集合”,并强调这些变量在函数返回后仍可被访问,这有助于直观理解闭包的含义(注:此为便于理解的通俗定义,非官方标准定义)。

深入理解JavaScript闭包至关重要,本文将通过最简化的示例来阐释这一概念。


function greet(sth){
  return function(name){
    console.log(sth + ' ' + name);
  }
}
//hi darren
greet('hi')('darren');

或者可以写成这样:


var sayHi = greet('hi');
sayHi('darren');

我们需要探讨的核心问题是:为什么内部函数greet能够访问外部变量sth?

其内部运作机制大致如下:

→ 创建全局执行上下文 → 执行var sayHi = greet('hi');语句,此时创建greet函数的执行上下文,变量sth存储在该上下文中。 → 继续执行greet函数体内的语句,返回一个匿名函数。尽管greet函数的执行上下文从调用栈中弹出消失,但变量sth依然存在于内存的某个特定空间。 → 继续执行sayHi('darren');,此时创建sayHi函数的执行上下文,并尝试搜寻变量sth。由于在sayHi自身的上下文中找不到sth,它会沿着作用域链向上查找,最终定位到sth变量所在的内存地址。外层函数就像一个闭包容器,其内部函数可以访问外部函数的变量。

闭包基础示例与原理

一个闭包的简单例子


function buildFunctions(){
  var funcArr = [];
  for(var i = 0; i < 3; i++){
    funcArr.push(function(){console.log(i)});
  }
  return funcArr;
}
var fs = buildFunctions();
fs[0](); //3
fs[1](); //3
fs[2](); //3

以上示例中,为什么输出结果不是预期的0、1、2呢?

这是因为变量i作为闭包变量,其当前值为3,被内部函数共享使用。若要实现预期效果,需要在每次遍历时创建一个独立的上下文,使内部函数不受外部闭包变量的影响。自调用函数(IIFE)可以实现这种独立上下文。


function buildFunctions(){
  var funcArr = [];

  for(var i = 0; i < 3; i++){
    funcArr.push((function(j){
      return function(){
       console.log(j);
      };
    }(i)));
  }

  return funcArr;
}
var fs = buildFunctions();
fs[0](); //0
fs[1](); //1
fs[2](); //2

本文的两个示例分别体现了闭包的两个方面:一是内部函数使用闭包变量;二是将内部函数置于自调用函数中,从而避免受外层闭包变量的影响。

作为局部变量,它们都可以被函数内的代码访问,这一点与静态语言没有区别。闭包的特殊之处在于,局部变量在函数执行结束后,仍然可以被函数外部的代码访问。这意味着函数必须返回一个指向闭包的“引用”,或者将该“引用”赋值给某个外部变量,才能确保闭包中的局部变量被外部代码访问。当然,包含这个引用的实体必须是一个对象,因为在JavaScript中,除了基本数据类型外,其余皆为对象。遗憾的是,ECMAScript标准并未提供直接访问闭包中局部变量的成员或方法。但在ECMAScript中,函数对象内定义的内部函数(inner function)可以直接访问外部函数的局部变量。通过这种机制,我们可以以下方式访问闭包:


function greeting(name) {
   var text = 'Hello ' + name; // local variable
   // 每次调用时,产生闭包,并返回内部函数对象给调用者
   return function () { alert(text); }
}
var sayHello=greeting( "Closure" );
sayHello() // 通过闭包访问到了局部变量text

上述代码的执行结果是弹出“Hello Closure”,因为sayHello()函数在greeting函数执行完毕后,依然能够访问定义在其内部的局部变量text。

这就是传说中的闭包效果。闭包在JavaScript中有多种应用场景和设计模式,例如Singleton(单例模式)、Power Constructor(强力构造函数)等,这些模式都离不开对闭包的使用。

ECMAScript闭包实现模型

ECMAScript闭包模型

ECMAScript是如何实现闭包的呢?想要深入了解的读者可以查阅ECMAScript规范文档,此处仅作简要讲解,内容参考自网络资料。

在ECMAscript脚本的函数运行时,每个函数都关联有一个执行上下文(Execution Context)。这个执行上下文包含三个部分:

词法环境(The LexicalEnvironment) 变量环境(The VariableEnvironment) this绑定

其中第三点this绑定与闭包无关,不在本文讨论范围内。词法环境用于解析函数执行过程中使用的变量标识符。我们可以将词法环境想象成一个对象,该对象包含两个重要组件:环境记录(Environment Record)和外部引用(指针)。环境记录包含函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局上下文中的此引用值为NULL。这种数据结构构成了一个单向链表,每个引用都指向外层的上下文场景。

例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:

 

因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure” 变量环境(The VariableEnvironment)和词法环境的作用基本相似,具体的区别请参看ECMAScript的规范文档。

闭包典型代码示例

闭包的样列

前文已大致介绍了JavaScript闭包的概念及其实现原理。下面通过几个具体示例,帮助大家更深入地理解闭包。共有5个样例,例子来源于JavaScript Closures For Dummies(镜像)。

例子1:闭包中局部变量是引用而非拷贝


function say667() {
  // Local variable that ends up within closure
  var num = 666;
  var sayAlert = function() { alert(num); }
  num++;
  return sayAlert;
}
var sayAlert = say667();
sayAlert()

因此执行结果应该弹出的667而非666。

例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。


function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 666;
  // Store some references to functions as global variables
  gAlertNumber = function() { alert(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}
setupSomeGolbals(); // 为三个全局变量赋值
gAlertNumber(); //666
gIncreaseNumber();
gAlertNumber(); // 667
gSetNumber(12);//
gAlertNumber();//12

例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包


function buildList(list) {
  var result = [];
  for (var i = 0; i < list.length; i++) {
    var item = 'item' + list[i];
    result.push( function() {alert(item + ' ' + list[i])} );
  }
  return result;
}
function testList() {
  var fnlist = buildList([1,2,3]);
  // using j only to help prevent confusion - could use i
  for (var j = 0; j < fnlist.length; j++) {
    fnlist[j]();
  }
}

testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.

例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。


function sayAlice() {
  var sayAlert = function() { alert(alice); }
  // Local variable that ends up within closure
  var alice = 'Hello Alice';
  return sayAlert;
}
var helloAlice=sayAlice();
helloAlice();

执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。

例子5:每次函数调用的时候创建一个新的闭包


function newClosure(someNum, someRef) {
  // Local variables that end up within closure
  var num = someNum;
  var anArray = [1,2,3];
  var ref = someRef;
  return function(x) {
    num += x;
    anArray.push(num);
    alert('num: ' + num +
    '\nanArray ' + anArray.toString() +
    '\nref.someVar ' + ref.someVar);
  }
}
closure1=newClosure(40,{someVar:'closure 1'});
closure2=newClosure(1000,{someVar:'closure 2'});
closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1'
closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2'

闭包在实际开发中的应用

闭包的应用

Singleton 单件:


var singleton = function () {
  var privateVariable;
  function privateFunction(x) {
    ...privateVariable...
  }
  return {
    firstMethod: function (a, b) {
      ...privateVariable...
    },
    secondMethod: function (c) {
      ...privateFunction()...
    }
  };
}();

这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()',如果没有这个'()'就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章