引言
在 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 代码的执行流程与上下文分析
-
全局执行上下文创建当脚本开始执行,JavaScript 引擎首先创建全局执行上下文。在此上下文中:
- 全局变量对象(Global VO) :存储了
foo函数对象以及bar变量(初始值为undefined)。 - 作用域链:此时仅包含全局作用域。
- 全局变量对象(Global VO) :存储了
-
调用
foo(),创建函数执行上下文当执行var bar = foo()时,JavaScript 引擎为foo创建新的函数执行上下文并进入执行阶段:-
变量对象激活为活动对象(AO) :在
foo的活动对象中,依次完成以下初始化操作:myName:声明并赋值为"极客时间"。test1:通过let声明,先进入 “暂时性死区”,随后赋值为1。test2:通过const声明并赋值为2。innerBar:声明并赋值为一个包含getName、setName方法的对象。
-
构建作用域链:
foo的作用域链由foo自身的活动对象(AO)加上全局变量对象(VO)组成,即foo的作用域链 =foo自身 AO + 全局 VO(外层作用域)。 -
this绑定:由于foo作为普通函数调用,this指向全局对象(在浏览器环境中为window)。
-
-
**
foo执行完毕,返回innerBar**当foo执行到return innerBar时,将innerBar对象的引用赋值给全局变量bar。通常情况下,foo的函数执行上下文本应出栈销毁,但由于闭包机制的存在,这一过程被阻止。
1.2 闭包的形成:词法作用域与函数 [[Scope]] 属性
- 词法作用域的约束词法作用域规定,函数的作用域由其定义时的位置决定,而非调用时的位置。在上述代码中,
getName和setName作为嵌套在foo内部的函数,它们的词法环境在定义时就与外层foo的词法环境紧密相连。这意味着它们天然有权访问foo作用域中的变量,如myName、test1和test2。 - 函数对象的 [[Scope]] 内部属性在 JavaScript 中,每个函数都是一个对象,拥有一个不可直接访问的内部属性
[[Scope]](也被称为 “作用域链”)。当getName和setName被定义时,它们的[[Scope]]属性会捕获并保存当前的作用域链,即foo的活动对象(AO)加上全局变量对象(VO)。 - 闭包的核心:作用域的 “持久化引用” 当
foo执行完毕后,其执行上下文虽然从调用栈中出栈,但由于innerBar对象(及其包含的getName和setName函数)被全局变量bar引用,getName和setName的[[Scope]]属性仍然保留着对foo活动对象(AO)的引用。这使得 JavaScript 引擎的垃圾回收机制无法回收foo的活动对象,因为存在有效的引用。此时,getName和setName函数与其所引用的foo作用域共同构成了闭包。
1.3 闭包的具体表现:变量的访问与修改
-
bar.setName("极客邦")的执行当调用setName时,JavaScript 引擎为setName创建函数执行上下文:- 作用域链:
setName的作用域链由setName自身的活动对象(AO)加上通过[[Scope]]获取的foo的活动对象(AO),再加上全局变量对象(VO)组成,即setName的作用域链 =setName自身 AO +foo的 AO(通过[[Scope]]获取) + 全局 VO。 - 变量修改:当执行
myName = newName时,引擎沿着作用域链向上查找myName,最终在foo的活动对象(AO)中找到并将其值修改为"极客邦"。
- 作用域链:
-
bar.getName()的执行当调用getName时:- 变量访问:首先在作用域链中找到
test1,执行console.log(test1)输出1。 - 返回结果:继续沿着作用域链向上查找
myName,返回修改后的值"极客邦"。 - 最终输出:最终控制台输出
1和"极客邦"。
- 变量访问:首先在作用域链中找到
1.4 闭包的专业定义与特性
根据 ECMAScript 规范,闭包是指一个函数与其词法环境的引用捆绑在一起形成的组合体,该环境包含了这个函数创建时所能访问的所有局部变量。结合上述代码,闭包体现出以下核心特性:
- 作用域的延伸:内部函数(如
getName和setName)能够访问外层函数(foo)的变量,即便外层函数已经执行完毕。 - 变量的私有化:
foo的活动对象(AO)中的变量(myName、test1)无法被全局作用域直接访问,只能通过闭包提供的接口(getName和setName)进行操作,从而实现了 “私有变量” 的效果。 - 状态的保留:闭包能够持久化保存外层作用域的状态,例如
myName被修改后的值会被保留下来。
二、闭包的深入理解
2.1 闭包基于词法作用域链
闭包的形成依赖于词法作用域链。词法作用域在函数定义时就确定了变量的访问规则,而闭包正是利用了这一特性,使得内部函数在外部调用时,依然能够按照定义时的作用域链访问变量。
2.2 形成条件是函数嵌套函数
函数嵌套是闭包形成的重要条件之一。只有在内部函数定义在外部函数内部时,内部函数才能捕获外部函数的词法环境,进而形成闭包。
2.3 被闭包的函数要在外部可以访问到 return 才能形成闭包
外部对内部函数的引用是闭包形成的关键。在上述代码中,foo 函数返回了包含 getName 和 setName 函数的 innerBar 对象,使得这些内部函数在外部可以被访问到,从而为闭包的形成创造了条件。
2.4 被闭包的函数执行的时候能找到定义它的时候的执行上下文的变量
这是闭包的核心特征之一。就像在 getName 和 setName 函数执行时,它们能够通过作用域链找到 foo 函数定义时的执行上下文的变量,如 myName 和 test1。可以形象地理解为,getName 和 setName 函数背着一个专属背包(闭包),里面装着它们在定义时所能访问到的外部变量(自由变量)。
三、总结
闭包作为 JavaScript 中强大的编程概念,其底层原理基于函数的 [[Scope]] 属性捕获定义时的作用域链,并且外部引用阻止了外层作用域的垃圾回收,从而形成对外部变量的持久引用。通过对上述代码的详细分析,我们清晰地看到了闭包的形成过程及其具体表现,包括作用域的延伸、变量的私有化和状态的保留等特性。闭包的核心价值在于实现变量私有化和状态保留,它是模块化编程、函数柯里化、防抖节流等高级编程技巧的基础,为 JavaScript 开发者提供了更灵活、更强大的编程能力。