在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。

作用域链具体是什么样呢?

静态作用域链

比如这样一段代码

function func() {
    const guang = 'guang';
    function func2() {
      const ssh = 'ssh';
      {
        function func3 () {
          const suzhe = 'suzhe';
        }
      }
    }
  }

其中,有 guang、ssh、suzhe 3 个变量,有 func、func2、func3 3 个函数,还有一个块,他们之间的作用域链可以用 babel 查看一下。

const parser = require('@babel/parser');

const traverse = require('@babel/traverse').default;

const code = `

  function func() {
    const guang = 'guang';
    function func2() {
      const ssh = 'ssh';
      {
        function func3 () {
          const suzhe = 'suzhe';
        }
      }
    }
  }

`;

const ast = parser.parse(code);

traverse(ast, {

  FunctionDeclaration (path) {
    if (path.get('id.name').node === 'func3') {
      console.log(path.scope.dump());
    }
  }})

结果是

用图可视化一下就是这样的

函数和块的作用域内的变量声明会在作用域 (scope) 内创建一个绑定(变量名绑定到具体的值,也就是 binding),然后其余地方可以引用 (refer) 这个 binding,这样就是静态作用域链的变量访问顺序。

为什么叫 “静态” 呢?

因为这样的嵌套关系是分析代码就可以得出的,不需要运行,按照这种顺序访问变量的链就是静态作用域链,这种链的好处是可以直观的知道变量之间的引用关系。

相对的,还有动态作用域链,也就是作用域的引用关系与嵌套关系无关,与执行顺序有关,会在执行的时候动态创建不同函数、块的作用域的引用关系。缺点就是不直观,没法静态分析。

静态作用域链是可以做静态分析的,比如我们刚刚用 babel 分析的 scope 链就是。所以绝大多数编程语言都是作用域链设计都是选择静态的顺序。

但是,JavaScript 除了静态作用域链外,还有一个特点就是函数可以作为返回值。比如

function func () {
  const a = 1;
  return function () {
    console.log(a);

  }

}

const f2 = func();

这就导致了一个问题,本来按照顺序创建调用一层层函数,按顺序创建和销毁作用域挺好的,但是如果内层函数返回了或者通过别的暴露出去了,那么外层函数销毁,内层函数却没有销毁,这时候怎么处理作用域,父作用域销不销毁?(比如这里的 func 调用结束要不要销毁作用域)

不按顺序的函数调用与闭包