在 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 调用结束要不要销毁作用域)
不按顺序的函数调用与闭包