news 2026/5/13 21:54:05

js 解析 和作用域的锁定 词法分析 和 语法分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
js 解析 和作用域的锁定 词法分析 和 语法分析

这部分内容,学了当然最好,没学,也不影响前端开发。当然,能了解肯定是比不了解的强。

依旧是无图无码,网文风格。我觉得,能用文字把逻辑或者概念表述清楚,一是对作者本身的能力提升有好处,二是对读者来说 思考文字表达的内容 有助于多使用抽象思维和逻辑思维能力,构建自己的思考模式,用现在流行的说法 就是心智模型。你自己什么都可以脑补,那不是厉害大了嘛。

上面的话不要相信,其实我就是为自己懒找的借口。

有些细节做了省略 有些边界情况做了简化表述。但是总体来说 准确性还是相当不错的。 如果有错漏的地方,还请多多指正。这是第一部分 词法和语法分析。

一.词法分析和语法分析

当浏览器从网络下载了js文件,比如app.js,浏览器引擎拿到的最初形态是一串**字节流 **。

  1. 识别:V8 首先要处理编码,V8 接收的是 UTF-8 编码的字节流,内部会转换为 UTF-16 处理字符串。

  2. 流式快速处理:引擎并不是等整个文件下载完才开始干活的。只要网络传过来一段数据,V8 的扫描器就开始工作了。 这样可以加快启动速度。此时的状态就是毫无意义的字符c,o,n,s,t, ,a, ,=, ,1,;

  3. 然后的这一步叫Tokenization 词语切分。 负责这一步的组件就是上面提到的叫Scanner(扫描器)。它的工作就像是一个切菜工,把滔滔不绝连绵不断的字符串切成一个个有语法意义的最小单位,叫做Token(记号)。看到这个词 ,大家是不是惊觉心一缩,没错,就是它,它们就是以它为单位来收咱钱的。

    scanner 内部是一个状态机。它逐个读取字符:

    • 读到c可能是const,也可能是变量名,继续。
    • 读到o,n,s,t凑齐了5个娃,且下一个字符不是字母(比如是空格),确认这是一个关键字 const。”(防止误判constant这种变量名)
    • 读到 空格 忽略,跳过去。
    • 读到1这是一个数字。

    这样就由原来的字节流变成了Token 流。这是一种扁平的列表结构。

    • 源码:const a = 1;
    • Token 流:
      • CONST(关键字)
      • IDENTIFIER(值为 “a”)
      • ASSIGN(符号 “=”)
      • SMI(小整数 “1”)
      • SEMICOLON(符号 “;”)

    这一步,注释和多余的空格和换行符会被抛弃。

  4. 现在就是解析阶段了

    其实解析是一个总称,它分为 全量解析 和 预解析 两种形式。

    这就是v8的懒解析机制。看到这个懒字,也差不多能明白了吧。

    对于那些不是立即执行的函数(比如点击按钮才触发的回调),V8 会先用预解析快速扫一遍。

    检查基本的语法错误(比如有没有少写括号),确认这是一个函数。并不会生成复杂的 AST 结构,也不建立具体的变量绑定,只进行最基础的闭包引用检查。御姐喜的结果是这个函数在内存里只是一个很小的占位符,跳过内部细节。

    而只有那些立即执行函数或者顶层代码,才会进入真正的全量解析,进行完整的 AST 构建。

    那么,问题就来了,v8怎么判断到底是使用预解析还是使用全量解析呢?

    它的原则就是 懒惰为主 全量为辅

    就是v8默认你写的函数暂时不会执行,除非是已经显式的通过语法告诉它,这段这行代码 马上就要跑 你赶快全量解析。

    下面 我们稍微详细的说一下

    • 默认绝大多数函数都是预解析

      v8认为js在初始运行时,仅仅只有很少很少一部分代码 是需要马上使用的 其他觉得大部分 都是要么是回调 要么是其他的暂时用不到的,所以,凡是具名函数声明、嵌套函数,默认都是预解析。

      function clickHandler() { console.log("要不要解析我"); } // 引擎认为 这是一个函数声明 看起来还没人调勇它 // 先不浪费时间了,只检查一下括号匹配吧, // 把它标记为 'uncompiled',然后跳过。"
    • 那么 如何才能符合它进行全量解析的条件呢

      1. 顶层代码

        写在最外层 不在任何函数内 的代码,加载完必须立即执行。

        判断依据:只要不在function块里的代码,全是顶层代码,必须全量解析。

      2. 立即执行函数

        那么这里有个问题,就是V8 如何在还没运行代码时,就知道这个函数是立即调用执行函数呢?

        答案就是 看括号()

        当解析器扫描到一个函数关键字function时,它会看一眼这个 function 之前有没有左括号(

        • 没括号

          function foo() { ... } // 没看到左括号,那你先靠边吧, 对它预解析。
        • 有括号

          (function() { ... })(); // 扫描器扫到了这个左括号 // 欸,这有个左括号包着 function // 根据万年经验,这是个立即执行函数,马上就要执行。 // 直接上大菜,全量解析,生成 AST
        • 其他的立即执行的迹象:除了括号,!+-等一元运算符放在function前面,也会触发全量解析

          !function() { ... }(); // 全量解析
    • 如果有嵌套函数咋办呢

      嵌套函数默认是预解析,即使外部函数进行的是全量解析,它内部定义的子函数,默认依然是预解析。只有当子函数真的被调用时,V8 才会暂停执行,去把子函数的全量解析做完 把 AST 补齐

      //顶层代码全量解析 (function outer() { var a = 1; // 内部函数 inner: // 虽然 outer 正在执行,但 inner 还没被调用 // 引擎也不确定 inner 会不会被调用。 // 所以inner 默认预解析。 function inner() { var b = 2; } inner(); // 直到执行到这一行,引擎才会回头去对 inner 进行全量解析 })();
    • 那么 引擎根据自己的判断 进行全量解析或者预解析,会出错吗

      当然会,

      如果是本该预解析的 结果判断错了 进行了全量解析 浪费了时间和内存生成了 AST 和字节码,结果这代码根本没跑。

      如果是本该全量解析的又巨又大又重的函数 结果判断错了 进行了预解析,然后马上下一行代码就调用了,结果就是 白白预解析了一遍,浪费了时间,发现马上被调用,又马上回头全量解析一边 又花了时间,两次的花费。

  5. 在上面只是讲了解析阶段的预解析和全量解析的不同,现在我们讲解析阶段的过程

    V8 使用的是递归下降分析法。它根据js 的语法规则来匹配 Token。

    它的规则类似于:当我们遇到const,根据语法规则,后面必须跟一个变量名,然后是一个赋值号,然后是一个表达式。

    过程示例:

    看到const创建一个变量声明节点。

    看到a把它作为声明的标识符

    看到=知道后面是初始值

    看到1创建一个字面量节点,挂在=的右边。

    而在这个阶段的同时,作用域分析也在同步进行,因为在构建 AST 的过程中,解析器必须要搞清楚变量在哪里

    它会盘算 这个a是全局变量,还是函数内的局部变量?

    如果当前函数内部引用了外层的变量,解析器会在这个阶段打上标记:“要小心,这个变量被逮住了,将来可能需要上下文来分配”。

    这个作用域分析比较重要,我们用稍微大点的篇幅来讲讲。

    首先 强烈建议 不要再去用以前的 活动对象AO vo 等等的说法来思考问题。应该使用现在的词法作用域 环境记录 等等思考模型。

    词法作用域 (Lexical Scoping)”的定义:作用域是由代码书写的位置决定的,而不是由调用位置决定的。

    这说明,引擎在还没开始执行代码,仅仅通过“扫描”源代码生成 AST 的阶段,就已经把“谁能访问谁”、“谁被谁逮住”这笔账算得清清楚楚了。

    一旦AST被生成,那么至少意味着下面的情况

    作用域层级被确定

    AST 本身的树状结构,就是作用域层级的物理体现。

    变量引用关系被识别

    这是解析器最忙碌的工作之一,叫做变量解析

    这里要注意:这个“找”的过程是在编译阶段完成的逻辑推导。

    闭包的蓝图被预判

    这一步是 V8 性能优化的关键,也就是作用域分析。

下面就是解释器Ignition该登场了。我们第二部分再见。

本文首发于: 掘金社区

同步发表于: csdn

博客园

码字虽不易 知识脉络的梳理更是不易 ,但是知识的传播更重要,

欢迎转载,请保持全文完整。

谢绝片段摘录。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 17:33:42

24、Linux系统设备管理与任务调度全解析

Linux系统设备管理与任务调度全解析 1. /proc文件系统与内核版本 在Linux系统中,/proc文件系统是一个特殊的文件系统,它提供了对内核数据的访问。通过 /proc/version ,可以查看内核版本号。你可以像操作其他目录和文件一样在 /proc 文件系统中导航,使用 more 或 c…

作者头像 李华
网站建设 2026/5/13 21:47:25

终极免费C语言教程:完整PDF电子书资源下载指南

终极免费C语言教程:完整PDF电子书资源下载指南 【免费下载链接】C语言程序设计电子书PDF版 这本C语言程序设计电子书是学习与提升C语言编程技能的绝佳资源,适合所有层次的读者。内容详实且深入浅出,不仅涵盖C语言的基本语法,还提供…

作者头像 李华
网站建设 2026/5/6 2:32:15

研究生如何在大量文献中筛选有价值信息?

研究生在海量文献中高效筛选有价值信息,核心是建立分层筛选逻辑,结合工具辅助 专业判断,避免陷入 “逐篇精读” 的低效陷阱。以下是一套可落地的方法论:一、 明确筛选目标:先定方向,再找文献筛选前先锚定核…

作者头像 李华
网站建设 2026/5/13 21:36:39

小白前端必看:用CSS3 object-fit轻松搞定图片视频比例自适应(附实

小白前端必看:用CSS3 object-fit轻松搞定图片视频比例自适应(附实小白前端必看:用CSS3 object-fit轻松搞定图片视频比例自适应(附实战技巧)为什么你的图片和视频在不同设备上总是变形?揭开 object-fit 的神…

作者头像 李华
网站建设 2026/5/13 9:44:42

Diffy终极指南:轻松实现文本差异对比

Diffy终极指南:轻松实现文本差异对比 【免费下载链接】diffy Easy Diffing in Ruby 项目地址: https://gitcode.com/gh_mirrors/dif/diffy Diffy是一个专为Ruby开发者设计的简单易用的文本差异对比库,它通过利用Unix系统中成熟的diff工具&#xf…

作者头像 李华