概述

js是一种非常灵活的语言,理解js引擎的执行过程对我们学习javascript非常重要,但是网上讲解js引擎的文章也大多是浅尝辄止或者只局部分析,例如只分析事件循环(Event Loop)或者变量提升等等,并没有全面深入的分析其中过程。所以我一直想把js执行的详细过程整理成一个较为详细的知识体系,帮助我们理解和整体认识js。

在分析之前我们先了解以下基础概念:

  • javascript是单线程语言

    在浏览器中一个页面永远只有一个线程在执行js脚本代码(在不主动开启新线程的情况下)。

  • javascript是单线程语言,但是代码解析却十分的快速,不会发生解析阻塞。

    javascript是异步执行的,通过事件循环(Event Loop)的方式实现。

下面我们先通过一段较为简单的代码(暂不存在事件循环(Event Loop))来检验我们对js引擎执行过程的理解是否正确,如下:

<script>
console.log(fun) console.log(person)
</script> <script>
console.log(person) console.log(fun) var person = "Eric"; console.log(person) function fun() {
console.log(person)
var person = "Tom";
console.log(person)
} fun() console.log(person)
</script>

我们可以先分析上面的代码,按自己的理解分析输出的顺序是什么,然后在浏览器执行一次,结果一样的话,那么代表你已经对js引擎执行过程有了正确的理解;如果不是,则代表还存在模糊或者概念不清晰等问题。结果我们不在这里进行讨论,我们利用上面简单的例子全面分析js引擎执行过程,相信在理解该过程后我们就不难得出结果的,js引擎执行过程分为三个阶段:

  1. 语法分析

  2. 预编译阶段

  3. 执行阶段

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

 

语法分析

js脚本代码块加载完毕后,会首先进入语法分析阶段。该阶段主要作用是:

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

语法错误报错如下图:

 

预编译阶段

js代码块通过语法分析阶段后,语法正确则进入预编译阶段。在分析预编译阶段之前,我们先了解一下js的运行环境,运行环境主要有三种:

  • 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)

  • 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)

  • eval(不建议使用,会有安全,性能等问题)

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

 

函数调用栈

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

我们分析下段简单的JS脚本代码来理解函数调用栈:

function bar() {
var B_context = "Bar EC"; function foo() {
var f_context = "foo EC";
} foo()
} bar()

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

  1. 首先进入全局环境,创建全局执行上下文(Global Execution Context),推入stack栈中

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

  3. 在bar函数内部调用foo函数,则再进入foo函数运行环境,创建foo函数执行上下文(foo Execution Context),推入stack栈中

  4. 此刻栈底是全局执行上下文(Global Execution Context),栈顶是foo函数执行上下文(foo Execution Context),如上图,由于foo函数内部没有再调用其他函数,那么则开始出栈

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

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

  7. Global Execution Context则在浏览器或者该标签页关闭时出栈。

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

 

创建执行上下文

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

  1. 创建变量对象(Variable Object)

  2. 建立作用域链(Scope Chain)

  3. 确定this的指向

 

创建变量对象

创建变量对象主要经过以下几个过程,如图:

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

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

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

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

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

我们分析一段简单的代码,帮助我们理解该过程,如下:

function fun(a, b) {
var num = 1; function test() { console.log(num) }
} fun(2, 3)

这里我们在全局环境调用fun函数,创建fun执行上下文,这里为了方便大家理解,暂时不讲解作用域链以及this指向,如下:

funEC = {
//变量对象
VO: {
//arguments对象
arguments: {
a: undefined,
b: undefined,
length: 2
}, //test函数
test: <test reference>, //num变量
num: undefined
}, //作用域链
scopeChain:[], //this指向
this: window
}
  • funEC表示fun函数的执行上下文(fun Execution Context简写为funEC)

  • funE的变量对象中arguments属性,上面的写法仅为了方便大家理解,但是在浏览器中展示是以类数组的方式展示的

  • <test reference>表示test函数在堆内存地址的引用

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

 

建立作用域链

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

理清作用域链可以帮助我们理解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
}

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

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

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

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

在这里我们顺便思考一下,什么是闭包

我们先看下面一个简单例子,如下:

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

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

如上图所示,chrome浏览器理解闭包是foo,那么按浏览器的标准是如何定义闭包的,我总结为三点:

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

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

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


 

确定this指向

在全局环境下,全局执行上下文中变量对象的this属性指向为window;函数环境下的this指向却较为灵活,需根据执行环境和执行方法确定,需要举大量的典型例子概括,本文先不做分析。

 

总结

