Skip to content

以 V8 的角度看 JS

很长一段时间我都在尝试构筑一套足以自洽的 JS 世界观,试图找到一些更为底层的东西串联整个 JS,一如“分子”之于物理、“元素”之于化学、“细胞”之于生物。

但遗憾的是,遍观众多 JS 书籍,于我所求所言甚少;翻过万千网上博文,同样大多语焉不详。 而试图翻阅 V8 源码又能力不足无疾而终,在这条探索的路上一直显得迟钝且力不从心。

好在,走过一些弯路经过一些兜转,终于能画出大体模型。 于心甚慰。 不求概念最为精准,胜在足以自洽。

浏览器如何执行 js 代码

一段 JS 代码的执行,需要“翻译”成机器能够识别、直接执行的字节码
在这期间,需要通过 词法分析 将其转成一段段 token,再经过语法分析转成 AST(抽象语法树)。 再经过语义分析转成字节码,之后便能被机器识别直接执行。 由 JS 代码到字节码这段过程称之为『编译』。 而执行上下文的产生就是在 AST 生成之后。

由此连接到执行上下文与作用域

作用域

基于词法作用域模型,说白了就是 一套规则,规定变量和函数的可执行范围。有两个重要特点

    1. 由代码书写位置决定。
    1. 内层函数总能访问到外层函数中的变量。

作用域分为三类:

  • 全局作用域
  • 函数作用域
  • 块作用域: let 或 const 加上 {} 构成的代码块。 其声明的变量在执行过程中存储在执行上下文的词法环境中。 ES6 的块作用域解决了**『变量提升』**的弊端。

由此连接到执行上下文。

执行上下文:

ES5 中,由 this(值为调用当前上下文的对象的引用。) + 词法环境 + 语法环境组成。

  • 语法环境:var 声明的变量与函数存储其中, 加上外部的引用 outer
  • 词法环境:与语法环境并无二致,唯一区别是其中存储以 let const 等声明的变量。

let 与 const 是 ES6 的东西,ES5 的执行上下文为什么会定义 词法环境? 不知道, 找不到答案。

由此连接到块级作用域变量的存储。

作用域与执行上下文区别:

函数执行上下文是在调用函数时, 函数体代码执行之前创建,函数调用结束时就会自动释放。因为不同的调用可能有不同的参数: 而 JavaScript 采用的是词法作用域,fn 函数创建的作用域在函数定义时就已经确定了

简言之:

  • 作用域是一套规则,包含可使用的变量
  • 执行上下文是具体的 key 与 value。

作用域链:

作用域层层嵌套,形成的关系叫做作用域链,作用域链也就是查找变量的过程。
查找变量的过程:当前作用域 => 上一级作用域 => 上一级作用域 .... => 直到找到全局作用域 => 还没有,报错。

执行上下文栈 || 调用栈:

(先进后出) 的形式管理执行上下文。
执行过程中,全局执行上下文被压入栈底。执行完成,则函数执行上下文退出。 如果出现无限递归时,则会出现爆栈。 解决方案可以是尾调用或尾递归。
二者某种程度上都是执行外部函数后,当前外部函数被推出执行栈。注意,闭包的概念可以联系至此。 闭包返回了一个函数,算是尾调用

执行上下文栈由 JS 引擎线程维护,从而联系到消息队列、实现循环以及其他线程乃至进程乃至各进程之间的协作。

闭包:

因词法作用域“内层函数总能访问到外层函数中的变量。”的规则产生。 当外部函数执行完成并 return 了一个内部函数,当前外部函数本应推出执行栈,但由于内部函数中存在对外部函数变量的引用,故调用栈就出现了内部函数总是背负着的对外部函数变量引用的“背包”,这背包,就是闭包。可联系到柯里化、模块化方案。

this:

在执行过程中确定,每个执行上下文都绑定一个 this。总而言之,谁调用当前上下文,则 this 指向谁。 箭头函数的 this 指向最近的非箭头函数的 this

视觉上移,稍微宏观一些。

  • V8 如何借助消息队列和事件循环处理统筹任务。宏任务与微任务,以及微任务的出现解决了什么问题。
  • Javascript 是一门单线程、异步、非阻塞、解析类型脚本语言。 何为单线程、异步、非阻塞、解析类型? 之后我会逐一缕清

骨架大体如是。之后再慢慢结合细节填充。

后续文章主要参考 浏览器工作原理与实践

君子慎独