写在最前:本文转自掘金
本文会解答你以下疑问:
- 静态作用域链和动态作用域链的区别
- 为什么会有闭包
- [[scopes]]属性是什么
- 闭包保存什么内容
- 为什么eval性能不好
- eval 什么情况下会创建闭包
在JavaScript里面,函数、块、模块都可以行程作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
静态作用域链
比如下面这段代码
function func(){
const guang = 'guang';
function func2(){
const ssh = 'ssh';
{
function func3(){
const suzhe = 'suzhe';
}
}
}
}
其中有三个变量,三个函数,还有一个块,他们之间的作用域链可以用babel查看一下,用图可视化一下。
作用域链.png
函数和块的作用域内的变量会在作用域(scope)内创建一个绑定(变量名绑定具体的值,也就是binding),然后其余地方可以引用(refer)这个binding,这样就是静态作用域链的变量访问顺序。
为什么叫“静态”呢?
因为这样的浅谈关系是分析代码可以得出来的,不需要运行,这种链的好处是可以直观的知道变量之间的引用关系。
相对的,还有动态作用域链,也就是作用域的引用关系与嵌套关系无关,与执行顺序有关,会在执行的时候动态创建不同函数、块的作用域的引用关系。确定就是不直观,没法静态分析。
所以绝大多数编程语言作用域链设计都是选择静态的顺序。但是,JavaScript除了静态作用域链外,还有一个特点就是函数可以作为返回值。比如:
function func(){
const a = 1;
return function(){
console.log(a)
}
}
const f2 = func()
这就导致一个问题,本来按照顺序创建调用一层层函数,按顺序创建和销毁作用域挺好的,但是如果内层函数返回了或者通过别的暴露出去了,那么外层函数销毁,内层函数却没有销毁,这时候怎么处理作用域,父作用域销不销毁?(比如这里的func调用结束要不要销毁作用域)
不按顺序的函数调用与闭包
比如把上面的代码改造下,返回内部函数,然后在外面调用:
// 示例一
function func() {
const guang = 'guang';
function func2() {
const ssh = 'ssh';
function func3 () {
const suzhe = 'suzhe';
}
return func3;
}
return func2;
}
const func2 = func();
const func3 = func2()
func3();
当调用func2的时候,func已经执行完了,这时候销不销毁?于是JavaScript就设计了闭包的机制。
我们思考一下,要解决静态作用域中的父作用域先于子作用域销毁该如何解决。
首先,父作用域中有很多东西与子函数无关,为啥因为子函数没结束就一直常驻内存。这样肯定有性能问题,所以还是要销毁。但是销毁了父作用域不能影响子函数,所有要再创建个对象,要把子函数内引用(refer)的父作用域的变量打包起来,给子函数打包带走。
怎么让子函数打包带走?
设计个独特的属性,比如[[Scopes]],用这个来放函数打包带走用到的环境。并且这个属性是一个栈,因为函数有子函数,子函数可能还有子函数,每次打包都要放在这个包里,所以就要设计成一个栈结构,就像饭盒有多层一样。
我们所考虑的这个解决方案:销毁父作用域后,把用到的变量抱起来,打包给子函数,放到一个属性上。这就是闭包机制。
我们来实验下闭包的特性:
bibao.png
其实还是有闭包的,最少会包含全局作用域。
但是为啥guang、ssh、suzhe都没有?suzhe是因为不是外部的,只有外部变量才会生成,比如改动下代码,打印下3个变量。
bibao2.png
这时候就有两个包了,但是没有suzhe,因为闭包只保存外部引用。但是js引擎怎么知道函数引用了哪些外部数据呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部引用打包成Closure闭包,加到[[Scopes]]中。
所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打包成Closure,放到[[Scopes]]里
所以上面的函数会在 func3 返回的时候扫描函数内的标识符,把 guang、ssh 扫描出来了,就顺着作用域链条查找这俩变量,过滤出来打包成两个 Closure(因为属于两个作用域,所以生成两个 Closure),再加上最外层 Global,设置给函数 func3 的 [[scopes]] 属性,让它打包带走。
调用 func3 的时候,JS 引擎 会取出 [[Scopes]] 中的打包的 Closure + Global 链,设置成新的作用域链, 这就是函数用到的所有外部环境了,有了外部环境,自然就可以运行了。
为什么eval性能不好
再来思考一个问题: 闭包需要扫描函数内的标识符,做静态分析,那 eval 怎么办,他有可能内容是从网络记载的,从磁盘读取的等等,内容是动态的。用静态去分析动态是不可能没 bug 的。怎么办?
没错,eval 确实没法分析外部引用,也就没法打包闭包,这种就特殊处理一下,打包整个作用域就好了。
给闭包下个定义
闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。
过滤规则:
- 全局作用域不会被过滤
- 其余作用域会根据是否内部有变量被当前函数所引用而过滤掉一些。不是每个返回的子函数都会生成闭包。
- 被引用的作用域也会过滤掉没有引用的binding(变量声明)。只把用到的变量打个包。
闭包的缺点
容易导致内存泄漏









网友评论