JavaScript作用域与闭包核心机制详解

作者:异步漫步时间:2026-10-25 08:24:05浏览:516

本文深入解析JavaScript的作用域机制,区分LHS与RHS变量查询方式,阐述词法作用域与函数提升规则。重点讲解闭包的形成条件、作用域链查找过程,以及通过IIFE和let解决循环中setTimeout闭包陷阱的多种方案。

一、作用域

作用域本质上是依据名称查找变量的一套规则体系。在JavaScript中,查找变量主要涉及两种操作:LHS(左侧赋值)和RHS(右侧获取)。

LHS(Left Hand Side)大致对应于给变量赋值的操作,即出现在赋值符号(=)左侧的变量;RHS(Right Hand Side)则指获取变量的源值,通常出现在赋值符号(=)的右侧。

通过具体代码示例可以更清晰地理解这一概念,例如执行语句 a = 10; 和 a = b;。

在语句 var a 中,执行的是LHS查询,旨在查找当前作用域内是否存在变量a以进行赋值。若存在,则直接赋值;若不存在,在非严格模式下,JavaScript引擎会自动创建全局变量a并完成赋值;而在严格模式下,引擎将抛出ReferenceError错误。

对于语句 a = b;,对变量b执行的是RHS查询,目的是获取b的值。若查询成功,则进行赋值;若查询失败,则抛出ReferenceError错误。若查询成功但后续操作不当,例如将普通变量b当作函数调用(b()),则会抛出TypeError错误。

JavaScript采用的是词法作用域(Lexical Scoping)。这意味着变量的查找范围在代码编写时,由变量和函数在源代码中的物理位置决定,而非由函数调用的顺序决定。

以下示例展示了词法作用域的工作方式:

var num = 1;
function a() {
    console.log(num); // 1
}
function b() {
    var num = 2;
    a();
}
b();

当按顺序执行 b() 时,函数b内部定义了一个值为2的变量num。随后在b中调用a函数,此时引擎进入a函数的作用域进行查找(而非在b的作用域内),最终找到全局变量 num = 1。

不同作用域之间相互独立,互不影响。因此,在不同作用域中重复定义同名变量不会引发错误。

具体示例如下:

var i = 1;
var j = 2;
function a(){
    var i = 3;
    var j = 4;
    console.log(i,j); // 3,4
}
function b(){
    var i = 5;
    var j = 6;
    console.log(i,j); // 5,6
}
console.log(i,j);// 1,2
a();
b();

在for循环中,使用var声明的变量会泄露到全局作用域,且其值为循环结束后的最终值;而使用let声明的变量会形成块级作用域,每次迭代都会将值重新绑定到新的块作用域中。

for(var i = 0;i<5;i++){
    console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
 
for(let i = 0;i<5;i++){
    console.log(i); // 0 1 2 3 4
}
console.log(i); // ReferenceError

每个作用域都会发生变量提升(Hoisting)现象。其中,函数声明会被提升,但函数表达式不会被提升。

b() // TypeErrorn() // ReferenceError,因为不会变量提升// 函数声明function a(){    console.log(num); // undefined    var num = 1;}// 函数表达式var b = function n(){    console.log('b');}

在提升过程中,函数声明优先于变量声明被提升。

函数声明优先提升可理解为函数的优先级高于变量,因此若存在重复声明,变量声明会被函数声明覆盖。不过,后续的函数声明可以覆盖之前的函数声明。

b() // TypeError
n() // ReferenceError,因为不会变量提升
// 函数声明
function a(){
    console.log(num); // undefined
    var num = 1;
}
// 函数表达式
var b = function n(){
    console.log('b');
}

二、闭包

闭包是代码基于词法作用域书写时的必然产物。

无论函数在何处执行,它都会在其定义时的代码位置(即函数本身所在的作用域)进行作用域链的查找。当函数执行这种作用域查找时,便形成了闭包。

另一种理解方式是,当函数作为值被传递时(例如作为参数传递或作为返回值返回),就会形成闭包。

具体示例如下:

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}
var a = 3;
var baz = foo();
baz(); // 2 

当foo函数执行并返回bar函数时,若bar函数在全局作用域中执行,它会沿着bar函数定义时的位置开始进行RHS查找变量a的值,此时便产生了闭包。严格来说,闭包产生于函数在其定义作用域之外执行的情况。对于IIFE(立即执行函数)而言,虽然它创建了闭包结构,但由于其执行作用域即为定义作用域,并未真正利用闭包跨越作用域的特性,因此可以说它创建了闭包但未使用闭包的功能。

闭包的核心功能在于:当函数在其定义作用域之外执行时,它会沿着定义时的作用域链进行变量查找。

var a = 1;
// 立即执行函数
(
    function(){
        console.log(a);
     }
)();

2. 循环与闭包的应用

观察以下代码示例:

for(var i = 0;i<3;i++){
    setTimeout(()=>{
        console.log(i);
    },i*500)
}

输出结果为 3 3 3,而非预期的 0 1 2。这是因为在for循环结束时,setTimeout才开始执行。在执行时进行变量i的RHS查询,而变量i只能在全局作用域中找到,此时i的值已是循环退出后的值 i = 3。

这可能与预期结果存在差异,若要输出 0 1 2,应如何操作?

(1). 尝试使用IIFE形成闭包是否可行?

for (var i = 0; i < 3; i++) {
    (function () {
        setTimeout(() => {
            console.log(i);
        }, i*500)
    })()
}

结果依然是 3 3 3。因为虽然IIFE形成了闭包结构,但在该闭包内部并未包含变量i的值,引擎仍会沿作用域链向上查找至全局作用域中的 i = 3。

(2). 在IIFE中使用临时变量保存i值或进行传参

for (var i = 0; i < 3; i++) {
    (function () {
        var j = i;
        setTimeout(() => {
            console.log(j);
        }, i*500)
    })()
}
 
// 或
for (var i = 0; i < 3; i++) {
    (function (i) {
        setTimeout(() => {
            console.log(i);
        }, i*500)
    })(i)
}

结果为 0 1 2。在第一个for循环的闭包中引入了变量j,在第二个for循环中通过形参i接收值,这样变量查找就不会到达全局作用域。

(3). 使用let生成块级作用域

for(let i = 0;i<3;i++){
    setTimeout(()=>{
        console.log(i);
    },i*500)
}

结果为 0 1 2。let声明的变量被包含在块级作用域中,且作为循环变量时,每次迭代都会声明一个新变量并保存当前值。当访问变量时,得益于闭包机制,可以获取每次迭代的对应值。

总结

本文就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!

作者最新文章