一文读懂JS运行机制(略长)

一、前言

var a = 'heihei', b = 'xixi'
function foo () {
  console.log(a)
}

function bar () {
  var a = 'houhou'
  foo()
  console.log(b)
}

bar()
// heihei
// xixi

如果您很快就能得出上述结果,那相信您的功底非常之扎实,如果无法确定,那么这篇文章通读之后,相信可以帮您解疑。

二、执行上下文

JS中可执行代码有三种:全局代码函数代码eval代码。代码执行前需要准备的执行环境也称为执行上下文,所以也分为全局执行上下文函数执行上下文eval执行上下文

2.1 全局执行上下文

全局执行上下文中的变量对象就是全局对象,预置了很多属性和函数。在浏览器中是window,在NodeJS中是global

2.2 函数执行上下文

2.2.1 执行上下文初始化

  1. 复制函数[[scope]]属性创建作用域链(下面会讲到)
  2. arguments创建活动对象
  3. 初始化活动对象,即加入形参、函数声明、变量声明
  4. 将活动对象压入作用域链顶端

2.2.2 变量声明,声明提升

变量对象被激活为激活对象,此时发生"hoist"声明提升

  1. 函数的所有形参

    • 键为arguments,其值也是一个对象(类数组对象,有length属性),按形参顺序赋值,值为实参
    • 若无实参,属性值设为 undefined
  2. 函数声明

    • 在变量对象中添加以函数名命名的属性,它的值是一个指向堆区(堆内存)函数Function对象的引用。
    • 如果这个函数名字在变量对象属性中已存在,这个引用指针就会被重写,指向堆区中当前的函数对象。
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性(即形参和函数声明的优先级高于变量声明提升)

注意: 整个过程可以大概描述成: 函数的形参=>函数声明=>变量声明, 其中在创建函数声明时,如果名字存在,则会被重写,在创建变量时,如果变量名存在,则忽略不会进行任何操作。

2.2.3 当代码执行时

根据代码修改激活对象对象中对应的值。如果当前执行上下文中的变量对象没有该属性,就去父级的执行上下文变量对象中寻找,直至到全局执行上下文。找不到就报错

2.3 eval执行上下文

eval执行上下文比较特殊,它取决于eval函数是直接调用还是间接调用。
引用MDN上的说法:

如果间接的使用eval(),比如通过一个引用来调用它,而不是直接的调用eval。 从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。

  • 当直接调用时,eval执行上下文为执行时所处的执行上下文,具有和这个执行上文相同的作用域
  • 当间接调用时,eval执行上下文为全局执行上下文
function foo () {
  var x = 2, y = 4
  console.log(eval('x + y'))  // 直接调用,执行上下文为当前函数执行上下文,结果是 6

  var geval = eval // 等价于在全局作用域调用
  console.log(geval('x + y')) // 间接调用,执行上下文为全局执行上下文,x is not defined,实际上y也是not defined
  
  console.log(window.eval('x + y')) // 这也是间接调用
}
foo()

三、执行上下文栈

JS通过执行上下文栈来管理上述这些执行上下文

  1. JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以首先就会创建一个全局执行上下文,并压入执行上下文栈,我们用globalContext表示它,并且只有当整个应用程序结束的时候,ECStack才会被清空,所以程序结束之前,ECStack最底部永远有个globalContext
  2. 执行函数时,就会生成一个函数执行上下文并推入执行上下文栈,当函数执行完成就会把这个函数执行上下文弹出,并将控制权移交至执行栈中下一个执行环境,直至全局执行上下文globalContext
  3. 当程序结束或者浏览器关闭,全局执行上下文也会从执行栈中弹出并销毁
function foo (a) {
  console.log(a)
}
function bar (b) {
  foo(b)
}

bar('hehe')

在这里插入图片描述

四、变量对象(variable object)与激活对象(activation object)

  • 变量对象(variable object, VO):每个执行上下文都一个与之对应的变量对象,它是与执行上下文相关的数据作用域,存储了在上下文中的函数标识符、形参、变量声明等。但在规范上或者引擎实现上,这个对象是不能在JS环境中访问的
  • 激活对象(activation object, AO):当进入某个函数执行上下文中时,其对应变量对象会被激活,变量对象上的属性才能被访问,所以称之为激活对象。

