javascript系列javascript引擎执行的过程的理解语法分析和预编译阶段

35次阅读

共计 4192 个字符,预计需要花费 11 分钟才能阅读完成。

一、概述

js 是一种非常灵活的语言,理解 js 引擎的执行过程对于我们学习 js 是非常有必要的。看了很多这方便文章,大多数是讲的是事件循环(event loop)或者变量提升的等,并没有全面分析其中的过程。所以觉得把这个 js 执行的详细过程整理一下,帮助更好的理解 js。

1.1 基础概念

js 是单线程语言。

在浏览器中一个页面永远只有一个线程在执行 js 脚本代码

js 是单线程怨言,但是代码解析是非常迅速的,不会发生解析阻塞。

js 是异步执行的,通过实践循环(event loop)方式实现的

暂时我们不考虑事件循环(event loop),我们先来看这样一段代码,来确定我们是否理解 js 引擎的执行过程

console.log(person)

console.log(personFun)

var person = "saucxs";

console.log(person)

function personFun() {console.log(person)
    var person = "songEagle";
    console.log(person)
}

personFun()

console.log(person)

可以自己直接使用浏览器看出输出结果

首先我们来分析一下上面的代码,虽然很多开发人员基本上都能答出来,但是还是要啰嗦一下。

全面分析 js 引擎的执行过程,分为三个阶段

1、语法分析

2、预编译阶段

3、执行阶段

说明:浏览器先按照 js 的顺序加载 <script> 标签分隔的代码块,js 代码块加载完毕之后,立刻进入到上面的三个阶段,然后再按照顺序找下一个代码块,再继续执行三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。

二、语法分析

js 的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:

分析该 js 脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误(syntaxError),停止改 js 代码的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。

类似的语法报错的如下图所示:

三、预编译阶段

js 代码块通过语法分析阶段之后,语法都正确的下回进入预编译阶段。

在分析预编译阶段之前,我们先来了解一下 js 的运行环境,运行环境主要由三种:

1、全局环境(js 代码加载完毕后,进入到预编译也就是进入到全局环境)

2、函数环境(函数调用的时候,进入到该函数环境,不同的函数,函数环境不同)

3、eval 环境(不建议使用,存在安全、性能问题)

每进入到一个不同的运行环境都会创建 一个相应的执行上下文(execution context),那么在一段 js 程序中一般都会创建多个执行上下文,js 引擎会以栈的数据结构对这些执行进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(global execution context),栈顶则永远时当前的执行上下文。

3.1 函数调用栈

什么是函数调用栈?

函数调用栈就是使用栈存取的方式进行管理运行环境,特点是先进后出,后进后出

我们来分析一下简答的 js 代码来理解函数调用栈:

function bar() {
    var B_context = "bar saucxs";

    function foo() {var f_context = "foo saucxs";}

    foo()}

bar()

上面代码块通过语法分析后,进入预编译阶段,如图所示

1、首先进入到全局环境,创建全局执行上下文(global Execution Context),推入到 stack 中;

2、调用 bar 函数,进入 bar 函数运行环境,创建 bar 函数执行上下文(bar Execution Context),推入 stack 栈中;

3、在 bar 函数内部调用 foo 函数,则再进入到 foo 函数运行环境中,创建 foo 函数执行上下文(foo Execution Context),如上图,由于 foo 函数内部没有再调用其他函数,那么则开始出栈;

5、foo 函数执行完毕之后,栈顶 foo 函数执行上下文(foo Execution Context)首先出栈;

6、bar 函数执行完毕,bar 函数执行上下文(bar Execution Context)出栈;

7、全局上下文(global Execution Cntext)在浏览器或者该标签关闭的时候出栈。

说明:不同的运行环境执行都会进入到代码预编译和执行两个阶段,语法分析则在代码块加载完毕时统一检查语法。

3.2 创建执行上下文

执行上下文可以理解成当前的执行环境,与该运行环境相对应。创建执行上下文的过程中,主要是做了下面三件事,如图所示:

1、创建变量对象(variable object)

2、创建作用域链(scope chain)

3、确定 this 的指向

3.2.1 创建变量对象

创建变量对象主要是经过以下过程,如图所示:

