图解JS中的执行上下文和作用域

142 阅读2分钟

预备知识

首先我们看看 V8引擎的运行图

  1. V8引擎将js源码转成AST抽象语法树时,会创建全局对象Global Object(GO)
  2. 代码执行时,V8引擎创建一个执行上下文栈Execution Context Stack(ECStack)
  3. 若执行全局代码,创建全局执行上下文Global execution context(GEC)

🚧🚧注意:不同ES版本的区别

  • 早期ES3版本: 由Scope作用域、Variable object变量对象、This三个部分组成
  • 在ES5+的版本中: 由词法环境(lexical environment):存let/const声明的变量、变量环境(variable environment):存var声明的变量、This三个版本组成
  • ES2018中:This被归入词法环境(lexical environment) 虽然不同版本的ES定义不同,但是也不妨碍JS语言的思想,下文用ES3版本进行解读

在js的执行上下文中,假设有一段代码是这样的

var  name = "akechi"
foo(123)
function foo(num){
    console.log(m)
    var m = 10
    var n = 20
    console.log("foo")
}

JS的解析过程

首先进行全局的解析,创建ECStack并把区域的变量VO放入到全局对象GO中

  • 变量设置为undefined
  • 函数比较特殊,存放一个地址,指向函数内代码块存放的区域

开始执行

  • name属性赋值
  • 创建foo函数的执行上下文,初始化foo函数内的变量

执行foo内部函数

  • foo内部变量赋值
  • 执行foo内部的函数

执行结束

  • 输出结果打印
  • foo函数 弹出调用栈,等待GC回收

那如果是嵌套函数呢

var  name = "akechi"
foo(123)
function foo(num){
    console.log(m)
    var m = 10
    var n = 20
    function bar(){
        console.log(name)
    }
    bar()
}

图解如下

  • js会在调用栈中加入bar函数,并初始化它
  • bar函数打印name时沿着作用域链寻找,在全局找到name变量并打印
  • 在依次执行完bar与foo函数后,弹出调用栈

🚧🚧注意:作用域链在静态解析阶段就已经确认下来了,与在哪调用无关

举个🌰

var message = "Global"
function foo(){
    console.log(message)
}
function bar(){
    var message = "bar"
    foo()
}
bar() //打印的结果是Global

图解如下,foo函数的父级作用域时全局对象GO