美文网首页js css html
JS的静态作用域链与“动态”闭包链

JS的静态作用域链与“动态”闭包链

作者: 没名字的某某人 | 来源:发表于2022-06-16 13:36 被阅读0次

写在最前:本文转自掘金

本文会解答你以下疑问:

  • 静态作用域链和动态作用域链的区别
  • 为什么会有闭包
  • [[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

其实还是有闭包的,最少会包含全局作用域。
但是为啥guangsshsuzhe都没有?suzhe是因为不是外部的,只有外部变量才会生成,比如改动下代码,打印下3个变量。

bibao2.png

这时候就有两个包了,但是没有suzhe,因为闭包只保存外部引用。但是js引擎怎么知道函数引用了哪些外部数据呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部引用打包成Closure闭包,加到[[Scopes]]中。

所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打包成Closure,放到[[Scopes]]

所以上面的函数会在 func3 返回的时候扫描函数内的标识符,把 guangssh 扫描出来了,就顺着作用域链条查找这俩变量,过滤出来打包成两个 Closure(因为属于两个作用域,所以生成两个 Closure),再加上最外层 Global,设置给函数 func3[[scopes]] 属性,让它打包带走。

调用 func3 的时候,JS 引擎 会取出 [[Scopes]] 中的打包的 Closure + Global 链,设置成新的作用域链, 这就是函数用到的所有外部环境了,有了外部环境,自然就可以运行了。

为什么eval性能不好

再来思考一个问题: 闭包需要扫描函数内的标识符,做静态分析,那 eval 怎么办,他有可能内容是从网络记载的,从磁盘读取的等等,内容是动态的。用静态去分析动态是不可能没 bug 的。怎么办?
没错,eval 确实没法分析外部引用,也就没法打包闭包,这种就特殊处理一下,打包整个作用域就好了。

给闭包下个定义

闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。
过滤规则:

  • 全局作用域不会被过滤
  • 其余作用域会根据是否内部有变量被当前函数所引用而过滤掉一些。不是每个返回的子函数都会生成闭包。
  • 被引用的作用域也会过滤掉没有引用的binding(变量声明)。只把用到的变量打个包。

闭包的缺点

容易导致内存泄漏

相关文章

网友评论

    本文标题:JS的静态作用域链与“动态”闭包链

    本文链接:https://www.haomeiwen.com/subject/eejxvrtx.html