JavaScript 中的闭包及其重要性
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
开发者编写 JavaScript 代码时,其中一个主要特性或许是他们最不了解的。这可能是因为没有人会在编写代码时直接思考或意识到,代码不出错的原因恰恰与这个特性有关。
但这个功能究竟是什么呢?
嗯……这其实算不上一个特性。它是 JavaScript 构建方式以及它“编译”、运行和执行方式的副作用。让我们通过一个例子来深入了解一下。
在浏览器开发者工具中运行以下命令将导致
var age = 14;
function getOlder() {
var age = 14;
age++;
};
getOlder();
console.log(`I am ${age} years old.`); // <-- ???
- 它坏了(🤷)
- 打印
I am 14 years old. - 打印
I am 15 years old.
正确答案是2:I am 14 years old.!但是为什么呢?
解释执行过程
关于抽象语法树 (AST) 以及 JavaScript 的设计理念,有很多重要的信息,本文不会深入探讨,但读者可以这样理解(请查阅参考文献!):
当浏览器内部运行的虚拟机(例如 Chrome 中的 V8)执行代码时,它会解析每个变量的名称。解析变量的过程是必要的,这样在使用已声明和定义的变量时就不会破坏代码。如果代码尝试访问尚未正确定义的函数或变量,它将输出著名的错误信息:
Uncaught ReferenceError: yourVariable is not defined。
手动解析变量
如果命名解析后的结果可访问,则原始代码将转换为大致类似于以下内容:
var global__age = 14;
function global__getOlder() {
var getOlder__age = 14;
getOlder__age++;
};
global__getOlder();
console.log(`I am ${global_age} years old.`); // --> 'I am 14 years old.'
现在输出结果就说得通了,对吧?这个前缀的添加与命名解析时每个变量和方法的闭包I am 14 years old.有关。可以看出,这段代码中有两个闭包:
globalgetOlder
可以看出,getOlder闭包位于global闭包内部,但原始函数内部的变量getOlder()只能在这些括号内访问。
getOlder__age因此,说变量只存在于函数内部更有意义global__getOlder()。一个很好的验证例子是尝试在函数外部记录该变量的值:
var global__age = 14;
function global__getOlder() {
var getOlder__age = 14;
getOlder__age++;
};
global__getOlder();
console.log(`I am ${getOlder__age} years old.`); // --> Error!
结果输出为Uncaught ReferenceError: getOlder__age is not defined,原因是没有变量的名称解析为globalClosure 有效getOlder__age。
那么,Scopes乐队呢?
在创建函数时,闭包的创建方式与作用域相同。闭包内的所有变量和函数都可以被所有子函数访问,但不能被闭包外的函数访问(除非它们像后面将要讨论的那样被暴露出来)。
作用域和闭包几乎是等同的,但后者有一些“超能力”:在闭包内部创建并暴露出来的变量和函数,即使没有作用域,在闭包外部仍然可以正常工作。这两个概念之间的界限非常模糊。
即使这些暴露出来的项依赖于闭包内部的其他变量/函数,但这些变量/函数本身并未暴露出来,这个结论仍然成立。
闭合与内窥镜
以下代码与上述示例几乎相同,仅作少量修改,旨在解释这两个概念之间的区别。
function main() {
var age = 14;
function getOlder() {
age++;
console.log(`I am ${age} years old now.`); // --> 'I am 15 years old.'
};
getOlder();
};
main();
在这个例子中,函数getOlder()会在另一个函数内部被调用main(),并且会打印出I am 15 years old now.结果,对吗?变量age位于main函数的作用域内,可以被函数访问getOlder()。
如果将该函数返回getOlder()到外部“世界”,并像以下示例一样执行 3 次,结果会是什么?
function main() {
var age = 14;
function getOlder() {
age++;
console.log(`I am ${age} years old now.`); // <-- ???
};
return getOlder;
};
var getOlder = main();
getOlder(); // <-- ???
getOlder(); // <-- ???
getOlder(); // <-- ???
- 什么都没有。代码会出错。
- 3次
I am 15 years old now. - 变量的值
age仍将从15增加到16,然后再增加到17。
正确答案是答案 3。
但为什么会发生这种情况呢?
每次创建闭包时,所有变量和函数都会存储在其状态中。即使函数执行完毕main(),相应的闭包 状态仍然存在,并继续存储着变量和函数!
或许最神奇的地方在于:这个age变量被困在main() 闭包内部,无法在外部访问!如果下一部分代码尝试访问该age变量,就会出现前面提到的Uncaught ReferenceError: age is not defined错误,因为该变量在函数外部并不存在main()!
包起来
文中讨论了封闭性和作用域概念之间的一些重要区别:
- 闭包总是存储关于其变量和函数的状态
- 可以通过在创建闭包的函数末尾返回这些变量/函数,来公开部分、全部或不公开任何这些变量/函数。
- 甚至可以在闭包内部重新定义一些同名的外部变量/函数,虚拟机编译器会自动处理,避免运行时错误和名称冲突。
这篇文章对您有用吗?我在解释过程中是否遗漏了什么?请在评论区留言或私信告诉我!

