你须要知道的JS运行机制

1、前言

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

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

bar()
// heihei
// xixi
复制代码

若是您很快就能得出上述结果,那相信您的功底很是之扎实,若是没法肯定,那么这篇文章通读以后,相信能够帮您解疑。javascript

2、执行上下文

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

2.1 全局执行上下文

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

2.2 函数执行上下文

2.2.1 执行上下文初始化

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

2.2.2 变量声明,声明提高

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

  1. 函数的全部形参git

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

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

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

注意: 整个过程能够大概描述成: 函数的形参=>函数声明=>变量声明, 其中在建立函数声明时,若是名字存在,则会被重写,在建立变量时,若是变量名存在,则忽略不会进行任何操做。数组

2.2.3 当代码执行时

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

2.3 eval执行上下文

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

若是间接的使用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()
复制代码

3、执行上下文栈

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

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

bar('hehe')
复制代码

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

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

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

5、词法环境(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、做用域与做用域链

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深刻之做用域链
相关文章
相关标签/搜索