学习链接
JavaScript深入之闭包
(👍👍)
现代 JavaScript 教程:变量作用域,闭包
(👍👍👍)
-
闭包的定义,为什么说 JavaScript 中的所有函数都是闭包的
-
关于
[[Environment]]
属性和词法环境原理的技术细节
闭包
闭包定义
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。
在 JavaScript 中,所有函数都是天生闭包的(只有一个例外
new Function
,见
补充
)。
也就是说:JavaScript 中的函数会自动通过隐藏的
[[Environment]]
属性
记住创建它们的词法作用域
,所以它们
都可以访问外部变量
。
本质
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁,
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
每当创建一个函数,闭包就会在函数创建的同时被创建出来。
两个角度
或许从两个角度来看待闭包,就会少很多分歧和误解:
-
从
理论
角度:所有的函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了,都可以访问外部的变量。
-
从
实践
角度:以下函数才算是闭包:- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了外部函数变量
细节概括
在 JavaScript 中,每个运行的函数,代码块
{...}
以及整个脚本,都有一个被称为
词法环境(Lexical Environment)
的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
-
环境记录
(Environment Record)
—— 一个
存储所有局部变量作为其属性
(包括一些其他信息,例如
this
的值)的对象。 -
对
外部词法环境的
引用
,与外部代码相关联。
一个“变量”只是
环境记录
这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
变量声明
当脚本开始运行,词法环境
预先填充了所有声明的变量
。
- 最初,它们处于“未初始化(Uninitialized)”状态。
-
这是一种特殊的内部状态,这意味着
引擎知道变量
,但是在用
let
声明前,不能引用
它。(
暂时性死区
)
函数声明
与变量不同,
函数声明的初始化会被立即完成。
当
创建了一个词法环境
时,函数声明会立即
变为即用型函数
(不像
let
那样直到声明处才可用)。
所有的函数在“诞生”时都会记住创建它们的词法环境。
所有函数都有
名为
[[Environment]]
的隐藏属性
,该属性保存了
对创建该函数的词法环境的引用
。
这就是函数
记住它创建于何处
的方式,与函数被
在哪儿调用无关
。
[[Environment]]
引用在**
函数创建时
被设置
并
永久保存
**。
函数调用
在一个函数运行时,在
调用刚开始时
,会自动**
创建一个新的词法环境
以存储这个调用的局部变量和参数,并且
其外部词法环境引用获取于 函数的
[[Environment]]
属性**。
访问修改变量
当代码要访问一个变量时 —— 首先会
搜索内部词法环境
,然后
搜索外部环境
,然后
搜索更外部的环境
,以此类推,
直到全局词法环境
。
在
变量所在的词法环境
中
更新变量
,这也就意味着多次调用同一内部函数,使用的也是同一个外部函数的变量。
应用场景
任何闭包的使用场景都离不开这两点:
-
创建私有变量
-
延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
-
可以读取函数内部的变量
。 -
可以使变量的值长期保存在内存中,生命周期比较长
。因此不能滥用闭包,否则会造成网页的性能问题。
使用闭包模拟私有方法
:数据内部私有,暴露方法或函数供外面使用。
柯里化函数
:把一个多参数函数转化成一个嵌套的一元函数,便于重用。
缺点
性能
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在
处理速度和内存消耗方面
对脚本
性能具有负面影响
。
内存泄漏
栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域。
-
全局作用域
:只有当页面关闭的时候全局作用域才会销毁 -
私有的作用域
:只有函数执行才会产生
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。
但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
function fn(){
let num = 100;
return function(){
return num * 2;
}
}
const f = fn(); // fn执行形成的这个私有的作用域就不能再销毁了
也就是像上面这段代码,fn函数内部的私有作用域会被一直占用的,发生了内存泄漏。
所谓内存泄漏,指的是
任何对象
在我们
不再拥有或需要
它之后
仍然存在
。
-
闭包
不能滥用
,否则会导致
内存泄露
,影响网页的性能。 -
闭包
使用完后
,要立即
释放资源
,将
引用变量指向
null
。
补充
new Function
现代 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
中直接使用外部变量。不过这样是好事,这有助于降低我们代码出错的可能。并且,从代码架构上讲,显式地使用参数传值是一种更好的方法,并且
避免了与使用压缩程序而产生冲突的问题
。
细节展开
变量
矩形表示环境记录(变量存储),箭头表示外部引用。
此时的词法环境只有一个,
全局词法环境
。全局词法环境
没有外部引用
,所以箭头指向了
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()
的词法环境
,并且
在哪里找到就在哪里修改
。
在
变量所在的词法环境
中
更新变量
。
执行后:
法环境**,并且
其外部词法环境引用获取于
counter.[[Environment]]
。
[外链图片转存中…(img-9s2X2S5D-1657782572537)]
当
counter()
中的代码查找
count
变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是
外部
makeCounter()
的词法环境
,并且
在哪里找到就在哪里修改
。
在
变量所在的词法环境
中
更新变量
。
执行后:
[外链图片转存中…(img-wHJkWP0Q-1657782572539)]