JavaScript嵌套函数与闭包机制详解

作者:闭包旅人时间:2026-10-26 02:47:04浏览:631

本文深入解析JavaScript中嵌套函数的词法作用域特性,详细阐述作用域链的查找机制。通过对比局部变量与全局变量在闭包中的表现,揭示闭包如何保存外部变量值至内存。文章还探讨了利用函数属性与闭包实现状态保持的区别,强调闭包在避免全局污染方面的优势。

【嵌套函数】

JavaScript嵌套函数与词法作用域

JavaScript支持函数嵌套定义,允许将函数作为数据传递,并在词法作用域的约束下展现出与传统面向对象语言截然不同的特性。


JavaScript的作用域划分基于词法而非动态规则,这意味着函数在定义时所处的作用域中运行,而非在执行时所在的作用域。因此,当嵌套函数与其外层函数共同定义于同一词法作用域时,其行为逻辑是直观且易于理解的。例如以下基础代码:


代码如下:

 
var x = 'global';
function f () {
var x = 'local';
function g() {
alert(x);
} 
g();
} 
f(); // 'local'

当调用函数f时,其作用域链由两部分构成:首先是包含f调用的调用对象,其次是全局对象。查找变量x时,引擎会先在f的调用对象中搜索,若未找到则继续在全局对象中查找。同理,由于g是f的嵌套函数,调用g时其作用域链包含三层:g的调用对象、f的调用对象以及全局对象。函数g旨在输出x的值,因此它会先在g自身的调用对象中查找x。由于g内部未定义x,引擎转而查找外层f的调用对象,从而找到x='local'并输出该值,不再继续向全局对象查找。若f中亦未定义x,则继续向上查找至全局对象,结果为global;若全局对象中也不存在,则返回undefined。


闭包机制与内存保存原理

基于对作用域链的初步理解,我们知道闭包主要有两个常见用途:一是允许访问局部变量,二是将外层作用域中的变量值保留在内存中,防止函数调用结束后立即销毁。


接下来通过一个看似普通的示例,帮助理解闭包为何能将外部变量值保存在内存中。


代码如下:

 
function makeFunc (x) {
return function () {return x++}
} 
var a = [makeFunc(0), makeFunc(1), makeFunc(2)];
alert(a[0]());
alert(a[1]());
alert(a[2]());

执行结果为0,1,2;这看似平淡无奇,却是严格词法作用域的正常表现。每次makeFunc调用结束后,其调用对象会从作用域链中移除,且不再有任何引用,最终被垃圾回收机制处理。详细来说,我们可以这样理解:


每次调用makeFunc时,都会为其创建一个调用对象并加入作用域链。对于makeFunc函数而言,该调用对象包含一个属性x(即函数参数,可视为调用对象的属性)。makeFunc返回一个匿名嵌套函数的引用,随后该匿名函数执行,创建自己的调用对象并加入作用域链。匿名函数返回x的值(注意:匿名函数的调用对象中并未定义x,因此它引用外层函数makeFunc的调用对象来访问x),然后x加1。至此,匿名函数执行完毕,其调用对象从作用域链中移除;接着makeFunc也执行完毕,其调用对象同样被移除。由于makeFunc的调用对象中包含x,x也随之销毁,并未被保存下来。


以上是函数的详细执行过程,请仔细理解后查看以下修改后的代码:


代码如下:

 
var x = 0;
function makeFunc () {
return function () {return x++}
} 
var a = [makeFunc(), makeFunc(), makeFunc()];
alert(a[0]());
alert(a[1]());
alert(a[2]());

现在x是一个全局变量,执行结果为0,1,2;但这一结果与前述示例有所不同。我们仍从作用域链的角度来解析这一结果产生的原因。


同样,每次调用makeFunc时会创建一个调用对象加入作用域链。由于它返回内部嵌套函数的引用,内部嵌套函数开始执行,又创建一个嵌套函数的调用对象加入作用域链。然后返回x的值,这里的关键区别在于:嵌套函数的调用对象中没有x,其外层makeFunc的调用对象中也没有x,因此只能继续向下查找至全局对象。在全局对象中找到x的定义后,正常执行并返回x的值,x加1。随后嵌套函数完毕,调用对象移除;接着makeFunc完毕,调用对象也移除。但由于它们的调用对象中均不包含x,其销毁不会影响x的值。因此,全局变量x的改变就这样被保存下来了。


注意,上述关于访问外层调用对象的描述仅为帮助理解的简化说法。JavaScript实际上并不以这种方式直接访问调用对象,但其定义的属性作为调用对象中作用域链的一部分,依然处于“活跃”状态。此外,如果一个外层函数包含两个或多个嵌套函数且都引用全局对象,那么这些嵌套函数共享同一个全局调用对象,其中一个对全局对象的修改对其他函数也是可见的。


闭包实现与全局污染规避

在JavaScript中,函数是将要执行的代码与执行这些代码的作用域结合而成的综合体。广义而言,我们可以将这种代码与作用域的综合体称为闭包。


  【闭包】

我们偶尔需要编写一个通过调用就能记住变量值的函数。既然了解了作用域,就会知道局部变量很难实现这一目标,因为函数的调用对象在调用后无法持续存在。虽然全局变量可以做到(如前述示例),但这极易导致全局变量污染。既然调用对象无法持久保存,我们是否可以将值保存在调用对象之外?答案是肯定的,一种实现方法是使用函数对象自身的属性来保存。


代码如下:

 
uniqueID = function () {
if (!arguments.callee.id) arguments.callee.id = 0;
return arguments.callee.id ++;
} 
alert(uniqueID()); //0
alert(uniqueID()); //1

如上所示,由于函数本身就是一个对象,利用其自身属性保存值是可行的。但这样做存在一个问题:任何人都可以在任何时候通过uniqueID.id强制访问并修改我们原本保存的值,这是我们不希望看到的。


因此,通常我们使用闭包来实现这一功能,如下所示:


代码如下:

 
_uniqueID = (function(){
var id = 0;
return function () {return id ++}
})(); 
alert(_uniqueID()); //0
alert(_uniqueID()); //1

同样,我们用作用域链来解释结果。注意到_uniqueID本身是一个匿名函数,其内部还有一个匿名嵌套函数。我们直接调用的是_uniqueID(),实际上调用的是_uniqueID内部的嵌套函数。由于该嵌套函数的调用对象中未定义id,它引用外层调用对象中的id并返回,随后id加1。执行完毕后,内层嵌套函数的调用对象移出作用域链。而外层的id并未被销毁,因此得以保存下来。


有人可能会疑惑:不是说调用对象在函数执行完毕后就从作用域链中移除吗?外层匿名函数(function(){})();也是调用完毕了的,应该调用对象也没了才对。


是的,调用对象在当前函数执行完毕后就结束了引用。但这里不要误解了上述_uniqueID()的调用,它并非直接调用外层函数,而是调用嵌套函数。嵌套函数的作用域链包含了外层函数的作用域链。因此,当它的调用对象从作用域链中移除时,仍能访问并改变这条作用域链上其他对象的属性。


闭包本身是一个难以理解但非常有用的概念,希望能对有需要的人提供一些帮助。此外,受限于资历,本人理解也可能存在误差,如发现错误,敬请指正。

作者最新文章