JavaScript 编译执行


作者:Seiya

时间:2019年06月03日


编译原理基础


尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。


传统编译语言的编译流程

  1. 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为 词法单元

分词与词法分析

分词(tokenizing)和词法分析(Lexing)之间的区别是非常微妙、晦涩的, 主要差异在于 词法单元 的识别是通过 有状态 还是 无状态 的方式进行的。

注意:

空格是否会被当作词法单元,取决于空格在 这门语言中是否具有意义。


  1. 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。


  1. 代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。

注意

这个过程与语言、目标平台等息息相关。



JavaScript 代码运行的三个阶段


浏览器首先按顺序加载由 <script> 标签分割的 JavaScript 代码块,加载代码块完毕后,立刻进入以下三个阶段。然后再按顺序查找下一个代码块,再继续执行以下三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。


  • 词法阶段

    编译原理部分介绍了,大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果出现不正确,则向外抛出一个语法错误,如果语法正确,则进入预编译阶段。如果是有状态的解析过程,还会赋予单词语义。


  • 预编译阶段

    之前的文章有介绍,V8 引擎有一个很重要的特点,就是“延迟”思想,这使得很多 JavaScript 代码的编译直到被调用的时候才会发生。此阶段会创建变量对象、确定作用域链以及 this 指向。更详细的预编译细节,后续的文章会继续进行讨论,这篇文章仅仅梳理大概的流程。


  • 执行阶段

    一般我们使用 JavaScript 引擎的方式:拿到一段 JavaScript 代码,将它传递给 JavaScript 引擎,并要求它执行。然而执行 JavaScript 代码并非一锤子买卖,宿主环境中遇到一些事件时,会继续把一段代码传递给 JavaScript 引擎去执行。

    这样我们就形成了对 JavaScript 引擎的感性认知:一个 JavaScript 引擎会常驻与内存中,它等待我们(宿主)把 JavaScript 代码传递给它执行。

    实际上,JavaScript 引擎的处理方式远比这个认知更为复杂,前面关于引擎的相关文章也有所介绍,JavaScript 代码传递的过程并不是发生在宿主和引擎之间,而是发生在引擎内部,JavaScript 引擎会在需要的时候编译并执行一段代码。



JavaScript 编译执行


  • 函数声明提升:

    无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面;

  • 变量声明提升:

    无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是 undefined;


我们通过一个示例,来观察 JavaScript 代码的编译执行过程:

<script>
	var a = 1;
	console.log(a);

	function test(a) {
		console.log(a);

		var a = 123;
		console.log(a);

		function a() {};
		console.log(a);

		var b = function() {};
		console.log(b);

		function d() {};
	}

	var c = function() {
		console.log("I at C function");
	}
	console.log(c);

	test(2);
</script>

  • 第一次编译阶段过程分析:

    • 页面产生便创建了GO全局对象(Global Object)(也就是window对象);

    • 第一个脚本文件加载(也就是上面的 JavaScript 代码);

    • 脚本加载完毕后,分析语法是否合法(词法阶段);

    • 开始预编译,查找变量声明,作为全局对象属性,值赋予undefined;查找函数声明,作为全局对象属性,值赋予函数体;


    此阶段过程可以抽象为:

    GlobalObject/window = {
        a: undefined,
        c: undefinedtest: function(a) {
    		console.log(a);
    
            var a = 123;
    		console.log(a);
    
            function a() {};
    		console.log(a);
    
            var b = function() {};
    		console.log(b);
    
            function d() {}
        }
    }
    

    简单描述就是创建全局对象(全局作用域),加载 JavaScript 代码。词法分析通过后,开始查找变量声明作为全局对象属性,也就是示例中的变量 ac,并赋值 undefined。最后查找函数声明作为全局对象属性,也就是 test(),并赋予函数体。


  • 第一次执行阶段过程分析:

    • 从全局对象属性中查找变量 a,并为其赋值为 1;

    • 执行 console.log 语句,从全局对象属性中查找变量 a,并取出它的值进行打印;

    • 从全局对象属性中查找变量 c,并为其赋值;

    • 执行 console.log 语句,从全局对象属性中查找变量 c,并取出它的值进行打印;

    • 从全局对象属性中查找函数 test(),并开始预编译;


    此阶段过程可以抽象为:

    GlobalObject/window = {
        a: 1,
        c: function (){
            console.log("I at C function");
        }
        test: function(a) {
    		console.log(a);
    
            var a = 123;
    		console.log(a);
    
            function a() {};
    		console.log(a);
    
            var b = function() {};
    		console.log(b);
    
            function d() {}
        }
    }
    

    此阶段执行结果如下:

    // console.log(a)
    1
    
    // console.log(c)
    ƒ (){
    	console.log("I at C function");
    }
    

  • 第二次编译阶段过程分析:

    • 创建AO活动对象(Active Object);

    • 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为 undefined;

    • 实参、形参相统一,实参值赋给形参;

    • 查找函数声明,函数名作为AO对象的属性,值为函数引用;


    此阶段过程可以抽象为:

    // 首先,查找形参和变量声明,值赋予undefined(注意变量a既是全局变量,也是形参和函数体内部的变量)
    AO = {
        a:undefined,
        b:undefined
    }
    
    // 其次,将实参值赋值给形参
    AO = {
    	a:2,
    	b:undefined
    }
    
    // 之后,查找函数声明,值赋予函数体,所以变量 a 的值被覆盖
    AO = {
        a:function a() {},
        b:undefined,
        d:function d() {}
    }
    

  • 第二次执行阶段过程分析:

    • 执行 console.log 语句,从活动对象属性中查找变量 a,并取出它的值进行打印;

    • 从活动对象属性中查找变量 a,并为其赋值为 123;

    • 执行 console.log 语句,从活动对象属性中查找变量 a,并取出它的值进行打印;

    • 执行 console.log 语句,从活动对象属性中查找变量 a,并取出它的值进行打印;

    • 从活动对象属性中查找变量 b,并为其赋值(赋值函数体);

    • 执行 console.log 语句,从活动对象属性中查找变量 b,并取出它的值进行打印;


    此阶段执行结果如下:

    // console.log(a)
    ƒ a() {}
    
    // console.log(a)
    123
    
    // console.log(a)
    123
    
    // console.log(b)
    ƒ () {}
    

注意:

预编译阶段发生变量声明和函数声明,没有初始化行为(匿名函数不参与预编译); 只有在解释执行阶段才会进行变量初始化 ;

最后更新时间: 2019-7-7 21:55:38