解锁 JavaScript 闭包:从神秘背包到编程魔法

76 阅读6分钟

引言

在 JavaScript 的高级编程概念中,闭包是一个无处不在且至关重要的部分。它基于词法作用域链,为开发者提供了强大而灵活的编程能力。通过深入剖析一段具体代码,我们将全面理解闭包的形成机制、工作原理以及其核心价值。

一、代码示例分析

javascript

function foo() {
    var myName = "极客时间";
    let test1 = 1;
    const test2 = 2;
    var innerBar = {
        getName: function () {
            console.log(test1);
            return myName;
        },
        setName: function (newName) {
            myName = newName;
        }
    }
    return innerBar;
}

var bar = foo();
bar.setName("极客邦");
bar.getName;
console.log(bar.getName());

1.1 代码的执行流程与上下文分析

  1. 全局执行上下文创建当脚本开始执行,JavaScript 引擎首先创建全局执行上下文。在此上下文中:

    • 全局变量对象(Global VO) :存储了 foo 函数对象以及 bar 变量(初始值为 undefined)。
    • 作用域链:此时仅包含全局作用域。
  2. 调用 foo(),创建函数执行上下文当执行 var bar = foo() 时,JavaScript 引擎为 foo 创建新的函数执行上下文并进入执行阶段:

    • 变量对象激活为活动对象(AO) :在 foo 的活动对象中,依次完成以下初始化操作:

      • myName:声明并赋值为 "极客时间"
      • test1:通过 let 声明,先进入 “暂时性死区”,随后赋值为 1
      • test2:通过 const 声明并赋值为 2
      • innerBar:声明并赋值为一个包含 getNamesetName 方法的对象。
    • 构建作用域链foo 的作用域链由 foo 自身的活动对象(AO)加上全局变量对象(VO)组成,即 foo 的作用域链 = foo 自身 AO + 全局 VO(外层作用域)。

    • this 绑定:由于 foo 作为普通函数调用,this 指向全局对象(在浏览器环境中为 window)。

  3. **foo 执行完毕,返回 innerBar**当 foo 执行到 return innerBar 时,将 innerBar 对象的引用赋值给全局变量 bar。通常情况下,foo 的函数执行上下文本应出栈销毁,但由于闭包机制的存在,这一过程被阻止。

1.2 闭包的形成:词法作用域与函数 [[Scope]] 属性

  1. 词法作用域的约束词法作用域规定,函数的作用域由其定义时的位置决定,而非调用时的位置。在上述代码中,getName 和 setName 作为嵌套在 foo 内部的函数,它们的词法环境在定义时就与外层 foo 的词法环境紧密相连。这意味着它们天然有权访问 foo 作用域中的变量,如 myNametest1 和 test2
  2. 函数对象的 [[Scope]] 内部属性在 JavaScript 中,每个函数都是一个对象,拥有一个不可直接访问的内部属性 [[Scope]](也被称为 “作用域链”)。当 getName 和 setName 被定义时,它们的 [[Scope]] 属性会捕获并保存当前的作用域链,即 foo 的活动对象(AO)加上全局变量对象(VO)。
  3. 闭包的核心:作用域的 “持久化引用” 当 foo 执行完毕后,其执行上下文虽然从调用栈中出栈,但由于 innerBar 对象(及其包含的 getName 和 setName 函数)被全局变量 bar 引用,getName 和 setName 的 [[Scope]] 属性仍然保留着对 foo 活动对象(AO)的引用。这使得 JavaScript 引擎的垃圾回收机制无法回收 foo 的活动对象,因为存在有效的引用。此时,getName 和 setName 函数与其所引用的 foo 作用域共同构成了闭包。

1.3 闭包的具体表现:变量的访问与修改

  1. bar.setName("极客邦") 的执行当调用 setName 时,JavaScript 引擎为 setName 创建函数执行上下文:

    • 作用域链setName 的作用域链由 setName 自身的活动对象(AO)加上通过 [[Scope]] 获取的 foo 的活动对象(AO),再加上全局变量对象(VO)组成,即 setName 的作用域链 = setName 自身 AO + foo 的 AO(通过 [[Scope]] 获取) + 全局 VO。
    • 变量修改:当执行 myName = newName 时,引擎沿着作用域链向上查找 myName,最终在 foo 的活动对象(AO)中找到并将其值修改为 "极客邦"
  2. bar.getName() 的执行当调用 getName 时:

    • 变量访问:首先在作用域链中找到 test1,执行 console.log(test1) 输出 1
    • 返回结果:继续沿着作用域链向上查找 myName,返回修改后的值 "极客邦"
    • 最终输出:最终控制台输出 1 和 "极客邦"

1.4 闭包的专业定义与特性

根据 ECMAScript 规范,闭包是指一个函数与其词法环境的引用捆绑在一起形成的组合体,该环境包含了这个函数创建时所能访问的所有局部变量。结合上述代码,闭包体现出以下核心特性:

  1. 作用域的延伸:内部函数(如 getName 和 setName)能够访问外层函数(foo)的变量,即便外层函数已经执行完毕。
  2. 变量的私有化foo 的活动对象(AO)中的变量(myNametest1)无法被全局作用域直接访问,只能通过闭包提供的接口(getName 和 setName)进行操作,从而实现了 “私有变量” 的效果。
  3. 状态的保留:闭包能够持久化保存外层作用域的状态,例如 myName 被修改后的值会被保留下来。

二、闭包的深入理解

2.1 闭包基于词法作用域链

闭包的形成依赖于词法作用域链。词法作用域在函数定义时就确定了变量的访问规则,而闭包正是利用了这一特性,使得内部函数在外部调用时,依然能够按照定义时的作用域链访问变量。

2.2 形成条件是函数嵌套函数

函数嵌套是闭包形成的重要条件之一。只有在内部函数定义在外部函数内部时,内部函数才能捕获外部函数的词法环境,进而形成闭包。

2.3 被闭包的函数要在外部可以访问到 return 才能形成闭包

外部对内部函数的引用是闭包形成的关键。在上述代码中,foo 函数返回了包含 getName 和 setName 函数的 innerBar 对象,使得这些内部函数在外部可以被访问到,从而为闭包的形成创造了条件。

2.4 被闭包的函数执行的时候能找到定义它的时候的执行上下文的变量

这是闭包的核心特征之一。就像在 getName 和 setName 函数执行时,它们能够通过作用域链找到 foo 函数定义时的执行上下文的变量,如 myName 和 test1。可以形象地理解为,getName 和 setName 函数背着一个专属背包(闭包),里面装着它们在定义时所能访问到的外部变量(自由变量)。

三、总结

闭包作为 JavaScript 中强大的编程概念,其底层原理基于函数的 [[Scope]] 属性捕获定义时的作用域链,并且外部引用阻止了外层作用域的垃圾回收,从而形成对外部变量的持久引用。通过对上述代码的详细分析,我们清晰地看到了闭包的形成过程及其具体表现,包括作用域的延伸、变量的私有化和状态的保留等特性。闭包的核心价值在于实现变量私有化和状态保留,它是模块化编程、函数柯里化、防抖节流等高级编程技巧的基础,为 JavaScript 开发者提供了更灵活、更强大的编程能力。