JavaScript 词法作用域、调用对象与闭包机制详解

作者:闭包旅人

时间:2026-10-26

浏览:513

本文深入解析 JavaScript 的词法作用域、调用对象及闭包三者关系。阐明词法作用域在定义时确定,而调用对象在运行时实例化。通过代码示例展示如何利用闭包保存状态、处理异步参数及事件绑定,揭示其作为高级 JS 核心设计模式的基础原理。

文章目录

JavaScript 作用域与闭包基础

JavaScript 的函数作用域、调用对象与闭包之间存在着微妙且紧密的联系。尽管相关论述已不少见,但许多初学者仍感到困惑。本文旨在用通俗易懂的方式,梳理并阐述我对这一机制的理解。


作用域(Scope)

词法作用域的定义与静态特性

JavaScript 函数遵循词法作用域规则,这意味着函数在定义它的作用域内运行,而非在执行(调用)时的作用域中运行。正如《JavaScript 高级程序设计》(俗称犀牛书)所述,关键在于区分“定义时”与“执行时”。函数 A 的定义时刻是执行 `function A(){}` 语句时,而调用时刻则是执行 `A()` 语句时。明确区分这两个概念至关重要。


所谓词法作用域(后文简称“作用域”,除非特别说明),是一个抽象概念,本质上是“范围”(scope)。函数的作用域即其被定义时所处的外部范围,该范围包含了外层变量属性,并被设定为该函数的一个内部状态。对于全局函数,其定义时的全局范围被设为其内部状态;对于嵌套函数,其被嵌套的外层函数范围被设为其内部状态。这个“内部状态”实质上构成了作用域链,详见下文。


基于上述定义,函数的作用域在定义时即已确定,因此 JavaScript 的作用域是静态的,词法作用域也被称为静态作用域。


调用对象(Call Object)

调用对象的动态实例化

与静态的作用域不同,函数的调用对象是动态的,仅在函数被调用时实例化。当函数定义完成后,其作用域链已确定。当 JavaScript 解释器调用该函数时,会创建一个新的调用对象并置于作用域链前端。该调用对象包含一个名为 `arguments` 的属性,指向实际的 Arguments 对象(即传入的实际参数)。此外,所有使用 `var` 声明的局部变量也定义在此调用对象中。此时,调用对象位于作用域链头部,局部变量、形式参数及 Arguments 对象均处于该函数范围内,并覆盖作用域链中同名的属性。


作用域、作用域链和调用对象之间的关系

简而言之,作用域是抽象概念,而调用对象是具体实例。


在函数定义时(通常也是外层函数执行时),其确定的作用域链实际上指向的是外层函数的调用对象链。当函数被调用时,其作用域链由定义时确定的作用域链(即外层函数的调用对象链)加上当前实例化的调用对象组成。因此,函数的作用域链本质上就是调用对象链。在函数调用期间,其作用域链(或称调用对象链)是定义时确定的作用域链的超集。


三者的关系可概括为:作用域(抽象范围)通过作用域链连接至调用对象(实例化对象)。


作用域链与调用对象的关系示例

概念较为抽象,以下通过具体示例进行说明。


参考以下代码逻辑:

 
function f(x) {
var g = function () { return x; }
return g;
} 
var g1 = f(1);
alert(g1()); //输出 1
假设我们将全局环境视为一个类似的大匿名函数:
(function() {
//这里是全局范围
})(); 
则上述示例可重构为:
(function() {
function f(x) {
var g = function () { return x; }
return g;
} 
var g1 = f(1);
alert(g1()); //输出 1
})(); 

当全局大匿名函数被定义时,由于没有外层作用域,其作用域链为空。


当全局大匿名函数直接执行时,全局作用域链中仅包含一个“全局调用对象”。


函数 f 被定义,此时 f 的作用域链指向其外层的“全局调用对象”。


当执行 `f(1)` 时,f 的作用域链变为新的“f(1) 调用对象”加上定义时的作用域链,即“f(1) 调用对象 -> 全局调用对象”。


函数 g(此处返回给 g1,故暂称 g1)在 `f(1)` 执行期间被定义,其作用域链继承自外层 `f(1)` 的作用域链,即“f(1) 调用对象 -> 全局调用对象”。


`f(1)` 执行完毕,返回函数 g 的定义赋值给 g1。


当执行 `g1()` 时,g1 的作用域链变为新的“g1 调用对象”加上外层 `f(1)` 的作用域链,即“g1 调用对象 -> f(1) 调用对象 -> 全局调用对象”。


通过此过程,逻辑应当变得清晰。


闭包(Closuer)

闭包机制与应用场景

闭包的一种简单定义是:当嵌套函数在其被嵌套的函数外部被调用时,便形成了闭包。


前文示例即为一个典型的闭包案例。g1 在 `f(1)` 内部定义,却在 `f(1)` 返回后才被执行。这表明闭包的一个效果是:即使嵌套函数 f 返回后,其内部资源也不会被释放。在外部调用 g 函数时,g 仍能访问 f 的内部变量。利用这一特性,可以编写出许多优雅且高效的代码。


利用闭包维护状态

例如,若需在一个页面中实现统一的计数器,使用闭包写法可实现如下:


参考以下代码实现:

 
var counter = (function() {
var i = 0;
var fns = {"get": function() {return i;},
"inc": function() {return ++i;}};
return fns;
})(); 
//执行其他操作
counter.inc();
//执行其他操作
counter.inc();
var c_value = counter.get(); //此时 c_value 为 2

通过这种方式,内存中维持了一个变量 i,程序其他部分无法直接操作 i 的值,只能通过 counter 提供的两个方法间接访问。


闭包在异步与事件处理中的应用

在使用 `setTimeout(fn, delay)` 时,无法直接给 `fn` 传递参数,但可以通过闭包将所需参数绑定到 `fn` 内部。


参考以下代码演示:

 
for(var i=0,delay=1000; i< 5; i++, delay +=1000) {
setTimeout(function() {
console.log('i:' + i + " delay:" + delay);
}, delay);
} 

上述代码打印出的结果均为:


i:5 delay:6000


i:5 delay:6000


i:5 delay:6000


i:5 delay:6000


i:5 delay:6000


若改用闭包方式,可轻松绑定传入参数:


参考以下修正代码:

 
for(var i=0, delay=1000; i < 5; i++, delay += 1000) {
(function(a, _delay) {
setTimeout(function() {
console.log('i:'+a+" delay:"+_delay);
}, _delay);
})(i, delay);
} 

此时输出结果为:


i:0 delay:1000


i:1 delay:2000


i:2 delay:3000


i:3 delay:4000


i:4 delay:5000


闭包的另一个常见应用场景是事件回调函数的绑定。原理相同:绑定的函数句柄无法直接传参,但可通过闭包将参数绑定进去。


总结

核心概念总结

函数的词法作用域与作用域链是两个不同的概念:词法作用域是抽象的范围概念,而作用域链是实例化的调用对象链。


函数在被定义时,通常也是其外层函数正在执行的时刻。


函数在被定义时,其词法作用域即已确定,但这仍是一个抽象概念,无法被直接实例化。


函数在被定义时还确定了一件事:即其外层函数的作用域链,这是一个具体的实例化对象。


当函数被多次调用时,其作用域链每次都是不同的。


闭包功能强大。正如《JavaScript 高级程序设计》所言,理解这些概念后,你便可自称为高级 JavaScript 程序员。因为熟练掌握这些概念,能够灵活运用 JavaScript 的多种设计模式。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章