JS引擎线程的执行过程的三个阶段(一)

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

JS引擎线程的执行过程的三个阶段:

  • 语法分析
  • 预编译阶段
  • 执行阶段

一. 语法分析

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

下面阶段的代码执行不会再进行语法校验,语法分析在代码块加载完毕时统一检验语法。

二. 预编译阶段

1. js的运行环境

  • 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)
  • 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)
  • eval(不建议使用,会有安全,性能等问题)

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

2. 函数调用栈/执行栈

调用栈,也叫执行栈,具有LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶。

当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。

var a = ‘Hello World!‘;

function first() {
  console.log(‘Inside first function‘);
  second();
  console.log(‘Again inside first function‘);
}

function second() {
  console.log(‘Inside second function‘);
}

first();
console.log(‘Inside Global Execution Context‘);

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context

  

 

3. 执行上下文的创建

执行上下文可理解为当前的执行环境,与该运行环境相对应,具体分类如上面所说分为全局执行上下文和函数执行上下文。创建执行上下文的三部曲:

  • 创建变量对象(Variable Object)
  • 建立作用域链(Scope Chain)
  • 确定this的指向

3.1 创建变量对象

  • 创建arguments对象:检查当前上下文中的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行,全局环境没有此过程
  • 检查当前上下文的函数声明:按代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则为指向该函数所在堆内存地址的引用,如果存在,则会被新的引用覆盖。
  • 检查当前上下文的变量声明:按代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有该变量名属性,则在该变量对象以变量名建立一个属性,属性值为undefined;如果存在,则忽略该变量声明

函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。具体是如何函数和变量声明提前的可以看后面。

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

3.2 建立作用域链

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

可以通过一个例子简单理解:

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
}

  

 

深入理解的话,创建作用域链,也就是创建词法环境,而词法环境有两个组成部分:

  • 环境记录:存储变量和函数声明的实际位置
  • 对外部环境的引用:可以访问其外部词法环境

词法环境类型伪代码如下:

// 第一种类型: 全局环境
GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {    	  // 词法环境
    EnvironmentRecord: {   		// 环境记录
      Type: "Object",      		   // 全局环境
      // 标识符绑定在这里
      outer: <null>  	   		   // 对外部环境的引用
  }
}

// 第二种类型: 函数环境
FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {  	  // 词法环境
    EnvironmentRecord: {  		// 环境记录
      Type: "Declarative",  	   // 函数环境
      // 标识符绑定在这里 			  // 对外部环境的引用
      outer: <Global or outer function environment reference>
  }
}

  

在创建变量对象,也就是创建变量环境,而变量环境也是一个词法环境。在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定。

如例子:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);

  

 

执行上下文如下所示

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 标识符绑定在这里
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 标识符绑定在这里
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {  

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 标识符绑定在这里
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 标识符绑定在这里
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

  

 

变量提升的具体原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。此时let 和 const处于未初始化状态不能使用,只有进入执行阶段,变量对象中的变量属性进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,letconst才能进行访问。

关于函数声明和变量声明,这篇文章讲的很好:https://github.com/yygmind/blog/issues/13

另外关于闭包的理解,如例子:

function foo() {
    var num = 20;

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

        return result
    }

    bar()
}

foo()

  

 

浏览器分析如下:

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

  • 在函数内部定义新函数
  • 新函数访问外层函数的局部变量,即访问外层函数环境的活动对象属性
  • 新函数执行,创建新的函数执行上下文,外层函数即为闭包

3.3 this指向

比较复杂,后面专门弄一篇文章来整理。

第三个阶段请看  JS引擎线程的执行过程的三个阶段(二)

文章参考:

https://github.com/yygmind/blog/issues/12

https://heyingye.github.io/2018/03/19/js引擎的执行过程(一)

https://heyingye.github.io/2018/03/26/js引擎的执行过程(二)

https://github.com/yygmind/blog

