javascript源代码_JavaScript温故之预编译

8c968185ca80a119910a78bec9103295.png

记得最开始学习JavaScript时,我们老师就让我们一定要记住JavaScript的两个最重要的特性,即:解释性单线程

当时自然是一知半解,只是知道把代码放在 script 标签里,把 script 标签放在 html 里,再把 html 在浏览器里打开,就能看到 console 的内容了。而对这一切是怎么发生的更是一无所知,跟别提什么解释性和单线程了。

后来随着学习深入,我知道了预编译、作用域、上下文、Nodejs、Worder等。愈发觉得JavaScript似乎并没有我印象中的那么简单。而在工作中,我也慢慢对JavaScript有了更深刻的认识。虽说对于底层JavaScript是怎么执行的我还是一窍不通,我甚至看不懂V8引擎的源码。

但是总有一些隐秘的,但是可以捕获的规律。利用这些可以解释JavaScript中一些不合常理的表现。虽然大部分都是因为JavaScript先天规范不标准,并且已经在ES6+中通过各种方式进行弥补或在社区规范中禁止,但是还是有一些神奇的地方广泛用于现在的JavaScript中。如果要了解他们,就需要从头开始。看看JavaScript开始执行时发生了什么。

上面说到解释性,就不得不说说编译型语言和解释型语言。

计算机是不能理解高级语言的,更不能直接执行高级语言,它只能直接理解机器语言,所以使用任何高级语言编写的程序若想被计算机运行,都必须将其转换成计算机语言,也就是机器码。而这种转换的方式有两种:编译和解释。

编译型语言

使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。

特点:在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。

总结

  1. 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
  2. 与特定平台相关,一般无法移植到其他平台;
  3. 现有的C、C++、Objective等都属于编译型语言。

f164e8accd6466af7ae42a976eb99ffb.png

解释型语言

使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。

特点:解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。

总结

  1. 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
  2. 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
  3. JavaScript、Python、Php等属于解释型语言。

2c173081e45f1fe56ad9f12be5d92e11.png

也就是说,我们执行js文件,只需要用Nodejs执行就好。但是计算机又不认识js代码,那么Nodejs或者说浏览器内核是如何让 JavaScript 代码变为机器可以识别的 010101 呢?答案就是JavaScript引擎。

JavaScript引擎

javascript的引擎的作用简单的来讲,就是能够读懂javascript代码,并且准确地给出运行结果的程序,比如说,当我们写 var temp = 1+1; 这样一段代码的时候,javascript引擎就能解析我们这段代码,并且将temp的值变为2。

Javascript引擎的基本原理是: 它可以把JS的源代码转换成高效,优化的代码,这样就可以通过浏览器解析甚至可以被嵌入到应用当中。

每个javascript引擎都实现了一个版本的ECMAScript, javascript只是它的一个分支,那么ECMAScript在不断的发展,那么javascript的引擎也会在不断的改变。

为什么会有那么多引擎,那是因为他们每个都被设计到不同的web浏览器或者像Node.js那样的运行环境当中。他们唯一的目的是读取和编译javascript代码。

那么常见的javascript引擎有如下:

Mozilla浏览器 -----> 解析引擎为 Spidermonkey(由c语言实现的)
Chrome浏览器 ------> 解析引擎为 V8(它是由c++实现的)
Safari浏览器 ------> 解析引擎为 JavaScriptCore(c/c++)
IE and Edge ------> 解析引擎为 Chakra(c++)
Node.js ------> 解析引擎为 V8

注意。JavaScript引擎是浏览器内核的一部分。因此,webkit 的 JavaScript 引擎是 JavaScriptCore。而 blink 的 JavaScript 引擎是 V8。这并不冲突。

解析引擎是根据 ECMAScript定义的语言标准来动态执行javascript字符串的。

那么解析引擎是如何解析JS的呢?

869a62a62b95155c39c10257c5d5a90e.png

如上图我们可知: javascript解析分为:语法解析阶段 和 运行阶段。

语法解析阶段分为: 词法分析和语法分析。运行阶段分为:预解析 和 运行阶段。

语法解析阶段即:AST抽象语法树 的生成过程。关于 AST:

抽象语法树

抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,一种编程语言的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。

590d89075d2e8e358d0d0634a6ce2945.png

而在JavaScript中,JS引擎会把源代码转换成AST,解释器再基于AST生成字节码,提供给计算机。顺便说一句,JS引擎中还有优化编译器,它会花费更多的时间处理AST,生成优化后的机械码(比解释器interpreter生成的字节码更高效)。

aa23b9f397e441b9090ecf9b89c3f122.png

程序代码本身可以被映射成为一棵语法树,而通过操纵语法树,我们能够精准的获得程序代码中的每一个精确的节点。例如声明语句,赋值语句,而这是用正则表达式所不能准确体现的地方。esprima提供了一个在线解析JavaScript代码的地址,可以清楚地观察到js代码被转化为JSON格式,由一个个具体的符号组成。

AST Explorer 也可以让你对 AST 节点有一个更好的感性认识:AST会把代码本身(body)和注释(comments)分开,然后代码语句也会进行区别:函数声明、变量声明、表达式语句等等。

00e36c7cf688602a633aa18da384827d.png

