从【预编译】到【声明提升】到【作用域链】再到【闭包】

JavaScript引擎在代码执行之前会先进行如下操作:

  1. 先进行分词/词法分析将语句分割成词法单元 token,在对当前的整个作用域分析完成后,JS引擎会将 token进行解析/语法分析翻译成 AST(抽象语法树)
  2. 预编译(预处理)
  3. 边解释边执行(不是纯解释,还有JIT编译,这里不展开了)
预编译(预处理)
有人说JavaScript没有预编译,是属于语法分析的一部分,有人说有词法,语法和代码生成就已经属于编译了,只是不需要提前编译的,而是在执行前的几毫秒才编译。所以JavaScript到底有没有预编译或者这些步骤属不属于预编译(预处理)大佬们的意见也不同,我们就不探讨了,暂且把语法分析生成AST后到执行前的一些处理过程称之为“ 预编译 ”把。
  1. 全局预编译
    1. 创建 Global Object 全局上下文对象
    2. 找变量声明,将变量名绑定为GO的一个属性,值为undefined
    3. 找函数声明,将函数名绑定为GO的一个属性,值为函数体
    4. 执行全局代码
  2. 局部预编译
    1. 创建 Activation Object 函数上下文对象
    2. 找行参,将行参名绑定为AO的一个属性,值为 undefined
    3. 找变量声明,将变量名绑定为AO的一个属性,值为 undefined
    4. 实参值赋值给形参
    5. 找函数声明,将函数名绑定为AO的一个属性,值为 函数体
    6. 函数生成[[scope]]属性,值为一个数组,数组的第一项是本函数的AO,下一项为外层函数的AO,一直往外层,直到最后一项为GO
    7. 执行函数代码
声明提升,作用域链
上面预编译的前半部分就是所谓的声明提升,我有文章单独讲了声明提升就不赘述了,详情点击这里。
函数执行前会生成`[[scope]]`属性,值为一个数组,数组的第一项是本函数的 AO 下一项为外层函数的 AO,一直往外层,直到最后一项为 GO

AO是函数的作用域,而[[scope]]这样的链式数组则是函数的作用域链,当函数进行值查询RHS时会先在[[scope]]中的第一项中寻找,如果没有找到则查找第二层,以此类推直到查找到GO为止。
闭包
函数FN执行结束后,这个函数FNAO的生命周期就应该结束了,需要被销毁。但是有特殊情况,虽然说是特殊情况,但是开发中非常常见,就是函数FN执行结束后return了一个内部函数A并被外部保存后,本来应该被销毁的函数FNAO便无法被销毁,因为函数A的[[scope]]保存着函数FNAO作为自己原型链的一部分,只要保存函数A的变量不被销毁或者变量所在的AO不被销毁则函数FNAO永远不会被销毁,并且变量保存的函数A可以一直访问函数FN中的变量和值,这样的现象叫做闭包,这样的闭包大量出现会导致内存泄漏或者加载过慢。
var Variable_1 = 1; function FN() { var Variable_2 = 2; function A() { var Variable_3 = 3 console.log(Variable_2); } return A; } var Function_A = FN(); FnVariable(); // 接受 函数A 的变量 Function_A 通过调用可以输出 FN 中的 Variable_2

【从【预编译】到【声明提升】到【作用域链】再到【闭包】】从【预编译】到【声明提升】到【作用域链】再到【闭包】
文章图片

    推荐阅读