编译型语言与解释型语言

代码运行前将代码通过编译器装换为机器语言,运行时不需要重新编译,比如C,C++

解释型语言,又称为脚本语言,代码在运行时转换,速度慢于编译型语言,比如ruby,js

javascript引擎

javascript引擎是用来处理javascript脚本的虚拟机。javascript引擎有SpiderMonkey、Chakra、V8等。
chrome和node都是基于V8引擎,v8采用即时编译技术(Just In Time),即混合编译执行和解释执行这两种手段
js引擎主要包含编译器,解释器,TurboFan (优化编译器),以及垃圾回收器

1.编译器(parser)

将js代码拆分为Tokens,再转换为AST。

  • 词法分析:对js语句进行分割,单词化
    1
    2
    3
    4
    var name  = 1;
    被拆分为 var,name,=,1,;这5部分
    js引擎会将var name和name = 1分为编译阶段的任务和执行阶段的任务。
    编译阶段会进行查找和初始化,对变量赋值在执行过程中
  • 语法分析:将分割过的转换为AST。
    在线演示地址 https://resources.jointjs.com/demos/javascript-ast
    2.解释器(Ignition)
    将AST转换为字节码,并且解释执行
    3.TurboFan (优化编译器)JIT(Just-In-Time)工具
    对执行时的热点函数编译,v8通过TurboFan (优化编译器)将字节码转换为机器码。
    即同一部分代码不需要重复执行编译,解释,如果被多次调用的会被标记为热代码,再次调用直接使用转换过的机器码,如果代码有更改,此时会回退到AST阶段
    4.垃圾回收器(garbage collector)
    负责将程序不再需要的内存空间回收。

在Javascrip中,内存的分配和回收是自动管理的。
在js中,对象数组字符串等没有固定大小,所以运行时是动态进行内存分配,只要动态分配了内存,都需要通过释放内存以便能够再次使用,否则,js的解释器会消耗完内存,造成奔溃。

JavaScript通过自动垃圾收集机制实现内存管理,回收机制通过垃圾收集器每间隔固定时间找出不再使用的变量,释放其内存。

回收机制为分代式回收机制,新生代Scavenge算法,老生代是Mark-Sweep(标记清除)

执行上下文、执行上下文生命周期,执行上下文栈

执行上下文

js代码在执行前,会做一系列的准备工作,这个准备工作就是创建执行上下文,是js代码被解析和执行是所在环境的抽象概念。每次执行函数,都会创建一个执行上下文。另外始终会有一个全局上下文环境存在。
js可执行代码有三种

  • 全局代码(比如script代码片段)
  • 函数代码
  • eval代码

执行上下文生命周期分为创建阶段、执行阶段和回收阶段

创建执行上下文
1
2
3
4
5
6
7
8
9
注意:网络上文章很多停留在ES3时代,ES2018发生了很多变化

1.lexical enviroment: 词法环境,记录获取变量或者this时候使用
2.variable enviroment: 变量环境,声明变量时候使用
3.code evaluation state: 恢复代码执行位置
4.Function 表示正在被执行的函数
5.ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
6.Realm:使用的基础库和内置对象实例。
7.Generator:仅生成器上下文有这个属性,表示当前生成器。
  • 词法环境和变量环境可以理解为一个对象,提供标志符(函数名和变量名)和变量的映射表。都有环境记录ER外层引用OUTER引用

  • 环境记录记录了声明的函数和变量,外层引用记录了上一级词法环境引用

  • 词法环境和变量环境不同的是,变量环境只记录var,function声明
    伪代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let c = 100
    const d = 50
    function foo () {
    var a = 10
    function bar () {
    var b = 20
    }
    bar()
    }
    foo()
    var params = 100

    以上代码执行上下文如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // 默认存在的全局上下文
    GlobalExectionContext = {
    // 词法环境
    LexicalEnvironment:{
    // 环境记录
    EnvironmentRecord:{
    Type: "Object",
    // 全局变量let和const
    c: < uninitialized >,
    d: < uninitialized >,
    }

    // 外部词法环境引用
    outer: <null>,
    },
    // 变量环境
    VariableEnvironment: {
    EnvironmentRecord: {
    Type: "Object",
    // 标识符绑定在这里
    params: undefined,
    },
    outer: <null>
    }
    }

    // 函数执行上下文
    FunctionExectionContext = {
    // 词法环境
    LexicalEnvironment:{
    / 环境记录
    EnvironmentRecord: {
    Type: "Declarative",
    // 标识符绑定在这里
    Arguments: <null>,
    },
    // 外部词法环境引用
    outer: <GlobalLexicalEnvironment>
    },
    // 变量环境
    VariableEnvironment: {
    EnvironmentRecord: {
    Type: "Declarative",
    // 标识符绑定在这里
    a: undefined
    },
    outer: <GlobalLexicalEnvironment>
    }
    }

    执行上下文栈

    执行上下文栈是用来管理创建的执行上下文的。
    每创建一个函数,就会创建函数的执行执行上下文,并且压入栈顶,函数执行完成出栈,等待GC回收。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    debugger;
    function foo () {
    var a = 10
    function bar () {
    var b = 20
    }
    bar()
    }
    foo()
    var params = 100
    console.log(params)

    简而言之,js执行过程是,编辑阶段找到js可执行代码,会创建新的全局执行上下文,遇到函数创建函数的执行上下文,就会创建词法环境(this包含其中)变量环境词法环境又分为环境记录对外部环境引用(outer),创建完成加入执行上下文栈,最后执行代码,执行完成出栈等待GC回收,全局上下文在浏览器关闭时销毁。

    提示,let和const是存储在词法环境中,值为uninitialized(未初始化)。var声明存储在变量环境中,初始值为undefined,这也就是变量提升