抽象语法树的作用非常的多,比如编译器、IDE、压缩优化代码等。在JavaScript中,虽然我们并不会常常与AST直接打交道,但却也会经常的涉及到它。例如使用UglifyJS来压缩代码或babel转换代码,实际这背后就是在对JavaScript的抽象语法树进行操作。

关于 AST 具体的分析过程。这里不进行展开。有兴趣可以自行了解。要注意的是:在javascript解析过程中,如果遇到错误,会直接跳出当前的代码块,直接执行下一个script代码段,因此在同一个script内的代码段有错误的话就不会执行下去并抛出错误。但是它不会影响下一个script内的代码段。

而在代码在经过语法检查阶段并且成功生成了 AST 语法树。下一步就是。我们今天的题目了:

预编译

通常我们对解释型语言的理解都是大概都是有一个解释器,我们把代码输入。然后解释器解释一行,执行一行,解释一行,执行一行。但是实际上呢?通过上面那张图我们就知道了,在所有的执行开始之前,解释器会做许多工作,比如语法检查、生成 AST 语法树,也包括预编译。实际上 JavaScript 确实是一边解释一边执行的,只是不是一行一行的,而是解释(预编译)一个上下文后,执行一个上下文中的代码。那么上下文,即执行上下文是什么?

简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文总共有三种类型:

  • 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:
    • 1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。
    • 2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
  • Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。

简单的来说,预编译阶段就相当于编译型语言的编译。把 JavaScript AST语法树编译为计算机可以执行可执行文件 exe 文件,进而交给计算机执行。而 JavaScript 引擎并不会一下子把所有的 JavaScript 代码全部给编译了,而是只编译马上要执行的那部分。举个栗子,假设有这么一段代码:

var 

JavaScript 引擎拿到这段代码,在语法检查通过并生产 AST 语法树后。会对全局进行一次预编译,生成一个全局执行上下文。这个执行上下文是一个对象,包含了全局声明的变量和函数、全局作用域(全局中变量查找的规则)、this 指向等。全局执行上下文也叫 GO (Global Object)。下面是一个模拟的 GO 对象:

GO

当 GO 对象生成完毕后,JavaScript 开始执行:

GO

直到执行到 getName(1)。JavaScript 引擎判断 getName 是一个函数,准备执行函数,在函数执行的前一刻,进行一次局部预编译,生成函数执行上下文。即 AO(activation Object)下面是一个模拟的 GO 对象:

AO

当 Ao 对象生成完毕后,JavaScript 开始执行 getName 中的代码。简单的说就是:

当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。

JavaScript 在执行过程中,最少要进行一次预编译。每个函数在执行前一定会进行一次预编译。根据上面的模拟对象,函数的预编译(AO)大概分四步:

  1. 创建AO对象
  2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
  3. 将实参值和形参统一
  4. 在函数体里面找函数声明,值赋予函数体

而全局的预编译因为没有形参,因此只是少了第三步(我们都知道 JavaScript 的作用域分全局作用域、函数作用域等。这个作用域和作用域链也是在预编译时形成,这里不再展开)。

通过上面模拟的 GO/AO 的例子还有总结的大致预编译步骤,我想你应该就明白 JavaScript 中的变量提升是怎么个情况了。

提升

通过上面的例子,我们知道。JavaScript 并不是解释一行执行一行的。所以,当有如下代码:

var 

代码仅有一行,JavaScript 引擎会如何执行呢?

第一步:预编译。还是用一个GO模拟一下:

GO

第二部:执行上下文

GO

是的。预编译过程中会将所有的变量声明放在 GO 中,然后在执行过程中直接操作 GO 中的变量而忽略掉实际代码中的变量声明。

简单点说,就是:

var 

其实本质上是:

var 

而预编译会将声明的 a 放进 GO,并赋值 undefined 。并在执行时忽略 var。

那么有意思的就来了,执行如下代码。我们会得到相同的结果:

a 

这种情况,我们就说变量 a 被提升了。

同样,函数声明有同样的效果。只是函数声明在 GO 中的默认值是函数体且优先于变量 同样,同样的提升效果在 AO 预编译时也会发生 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地

考虑以下代码:

foo

会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:

function 

注意,var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽 略了),因为函数声明会被提升到普通变量之前。 尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

foo

是不是觉得很乱。这也是早期的 JavaScript 广为诟病的原因之一。不严谨,且出乎意料。因此也就有了同名变量覆盖、明明冲突的问题。前辈人使用了各种方法,如命名空间、IIFE、require.js再到现在的Es6+及各种前端工具链。

实际上使用ES6中的let、const 代替 var 已经几乎完美的解决提升问题。

let 进行的声明不会在上下文中进行提升。

console

而用 var:

console.log(bar); // undefined
var bar = 2;

对于函数声明提升。适当的时候使用函数表达式配合 let const 即可解决。

关于 let/const 的其他表现,这里只做简单总结:

  • let添加了块级作用域
  • let约束了变量提升
  • let有暂时性死区
  • let禁止重复声明变量
  • let不会成为全局对象的属性
  • 以上let所介绍的规则均适用于const命令,不同的是,const声明的变量不能重新赋值,也是由于这个规则,const变量声明时必须初始化,不能留到以后赋值

以上是大概的总结,不具体展开。是 ES6 对 let/const 命令的规范。而具体在预编译 GO/AO 中的体现,我在中文互联网上并没有找到相关的总结,本人暂时不得而知。如有大神通晓这方面的还请不吝赐教,万分感谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值