细读 JS | 深入了解从预编译到解析执行的过程

简介: 细读 JS | 深入了解从预编译到解析执行的过程

前言


先来一个最简单的案例。

var a = 1;

从字面上看,这就是简单的将 1 赋值给变量 a。可在 JS 引擎里面,它认为这是两个步骤:var aa = 1,分别是声明和赋值,它们发生在两个不同的阶段。

写这篇文章的原因是看到一道题,发现自己对预编译的理解出现了偏差。加上以往也没整理过,久了不接触就会慢慢遗忘、凌乱,所以借此机会整理下预编译的知识点,同时希望这篇文章能帮助屏幕前的你。


正文


一、JS 从加载到执行完经历了什么?


  1. 页面产生便创建了一个 GO 全局对象(Global Object,全局上下文),即 window 对象。
  2. 第一个脚本文件加载。(加载完成后,接着是 JS 运行三部曲)
    1). 语法分析
    2). 预编译
    3). 解析执行
  3. 三部曲第一步:语法分析,检查是否合法。
  4. 三部曲第二步:开始预编译:
    (其实这里还有创建了 document、navigator、screen 等属性,此处忽略)
    1). 查找变量声明,作为 GO 属性,并赋予 undefined;
    2). 查找函数声明,作为 GO 属性,并赋予函数体本身(若函数与变量同名,函数会覆盖变量;若多个函数声明是同名,后面的会覆盖前面的)
  5. 三部曲第三步:开始执行代码。
  6. 执行完一个 script 块,到下一个 script 块,会重复 2、3、4、5 步骤。

<script>
  console.log(a); // function a() {}
  console.log(b); // function b() {}
  console.log(c); // undefined
  var a = 1;
  function a() {};
  function b() {};
  var c = 3;
  console.log(a); // 1
  console.log(d); // Uncaught ReferenceError: d is not defined,且从这里开始代码终止执行
  d = 4;
  console.log(d);
  /**
  (有必要说明一下:下面的 GO 是伪代码,为了助于理解罢了)
  1. 创建 GO 对象;GO {}
  2. 加载 script 块
  3. 语法分析
  4. 开始预编译
    1). 找到变量声明 a 和 c,放到 GO {a: undefined, c: undefined}
    2). 找到函数声明 a 和 b,因为函数 a 和 变量 a 同名,所以函数覆盖掉变量:GO {a: f(), b: f(), c: undefined}
  5. 执行代码
    1). 执行到 console.log(a),然后从 GO 里面找到 a,所以打印出来是函数;
    2). 继续往下执行,同理打印 b 也是函数;
    3). 同理,c 打印结果是 undefined;
    4). 因为 a 赋值 1,所以第二个 console.log(a) 会打印 1;
    5). 到了 console.log(d) 这一步,因为暗示全局变量 (imply global variable),不参与预编译的过程,所以会报引用错误;(代码终止执行,不会继续往下走)
  */
</script>


JavaScript 不是全文编译完再执行,而是块编译,即一个 script 块中预编译然后执行,再按顺序预编译下一个 script 块再执行。但是此时上一个 script 块中的数据都是可用的,而下一个 script 块中的函数和变量则是不可用的。


二、解析执行阶段,当遇到函数执行的时候,会产生 AO 活跃对象(Activation Object,活跃上下文),那过程又是怎样的呢?


首先要明确一点,预编译不仅仅发生在 script 代码块执行之前,还发生在函数执行之前


函数执行之前的“预编译”的过程:


  1. 创建 AO 对象
  2. 查找形参和变量声明,并赋予 undefined;
  3. 实参值赋给形参;
  4. 查找函数声明,赋予函数本身;

预编译完了之后,接着是执行函数。(下面举例说明下这个过程)

<script>
  function fn(a) {
    console.log(a); // 123
    console.log(b); // undefined
    console.log(c); // function c() {}
    var a = 1;
    var b = function() {}; // 注意:函数表达式,其实就是将一个匿名函数赋值给变量 b
    function c() {};
    console.log(a); // 1
    console.log(b); // function() {}
  }
  fn(123);
  /**
    有必要说明一下:下面的 AO 是伪代码,为了助于理解罢了
    1. 创建 AO 对象;AO {}
    2. 预编译
      (还包括 arguments 等,此处忽略)
      1). 查找形参和变量声明 a、b 和 c,放到 AO {a: undefined, b: undefined}
      2). 实参赋值给形参:AO {a: 123, b: undefined}
      3). 找到函数声明 c,AO {a: 123, b: undefined, c: f()}
    3. 执行代码
      1). 执行到 console.log(a),然后从 AO 里面找到 a,所以打印出来是 123;
      2). 继续往下执行,打印 b 是 undefined;
      3). 继续往下,a 赋值 1;b 赋值一个匿名函数;
      4). 第二个 console.log(a) 会打印 1;
      5). 第二个 console.log(b) 会打印 function;
  */
</script>


三、其他


其实 GO 和 AO  就差在形参和实参这俩个东西。这也是我们常说的“提升” (Hoisting)。

其实这些蛋疼的情况,几乎只会出现在“面试”上,在实际的项目中几乎很少出现。如果在多人共同维护项目中,估计早被打死了。因为可读性差而且维护成本高。

在 ES6 中,新增的 let、const、class 特性就不会存在提升的问题,声明变量,必须要在调用之前发生,否则就会报错。这应该也是一个信号,减少那些蛋疼的情况。

但是无论是为了对付面试,还是为了了解 JavaScript 内部运行的原理,作为前端开发的一员,我们都应该要弄清楚。

目录
相关文章
|
9月前
|
JavaScript 前端开发 Go
CSS 与 JS 对 DOM 解析和渲染的影响
【10月更文挑战第16天】CSS 和 JS 会在一定程度上影响 DOM 解析和渲染,了解它们之间的相互作用以及采取适当的优化措施是非常重要的。通过合理的布局和加载策略,可以提高网页的性能和用户体验,确保页面能够快速、流畅地呈现给用户。在实际开发中,要根据具体情况进行权衡和调整,以达到最佳的效果。
257 57
|
1月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
80 19
|
2月前
|
JSON 前端开发 Serverless
Mock.js 语法结构全解析
Mock.js 的语法规范介绍,从数据模板定义规范和数据占位符定义规范俩部分介绍, 让你更好的使用 Mock.js 来模拟数据并提高开发效率。
|
4月前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
111 17
|
4月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
4月前
|
存储 JavaScript 前端开发
全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略
如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
4月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
9月前
|
存储 前端开发 JavaScript
JavaScript垃圾回收机制深度解析
【10月更文挑战第21】JavaScript垃圾回收机制深度解析
181 59
|
8月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
221 17
|
8月前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
195 0

热门文章

最新文章

推荐镜像

更多
  • DNS