由于涉及的内容过多,这里将第三个阶段(执行阶段)单独分离出来。另开新文章进行详细分析,下篇文章主要介绍js执行阶段中的同步任务执行和异步任务执行机制(事件循环(Event Loop))。本文如果错误,敬请指正。

 [原址链接](https://heyingye.github.io/2018/03/19/js%E5%BC%95%E6%93%8E%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%EF%BC%88%E4%B8%80%EF%BC%89/)

参考书籍

    • 你不知道的javascript(上卷)

(转载)js引擎的执行过程(一)的更多相关文章

  1. (转载)js引擎的执行过程(二)

    概述 js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下: 语法分析: 分别对加载完成的代码块进行语法检验,语法正 ...

  2. JS引擎的执行机制

    深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4 ...

  3. js为什么是单线程的?10分钟了解js引擎的执行机制

    深入理解JS引擎的执行机制 1.JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4.说说setT ...

  4. JS引擎的执行机制:探究EventLoop(含Macro Task和Micro Task)

    在我看来理解好JS引擎的执行机制对于理解JS引擎至关重要,今天将要好好梳理下JS引擎的执行机制. 首先解释下题目中的名词:(阅读本文后你会对这些概念掌握了解) Event Loop:事件循环Micro ...

  5. Js引擎解析执行 阅读笔记

    Js引擎解析执行 阅读笔记 一篇阅读笔记 http://km.oa.com/group/2178/articles/show/145691?kmref=search&from_page=1&a ...

  6. 深入理解JS引擎的执行机制

    深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4 ...

  7. JS 引擎的执行机制

    关于JS引擎的执行机制,首先牢记2点: .JS是单线程语言 JS的Event Loop是JS的执行机制.深入了解JS的执行,就等于深入了解JS里的event loop 关于单线程相对还比较好理解,就是 ...

  8. 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性

    我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...

  9. [转]JS 引擎的执行机制

    转: https://www.cnblogs.com/wancheng7/p/8321418.html ------------------------------------------------ ...

随机推荐

  1. Xn数列

     题目描述 Description 给你6个数,m, a, c, x0, n, g Xn+1 = ( aXn + c ) mod m,求Xn m, a, c, x0, n, g<=10^18 输 ...

  2. nashorn中js数组转为对象的问题

    背景 在项目中,使用jdk中的nashorn执行javascript脚本,例如如下脚本片段: let ctx = session.ctx; ctx.confirm = { //车牌划分后的数组 seg ...

  3. Windows性能监控perfmon工具的使用和性能指标的分析

    Windows性能监控工具perfmon的使用和性能指标分析 一.perfmon提供图表化的实时的性能监视器.性能日志.警报管理,能监控CPU的使用率.内存使用率.磁盘I/O(磁盘的读写速度).网络I ...

  4. SSD 坏了

    系统盘是SSD,系统盘坏了. 桌面所有数据都拿不回来了. 真的无奈啊,来吧,统计一下,有多少东西要重装. VS2008.VS2010.VS2013.VS2015. GITHUB.SVN.VMWare. ...

  5. python项目部署

    WSGI简介 Web框架和Wen服务器之间需要进行通信,如果在设计时它们之间无法相互匹配,那么对框架的选择就会限制对Web服务器的选择,这显然是不合理的.这时候需要设计一套双方都遵守的接口.WSGI是 ...

  6. shell脚本输出九九乘法表

    #!/bin/bash#输出九九乘法表 for ((i=1;i<=9;i++)) do for ((j=1;j<=$i;j++)) do echo -n $j'x'$i=$(($i*$j) ...

  7. 论文阅读笔记:《Interconnected Question Generation with Coreference Alignment and Conversion Flow Modeling》

    论文阅读:<Interconnected Question Generation with Coreference Alignment and Conversion Flow Modeling& ...

  8. cin,cout优化

    https://www.cnblogs.com/PrayG/p/5749832.html ios_base::sync_with_stdio(0);    cin.tie(0); 涨知识了

  9. 继承中的隐藏(hide)重写(Override)和多态(Polymorphism)

    继承中的隐藏:(不要使用隐藏,语法没有错误但是开发项目时会被视为错误) 在继承类中完全保留基类中的函数名 //基类,交通工具 class Vehicle { public void Run() { C ...

  10. 莫比乌斯反演+二维前缀和——hdu4746二刷

    第二次做这题,求前缀和的时候还是卡住了 fg函数的反演是可以直接用莫比乌斯基本代换式来代换的 #include<bits/stdc++.h> using namespace std; #d ...