JavaScript闭包原理与实例详解
本文深入解析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访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()',如果没有这个'()'就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