流程如下
image
以上内容可以在sources面板中右侧的call stack调用栈和scope看到

作用域、词法作用域、函数作用域、块作用域

作用域:作用域是一套规则,用于确定在何处查找变量。如果是赋值,会采用LHS查询,如果目的是获取变量的值,使用RHS查询。不成功的RHS引用会抛出ReferenceError异常,不成功的LHS会隐式的创建一个全局变量(非严格模式下)

词法作用域:词法作用域就是定义在词法阶段的作用域,词法阶段就是在编译js代码时候,执行词法分析的阶段。无论函数被如何调用,词法作用域都只由函数被声明时所处位置决定,也可以叫做静态作用域(词法作用域不是词法环境,完全不同)

函数作用域:属于这个函数的全部变量可以在整个函数范围内使用

块作用域:在js中,{}这个大括号就是一个块,letconst都有块作用域,var声明没有块作用域,只有函数作用域。在ES3时候,catch也具有块级作用域

欺骗词法作用域的witheval

通过witheval会在运行时修改或者创建新的作用域,严格模式下禁止使用。另外,如果代码中大量使用witheval,会造成性能问题,因为没办法发挥js引擎的优化功能,因为有些优化可以根据代码的词法进行静态分析和作用域查找进行优化,并且确定函数和变量的位置,才执行的更快

变量提升
  • 变量提升是编译时做的准备工作。js代码执行前,函数声明和变量声明都会被提升到其作用域顶部位置(变量包括函数接收的参数)。创建执行上下文时候创建了词法环境和变量环境,将var function let const赋值undefined或者未定义作为标志符绑定在环境记录中(var function在变量环境,初始值为undefined。let const在词法环境,初始值为未定义,提前访问会referenceError)
  • 重复命名情况下函数声明会被先提升执行,然后才是变量
    函数表达式和函数声明区别
  • 函数声明会被预解析,表达式只有变量会被预解析

自由变量与作用域链

自由变量:即为在函数内使用了某个变量,却没有在这个函数中声明(其他作用域声明的),对于这个函数着个变量就是自由变量

1
2
3
4
5
var a = 10 // 自由变量
function foo() {
var b = 20
console.log(a+b)
}

作用域链:简而言之,查找变量时候会从自己作用域查找,找不到就从父级,一直向上,直到全局。
但是这个答案并不准确。
实际上,在js代码在编译阶段,创建执行上下文时候,会创建词法环境和变量环境,词法环境和变量环境都有一个外层引用outer属性,提供了创建这个函数时候的的外层词法环境(重要,非父作用域),寻找变量先从自己的变量环境和词法环境寻找,找不到就从外层词法环境引用查找,一层一层直到全局。

闭包

MDN的定义闭包让你可以在一个内层函数中访问到其外层函数的作用域。

  • 理论上所有的javascript都是闭包,因为在解析js代码时候,会创建执行上下文,将外部引用数据保存在词法环境和变量环境中。
  • 实践中的闭包有两种情况,一种是函数作为返回值,另外一种是函数作为参数传递。实际上,在谷歌浏览器中也只有这两种情况会在调试面板的closure中看到,虽然函数执行完退出了执行上下文栈,但是其函数内部自由变量依然保存在了内存中,没有被垃圾回收
函数作为返回值
1
2
3
4
5
6
7
8
9
10
var a = 1;
const b = 2;
function foo () {
var max = 10
return function bar () {
console.log(10+max)
}
}
var fn = foo()
fn()
函数作为参数传递
1
2
3
4
5
6
7
8
9
10
11
function foo () {
var a = 2
function bar() {
console.log(a)
}
baz(bar)
}
function baz(fn) {
fn()
}

因为内部函数引用了外层自由变量,如果外层函数执行完就销毁,那么内层函数就会找不到这个值,就会出现RHS查询错误,导致ReferenceError,虽然函数执行完退出了执行上下文,但是变量依然保存在了内存中

闭包和词法环境

之前说过词法环境和变量环境,就是标志符和变量的映射表,每次执行函数,都会压入执行上下文栈,再去创建执行上下文,就包括词法环境、变量环境等等一系列东西。从词法环境的角度去解释,就是因为当前函数被外部引用(词法环境中的外部环境outer属性引用),导致无法变量被释放。

闭包和垃圾回收

为什么闭包中的变量无法被GC回收?要知道,目前v8中堆内存中老生代部分采用的垃圾回收算法是Mark-Sweep,也就是标记清除,每次垃圾回收时候,都会遍历老生代部分的对象,可达对象会被标记,并且向一端靠拢,只要我们词法环境中还有引用,就不会被认为是一个不可达对象,就不会被回收掉。

参考资料:

https://juejin.cn/post/6844903953981767688 JavaScript:V8编译过程

https://juejin.cn/post/6961349015346610184 Event Loop 和 JS
引擎、渲染引擎的关系
https://www.cnblogs.com/wangfupeng1988/p/3977924.html 深入理解javascript原型和闭包(完结)

https://blog.csdn.net/szengtal/article/details/78726143 词法环境:ECMA实现(一)— 简介

https://juejin.cn/post/6926831181681917959#heading-6 详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包

https://262.ecma-international.org ECMA原文地址