学习链接
JavaScript深入之闭包(👍👍👍)
现代 JavaScript 教程:变量作用域,闭包(👍👍👍)
- 闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的
- 以及可能的关于
[[Environment]]属性和词法环境原理的技术细节
闭包
闭包定义
闭包 closure 是 1964 年提出来的一个计算机编程的概念,按照当时论文《表达式的机器执行》的表述,它的定义包含控制和环境两个部分,在 JavaScript 中,以函数能够访问其定义时的环境中变量的方式得以实现。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。
在 JavaScript 中,所有函数都是天生闭包的(只有一个例外 new Function,见 补充)。
也就是说:JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的词法作用域,所以它们都可以访问外部变量。
本质
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁,
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
每当创建一个函数,闭包就会在函数创建的同时被创建出来。
两个角度
或许从两个角度来看待闭包,就会少很多分歧和误解:
- 从理论角度:所有的函数都是闭包。
因为它们都在创建的时候就将上层上下文的数据保存起来了,都可以访问外部的变量。 - 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了外部函数变量
细节概括
在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
- 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this的值)的对象。 - 对 外部词法环境的引用,与外部代码相关联。
一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
变量声明
当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。
- 这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let声明前,不能引用它。(暂时性死区)
函数声明
与变量不同,函数声明的初始化会被立即完成。
当创建了一个词法环境时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
所有的函数在“诞生”时都会记住创建它们的词法环境。
所有函数都有名为 **[[Environment]]** 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。
函数调用
在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数,并且其外部词法环境引用获取于 函数的 **[[Environment]]** 属性。
访问修改变量
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
在变量所在的词法环境中更新变量,这也就意味着多次调用同一内部函数,使用的也是同一个外部函数的变量。
细节展开
变量
矩形表示环境记录(变量存储),箭头表示外部引用。

此时的词法环境只有一个,全局词法环境。全局词法环境没有外部引用,所以箭头指向了 null。
- 当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。
- 这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let声明前,不能引用它。(暂时性死区)
- 然后
let phrase定义出现了。它尚未被赋值,值为undefined。此时就可以使用变量了。 phrase被赋予了一个值。phrase的值被修改。
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
词法环境是一个规范对象
“词法环境”是一个规范对象(specification object):它仅仅是存在于 编程语言规范 中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
函数声明
一个函数其实也是一个值,就像变量一样。
不同之处在于函数声明的初始化会被立即完成。
当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
这就是为什么我们可以在(函数声明)的定义之前调用函数声明。

正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)...。
内部和外部的词法环境
在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

在这个函数调用期间,总共有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say的当前执行相对应。它具有一个单独的属性:name,函数的参数。 - 外部词法环境是全局词法环境。它具有
phrase变量和函数本身。
内部词法环境引用了 outer。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
- 对于
name变量,当say中的alert试图访问name时,会立即在内部词法环境中找到它。 - 当它试图访问
phrase时,然而内部没有phrase,所以它顺着对外部词法环境的引用找到了它。
返回函数
尚未运行内部函数

在每次 makeCounter() 调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter 运行时的变量。
在执行 makeCounter() 的过程中创建了一个仅占一行的嵌套函数:return count++。此时尚未运行它,仅创建了它。

所有的函数在“诞生”时都会记住创建它们的词法环境。
所有函数都有名为 **[[Environment]]** 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
因此,counter.[[Environment]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。
开始运行内部函数
当调用 **counter()** 时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 **counter.[[Environment]]**。

当 counter() 中的代码查找 count 变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 **makeCounter()** 的词法环境,并且在哪里找到就在哪里修改。
在变量所在的词法环境中更新变量。
执行后:

应用场景
任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
- 可以读取函数内部的变量。
- 可以使变量的值长期保存在内存中,生命周期比较长。因此不能滥用闭包,否则会造成网页的性能问题。
使用闭包模拟私有方法:数据内部私有,暴露方法或函数供外面使用。
柯里化函数:把一个多参数函数转化成一个嵌套的一元函数,便于重用。
缺点
性能
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
内存泄漏
栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域。
- 全局作用域:只有当页面关闭的时候全局作用域才会销毁
- 私有的作用域:只有函数执行才会产生
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。
但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
function fn(){let num = 100;return function(){return num * 2;}}const f = fn(); // fn执行形成的这个私有的作用域就不能再销毁了
也就是像上面这段代码,fn函数内部的私有作用域会被一直占用的,发生了内存泄漏。
所谓内存泄漏,指的是任何对象在我们不再拥有或需要它之后仍然存在。
- 闭包不能滥用,否则会导致内存泄露,影响网页的性能。
- 闭包使用完后,要立即释放资源,将引用变量指向
**null**。
补充
现代 JavaScript 教程:”new Function” 语法
通常,闭包是指使用一个特殊的属性 [[Environment]] 来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境。
但是如果我们使用 **new Function** 创建一个函数,那么该函数的 **[[Environment]]** 并不指向当前的词法环境,而是指向全局环境。
因此,此类函数无法访问外部(outer)变量,只能访问全局变量。
function getFunc() {let value = "test";let func = new Function('alert(value)');return func;}getFunc()(); // error: value is not defined
可正常访问全局变量
let value = "test";let func = new Function('alert(value)');func(); // value
语法:
let func = new Function ([arg1, arg2, ...argN], functionBody);
由于历史原因,参数也可以按逗号分隔符的形式给出。
以下三种声明的含义相同:
new Function('a', 'b', 'return a + b'); // 基础语法new Function('a,b', 'return a + b'); // 逗号分隔new Function('a , b', 'return a + b'); // 逗号和空格分隔
用 new Function 创建的函数,它的 [[Environment]] 指向全局词法环境,而不是函数所在的外部词法环境。
因此,我们不能在 new Function 中直接使用外部变量。不过这样是好事,这有助于降低我们代码出错的可能。并且,从代码架构上讲,显式地使用参数传值是一种更好的方法,并且避免了与使用压缩程序而产生冲突的问题。
