《十一》JavaScript 引擎执行 JavaScript 代码的过程:执行上下文、执行上下文栈

JS 引擎对 JS 代码的处理包括解析和执行两个阶段。JS 代码的解析和执行并不是一次性完成的,而是交替进行的。当 JS 引擎执行代码时,它可能需要解析新的代码;反之,当它在解析阶段时,也可能会执行一些代码。
本章主要记录执行的过程,解析的过程在《浏览器基础及渲染引擎解析一个网页的过程、JavaScript 引擎解析 JavaScript 代码的过程》一章。

基础概念:

全局对象(Global Object、GO):

JS 引擎在解析 JS 代码时,会先在堆内存中创建一个全局对象,该全局对象默认包含一些全局的类和方法(例如:String、Math、Date、parseInt()setTimeout() 等),其中还有一个 window 属性指向自身。所有的作用域都可以访问它。

在浏览器中,Global Object 就是 Window 对象。

执行上下文(Execution Context,GE):

JS 代码要想执行,必须要先为其创建一个执行上下文。执行上下文就是当前代码的执行环境,当 JS 代码执行的时候,会进入不同的执行环境。

执行上下文的类型:

JS 中有三种类型的执行上下文:

  1. 全局执行上下文(Global Execution Context,GEC): 在执行全局的代码前,JS 引擎会创建一个全局执行上下文;直到应用程序退出(例如:关闭网页或浏览器),全局执行上下文才会被销毁。一个程序中只会有一个全局执行上下文。
  2. 函数执行上下文(Functional Execution Context,FEC):每当函数被调用时,都会为该函数创建一个新的函数执行上下文;当函数执行完毕后,函数执行上下文被销毁。函数执行上下文可以有无数个。

    多次调用同一个函数会创建多个不同的函数执行上下文。

  3. eval 函数执行上下文:eval 函数内部的代码也有属于它自己的执行上下文。很少用到。
执行上下文中三个重要的概念:
  1. 变量对象(Variable object,VO):每个执行上下文都会关联一个变量对象。在当前执行上下文中定义的所有变量和函数都会保存在这个对象中。

  2. 作用域链(Scope Chain):每个执行上下文都会关联一个作用域链。作用域链是变量对象组成的一个对象列表,作用域链的前端,始终都是当前执行上下文的变量对象,下一个变量对象来自包含环境,而再下一个变量对象则来自下一个包含环境,一直延续到全局执行上下文的变量对象。

    作用域链用来查找变量和函数,保证对访问的变量和函数的有序访问。当查找一个变量或函数时,都是从作用域链的前端开始,一级一级地向后查找,如果一直找到全局对象上都没有找到,就会报错。

    有些语句可以在作用域链的前端临时增加一个变量对象,作用域链就会得到加长,该变量对象会在代码执行后被移出。对 with 语句来说,会将指定的对象添加到作用域链的最前端;对 catch 语句来说,被抛出的错误对象会创建一个新的变量对象加到作用域的最前端。

    var obj = {
    	message: 'Hello'
    }
    with(obj) {
    	console.log(message) // Hello。message 所在执行执行上下文的作用链现在为 [obj, GO]
    }
    
    var num = 1
    function out(){
        var num = 2
        inner()
    }
    function inner(){
        console.log(num)
    }
    out()  // 1
    

    inner() 函数的作用域链的前端是它自己执行上下文的变量对象,下一个变量对象是来自它的包含环境全局执行上下文。
    请添加图片描述
    inner() 函数的作用域链的前端是它自己执行上下文的变量对象,下一个变量对象是来自它的包含环境 outer() 函数执行上下文。

    var num = 1
    function out(){
        var num = 2
        function inner(){
    	    console.log(num)
    	}
    	return inner
    }
    out()()  // 2
    

    请添加图片描述

    作用域是变量或函数可以被访问的范围。可以分为全局作用域、函数作用域和块级作用域(ES6 中新增)。作用是隔离变量,不同作用域下同名变量不会有冲突。
    ES6 之前,if 语句、for 语句等的大括号没有封闭作用域的功能,都是全局作用域。

    if (true) {
      var box=’blue’
    }
    console.log(box) // blue
    

    例如:
    使用 var 在全局声明的变量,有全局作用域,可以在全局访问。
    如果在一个函数内部访问它,就会沿着它所在执行上下文中的作用域链,先在函数执行上下文的变量对象中查找,找不到再去全局执行上下文的变量对象中查找。
    也就是说,作用域是一个变量或函数的,是它们可以被访问的范围;作用域链是一个变量或函数所在执行上下文的,是访问它们的一个链条。

  3. this 对象。

