内容提要
本文讲解JavaScript闭包:变量是词法环境对象的属性,函数创建时通过隐藏属性[[Environment]]记录所在环境,调用时新建词法环境并沿外部引用链查找变量。因此函数访问的是外部变量的当前值,且所有函数都是闭包。文中还介绍了嵌套函数、计数器、代码块作用域、IIFE及垃圾回收机制。
延伸解读
闭包与内存:引用链决定生命周期
文章指出,词法环境对象通常随函数运行结束而清理,但若存在内嵌函数且其[[Environment]]引用了该环境,则环境会保留在内存中。例如makeCounter返回的函数持续引用count,导致count所在环境无法释放。这解释了闭包可能引起的内存驻留,也说明为何将闭包置为null后内存才可能回收。
所有函数都是闭包:定义与实用意义的区分
文章强调,从技术定义看,JavaScript中所有函数都是闭包,因为它们都通过隐藏属性[[Environment]]记录创建时的词法环境。但日常所说的“实用意义”上的闭包,通常指函数被返回或传递到其他作用域后,仍能访问原外部变量的情况。理解这一区分有助于准确回答面试题,避免混淆概念。
块级作用域与IIFE:历史方案与现代替代
文章提到,代码块{...}也会创建词法环境,使let/const变量仅限块内访问。在ES6之前,没有块级作用域,常用IIFE(立即执行函数表达式)来隔离变量,避免污染全局。IIFE需用括号包裹函数表达式,否则会被解析为函数声明而报错。如今可直接用代码块实现类似隔离。
引擎优化:调试时变量可能“消失”
文章提醒,虽然理论上闭包会保留所有外部变量,但JavaScript引擎(如V8)可能优化掉未使用的变量以节省内存。这导致在调试器中无法查看这些变量,例如在Chrome DevTools中,闭包内未引用的value可能不可见。这不是bug,而是引擎特性,实际行为可能随版本变化。
Q&A
JavaScript 中的闭包到底是什么?为什么说所有函数都是闭包?
闭包是一个记录并可访问外层变量的函数。在 JavaScript 中,所有函数都是闭包,因为每个函数在创建时都会获得一个隐藏属性 [[Environment]],它指向函数创建时的词法环境。因此函数总能通过这个属性访问其外部变量。
函数访问外部变量时,获取的是变量创建时的值还是调用时的最新值?
函数获取的是外部变量的当前值(最新值)。因为函数在调用时会创建新的词法环境,并沿外部引用链查找变量,找到的是变量在那一刻的值。例如,如果全局变量 name 先被赋值为 'John',然后改为 'Pete',之后调用 sayHi() 会输出 'Pete'。
词法环境(Lexical Environment)是什么?它包含哪些部分?
词法环境是 JavaScript 中与运行中的函数、代码块或整个脚本关联的对象。它包含两部分:环境记录(Environment Record),一个存储所有局部变量作为属性的对象;以及对外部词法环境的引用(outer),通常指向外层代码的词法环境。变量实际上是环境记录中的属性。
嵌套函数如何访问外部变量?请举例说明。
嵌套函数通过作用域链访问外部变量:当嵌套函数执行时,会创建自己的词法环境,如果其中没有需要的变量,就通过 outer 引用向外层词法环境查找,直到全局环境。例如,makeCounter 内部的函数可以访问外部变量 count,每次调用递增并返回新值。
多个计数器函数(如通过多次调用 makeCounter 创建)之间的 count 变量是共享的还是独立的?
它们是独立的。每次调用 makeCounter() 都会创建一个新的词法环境,其中包含自己的 count 变量。因此,不同的计数器函数拥有各自的 count,互不影响。例如,counter1 和 counter2 分别递增,输出 0,1 和 0。
什么是 IIFE?它有什么作用?
IIFE 是立即执行函数表达式(Immediately-Invoked Function Expression)。它用于创建一个立即执行的函数,拥有自己的私有变量,避免污染全局作用域。写法通常是用括号包裹函数表达式,然后立即调用,如 (function() { ... })()。
闭包中的变量何时会被垃圾回收?
词法环境对象与普通值一样,当它变得不可达(unreachable)时会被垃圾回收。通常,函数执行完毕后,其词法环境如果没有被其他函数引用,就会被清除。但如果存在嵌套函数,且该函数被外部引用,则其 [[Environment]] 会保持外部词法环境存活,直到该函数也不再被引用。