原文地址:https://www.cnblogs.com/BoatGina/p/10433518.html

时间: 2024-08-29 03:43:56

JS引擎线程的执行过程的三个阶段(一)的相关文章

JS引擎线程的执行过程的三个阶段(二)

继续JS引擎线程的执行过程的三个阶段(一) 内容, 如下: 三. 执行阶段 1. 网页的线程 永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行, 不参与代码解析与执行. JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎) 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制.主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件

JS的解析与执行过程(javascript面向对象一)

JS的解析与执行过程 全局中的解析和执行过程 预处理:创建一个词法环境(LexicalEnvironment,在后面简写为LE),扫描JS中的用声明的方式声明的函数,用var定义的变量并将它们加到预处理阶段的词法环境中去. 一.全局环境中如何理解预处理 比如说下面的这段代码: var a = 1;//用var定义的变量,以赋值 var b;//用var定义的变量,未赋值 c = 3;//未定义,直接赋值 function d(){//用声明的方式声明的函数 console.log('hello'

JS的解析与执行过程—(全局预处理阶段)

问题:有如下代码 1 var a = 1; 2 function pop() { 3 alert(a); 4 var a = 5; 5 } 6 pop();//执行结果,弹出undefined 这段代码的执行结果为undefined,为什么呢? JS的解析与执行并不是读一行,处理一行,读一行,处理一行这样进行的,而是分为两个阶段: 1.预处理阶段: 2.执行阶段: 然后分别以全局和函数内部的局部代码而言: 1.全局预处理 在解析JS代码的时候,首先会创建一个全局LexicalEnviroment

JS的解析与执行过程—全局预处理阶段之命名冲突的处理策略

有如下代码: 1 <body> 2 <script> 3 alert(f); 4 5 function f() { 6 console.log("fff"); 7 } 8 var f = 5; 9 </script> 10 </body> 不论var f 与function f 的先后顺序如何,该代码执行的结果总是弹出function f 的字符串,为什么呢?像这种函数与变量命名冲突时JS的处理原则又是什么? 在扫描函数声明与变量声明的时

js事件线程机制和异步执行

浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程. javascript执行顺序:http://bbs.html5cn.org/thread-80116-1-1.html 1.javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序. 2.GUI渲染线程负责渲染浏览器界面,当界面需要重绘(R

单线程的JS引擎

先来思考一个问题,JS 是单线程的么?为什么单线程的JavaScript却能让AJAX异步发送和回调请求,还有setTimeout也看起来像是多线程的?还有non-blocking IO, event loop等概念. 目录: JS单线程 浏览器多线程 setTimeout(func, 0) 的应用场景 setTimeout与setInterval 参考资料 TODO: 接下来,梳理JS的并发模型与 Event Loop https://developer.mozilla.org/zh-CN/d

【JS】JavaScript引擎的内部执行机制

 近期在复习JavaScript,看到setTimeout函数时.想起曾经刚学时,在一本书上看过setTimeout()里的回调函数执行的间隔时间有昌不是后面设置的值.曾经没想太多.网上看了JS大神的解释,整理记录下JavaScript引擎的内部执行机制. 首先看一段小程序: <script> alert('第1'); setTimeout(function(){alert('第2');}, 2000); alert('第3'); </script> 输出顺序是:第1.第3,第

javascript从定义到执行 js引擎 闭包

javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境 栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等,这些概念正是JS引擎工作的核心组件.这篇文章的目的是孤立的为你讲解每一个概念,而 是通过一个简单的demo来展开分析,全局讲解JS引擎从定义到执行的每一个细节,以及这些概念在其中所扮演的角色. var x = 1;  //定义一个全局变量 x function A(y){ var x = 2; 

JS引擎的执行机制

深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4.说说setTimeout 首先,请牢记2点: (1) JS是单线程语言 (2) JS的Event Loop是JS的执行机制.深入了解JS的执行,就等于深入了解JS里的event loop 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 技术的出现,都跟现实