执行上下文栈(Execution Context Stack,GES)::

JS 引擎内部有一个执行上下文栈,它是用于执行代码的调用栈,被用来存储和管理代码运行时创建的所有执行上下文,拥有 LIFO(后进先出)的数据结构。

执行上下文用来存放和真正执行代码。
执行上下文栈用来调用执行上下文。

当 JS 引擎执行 JS 脚本时,它首先会创建一个全局的执行上下文并且压入执行上下文栈;每当 JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入执行上下文栈的顶部;当该函数执行结束时,执行上下文从执行上下文栈中弹出,控制流程到达执行上下文栈的下一个执行上下文。

全局执行上下文总是在栈的底部;当前运行的执行上下文总是栈顶的那个执行上下文。

let str = 'javascript'

function foo() {
    console.log('foo')
    bar()
}
function bar() {
    console.log('bar')
}
foo()
  1. 当上述代码在浏览器中运行时,JS 引擎首先会创建一个全局执行上下文并把它压入执行上下文栈。
  2. 当遇到 foo() 函数调用时, JS 引擎创建了一个 foo 函数的执行上下文并把它压入到执行上下文栈的顶部。
  3. 当从 foo() 函数内部调用 bar() 函数时,JS 引擎创建了一个 bar 函数的执行上下文并把它压入到执行上下文栈的顶部。
  4. bar() 函数执行完毕,它的执行上下文会从执行上下栈中弹出,控制流程到达下一个执行上下文,即 foo() 函数的执行上下文。
  5. foo() 函数执行完毕,它的执行上下文从执行上下栈中弹出,控制流程到达全局执行上下文。
  6. 全局执行上下文直到应用程序退出,才会被销毁。

在这里插入图片描述

JS 引擎执行代码的过程:

var message = 'Global Message'

// 定义函数
function foo(num) {
	var message = 'Foo Message'
}
// 调用函数
foo(1)

var num1 = 10
var num2 = 20
var result = num1 + num2
console.log(result)

JS 引擎执行全局代码的过程:

  1. JS 引擎在解析 JS 代码时,会先在堆内存中创建一个全局对象,将声明的全局变量、声明的全局函数等标识符添加到该全局对象中。

    在从上到下解析全局代码的过程中:

    1. 遇到声明的全局变量:将该变量添加到 GO 对象中,并默认赋值为 undefined。
    2. 遇到声明的全局函数:将该函数添加到 GO 对象中,并将函数提前创建出来。

    这就是变量提升和函数提升的原因。

  2. 为全局代码创建一个全局执行上下文。

    • 每个执行上下文都会关联一个 VO 对象,当进入全局执行上下文时,将全局执行上下文关联的 VO 指向 GO。
    • 创建作用域链,全局执行上下文的作用域链中只有 GO。
    • 确定 this 的指向:this 指向的就是全局对象。
  3. 将全局执行上下文压入执行上下文栈后,从上往下开始执行全局代码。

    • 为声明的全局变量赋值。
    • 由于声明的全局函数在解析阶段已经提前创建好了,会直接跳过。
    • 执行其他的逻辑代码。

请添加图片描述
请添加图片描述

JS 引擎执行函数代码的过程:

  1. 当执行到一个函数时,会根据函数体创建一个函数执行上下文。
    • 当进入函数执行上下文时,会创建一个AO 对象(Activation Object),使用函数的 arguments 来作为其初始化的值,将函数的形参、内部声明的局部变量、内部声明的函数添加到该 AO 对象中。然后将函数执行上下文关联的 VO 指向 AO。

      在从上到下解析函数代码的过程中:

      1. 遇到函数的形参:将形参添加到 AO 对象中,并默认赋值为 undefined。
      2. 遇到声明的局部变量:将该变量添加到 AO 对象中,并默认赋值为 undefined。
      3. 遇到声明的函数:将该函数添加到 AO 对象中,并将函数提前创建出来。

      这就是变量提升和函数提升的原因。

    • 创建作用域链。

    • 确定 this 的指向:this 的指向取决于该函数是如何被调用的。

  2. 将函数执行上下文压入执行上下文栈后,从上往下开始执行函数代码。
    • 将实参赋值给形参。
    • 为声明的变量赋值。
    • 由于声明的函数在解析阶段已经提前创建好了,会直接跳过。
    • 执行其他的逻辑代码。
  3. 函数执行完毕,函数执行上下文从执行上下文栈中弹出。

请添加图片描述
请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值