激活对象就是在函数执行上下文中被激活成可访问的变量对象

五、词法环境(lexical environment)

根据词法环境规范定义:

  • A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
  • 词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

环境记录:主要是声明性环境记录(declarative Environment Records)和对象环境记录(object Environment Records),其次还有全局环境记录(global Environment Records)和函数环境记录(function Environment Records)。

  • 声明性环境记录(declarative Environment Records):存储变量、函数和参数, 用于函数声明、变量声明和catch语句。

  • 对象环境记录(object Environment Records):用于像with这样绑定对象标识符(作用域)的语句。

  • 全局环境记录函数环境记录:是特殊的声明性环境记录,形式上可理解为对应的变量对象。

外部词法环境:构成作用域链的关键

六、作用域与作用域链

6.1 作用域

  • 词法作用域(静态作用域):函数的作用域在函数定义的时候就决定了。JS使用的是词法作用域。
  • 动态作用域:函数的作用域是在函数调用的时候才决定的。

作用域(Scope)用于规定如何查找变量,也就是确定当前执行上下文中对变量的访问权限。

6.2 作用域链

在函数中有一个内部属性,当函数创建的时候,就会保存所有父变量对象到其中,在查找变量值的时候,会先从[[scope]]顶部即当前上下文的变量对象(作用域)中查找,如果没有找到,就会根据当前执行上下文中的[[scope]]对外部执行环境的引用顺序,从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

注意

  1. 当进入函数执行上下文(函数激活)时,会将该函数的变量对象推入到作用域链前端。
  2. 正式由于作用域与作用域链的这种关系,在当前函数执行上下文的活动对象中必定存在this和arguments,所以this和arguments的搜索在当前执行执行上下文就停止了。

总结

回过头来看前言中的问题,按照下面的流程进行(只列出关键部分):

  1. 创建全局执行上下文,推入执行上下文栈中
ECStack = [
  globalContext
]
  1. 初始化全局执行上下文
globalContext = {
  VO: [global], // 指向全局对象
  Scope: [globalContext.VO], // 可访问权限
  this: globalContext.VO
}
  1. 同时foo函数和bar函数被创建,生成内部作用域链。
foo.[[scope]] = [
  globalContext.VO
]

bar.[[scope]] = [
  globalContext.VO
]
  1. 经过代码执行,全局执行环境的变量对象已经赋值。根据2.2节中所述,执行bar函数前,创建bar函数执行上下文,并推入执行上下文栈中。
ECStack = [
  barContext,
  globalContext
]
  1. 初始化bar函数执行上下文
barContext = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined,
    foo: <reference to function foo() {}>
  },
  Scope: [barContext.AO, globalContext.VO],
  this: undefined
}
  1. 中断bar函数执行,开始执行foo函数,同理,创建foo函数执行上下文,并推入执行上下文栈中
ECStack = [
  fooContext,
  barContext,
  globalContext
]
  1. 初始化foo函数执行上下文
fooContext = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [fooContext.AO, globalContext.VO],
  this: undefined
}
  1. foo函数执行,foo函数执行上下文中的激活对象没有属性a,所以沿着作用域链[[scope]]找到全局执行上下文中的变量对象,其指向全局对象,故输出'heihei'。执行完毕弹出foo函数执行上下文并销毁。
ECStack = [
  barContext,
  globalContext
]
  1. 继续bar函数执行,bar函数执行上下文中的激活对象没有属性b,所以沿着作用域链[[scope]]找到全局执行上下文中的变量对象,其指向全局对象,故输出'xixi'。执行完毕弹出bar函数执行上下文并销毁。
ECStack = [
  globalContext
]

参考

  1. 规范文档
  2. MDN
  3. 傻傻分不清的javascript运行机制
  4. javascript作用域,作用域链,[[scope]]属性
  5. JavaScript深入之作用域链
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章