1、创建 arguments 对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行的,全局环境没有此过程。

2、检查当前上下文的函数声明,按照代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在堆内存地址引用,如果存在,则会被新的引用覆盖掉。

3、检查当前上下文的变量声明,爱去哪找代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有变量名属性,则在该变量对象以变量名建立一个属性,属性值为 undefined;如果存在,则忽略该变量声明。

说明:在全局环境中,window 对象就是全局执行上下文的变量对象,所有的变量和函数都是 window 对象的属性方法。

所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。

下面我们再来分析这个简单代码

function fun(m,n){
    var saucxs = 1;

    function execution(){console.log(saucxs)
    }
}

fun(2,3)

这里我们在全局环境中调用 fun 函数,创建 fun 的执行上下文,这里暂时不说作用域链以及 this 指向的问题。

funEC = {
    // 变量对象
    VO: {
        //arguments 对象
        arguments: {
            m: undefined,
            n: undefined,
            length: 2
        },

        //execution 函数
        execution: <execution reference>, 

        //num 变量
        saucxs: undefined
    },

    // 作用域链
    scopeChain:[],

    //this 指向
    this: window
}

1、funEC 表示 fun 函数的执行上下文(fun Execution Context 简写为 funEC);

2、funEC 的变量对象中 arguments 属性,上面这样写只是为了理解,在浏览器中展示以类数组的方式展示的

3、<execution reference> 表示的是 execution 函数在堆内存地址的引用

说明:创建变量对象发生在预编译阶段,还没有进入到执行阶段,该变量对象都不能访问的,因为此时的变量对象中的变量属性尚未赋值,值仍为 undefined,只有在进行执行阶段,变量中的变量属性才进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是 VO->AO 过程。

3.2.2 创建作用域链

作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,保证了当前执行还款对符合访问权限的变量和函数有序访问。

理解清楚作用域链可以帮助我们理解 js 很多问题包括闭包问题等,下面我们结合一个例子来理解一下作用域链。

var num = 30;

function test() {
    var a = 10;

    function innerTest() {
        var b = 20;

        return a + b
    }

    innerTest()}

test()

在上面例子中,当执行到调用 innerTest 函数,进入到 innerTest 函数环境。全局执行上下文和 test 函数执行上下文已进入到执行阶段,innerTest 函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是 AO(global),AO(test)和 VO(innerTest),而 innerTest 的作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,如下:

innerTestEC = {

    // 变量对象
    VO: {b: undefined}, 

    // 作用域链
    scopeChain: [VO(innerTest), AO(test), AO(global)],  
    
    //this 指向
    this: window
}

我们这里可以直接使用数组表示作用域链,作用域链的活动对象或者变量对象可以直接理解成作用域。

1、作用域链的第一项永远是当前作用域(当前上下文的变量对象或者活动对象);

2、最后一项永远是全局作用域(全局上下文的活动对象);

3、作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或者函数,找到则会停止找,找不到则一直查找全局作用域,再找不到就会排除错误。

3.2.3 闭包

什么是闭包?思考一下

看一下简单的例子

function foo() {
    var num = 20;

    function bar() {
        var result = num + 20;

        return result
    }

    bar()}

foo()

因为对于闭包的有很多的不同理解,包括我看一些书籍(js 高级程序设计),我这直接以浏览器解析,以浏览器的闭包为准来分析闭包,如图

如图所示,谷歌浏览器理解的闭包是 foo,那么按照浏览器的标准是如何定义的闭包,自己总结为三点:

1、在函数内部定义新函数

2、新函数访问外层函数的局部变量,即访问外层函数环境的活动对象属性

3、新函数执行,创建新函数的执行上下文,外层函数即为闭包

3.2.4 确定 this 指向

1、在全局环境下,全局执行的上下文中变量对象的 this 属性指向为 window;

2、在函数环境下的 this 指向比较灵活,需要根据执行环境和执行方法确定,列举典型例子来分析

四、总结

由于涉及到的内容过多,下一次将第三阶段(执行阶段)单独分离出来。另开出新文章详细分析,主要介绍 js 执行阶段中的同步任务执行和异步任务执行机制(事件循环(Event Loop))。

五、参考书籍

你不知道的 javascript(上卷)

正文完
 0