Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript之执行上下文和执行栈 #2

Open
chenyong9528 opened this issue Jul 14, 2020 · 0 comments
Open

JavaScript之执行上下文和执行栈 #2

chenyong9528 opened this issue Jul 14, 2020 · 0 comments

Comments

@chenyong9528
Copy link
Owner

chenyong9528 commented Jul 14, 2020

先理解函数的执行过程

  1. 创建执行上下文
  2. 推入执行栈
  3. 执行代码
  4. 从执行栈弹出(前提是该执行上下文位于栈顶)

什么是执行栈?

执行栈(execution context stack,简称ECStack),它遵循一种LIFO(后进先出)的数据结构,该结构拥有存储在代码执行期间创建的所有执行上下文。一般来说,执行栈中总是保留一个全局上下文,当函数调用时,会创建一个函数的上下文并推入执行栈中,函数执行完毕后又从栈中弹出。

function foo() {
  console.log('I\'m foo')
}

function bar() {
  console.log('I\'m bar')
  foo()
}

bar()

以上代码整个执行过程执行栈的变化:

// 代码执行前
ECStack = [
  globalContext
]
// 执行bar
ECStack = [
  barContext,
  globalContext
]
// bar中调用foo
ECStack = [
  fooContext,
  barContext,
  globalContext
]
// foo执行完毕
ECStack = [
  barContext,
  globalContext
]
// bar执行完毕
ECStack = [
  globalContext
]

上下文的推入和弹出的目的是什么?

函数的执行依赖于上下文,上下文中有函数会用到的一些变量,它们保存在内存中。推入上下文意味着cpu会分配一片内存区域来保存上下文中的变量,方便函数调用。函数执行完毕,函数上下文中的变量也就没有存在的必要了,同时为了节省内存,上下文从执行栈中弹出便是理所当然。JavaScript中的一种特殊函数结构体叫闭包,情况又有所不同,后面会说到。

什么是执行上下文?

注:以下讨论的执行上下文针对ES6环境,ES3中的执行上下文可以移步JavaScript深入之执行上下文

执行上下文也叫执行上下文环境,它是为JavaScript代码的执行所创建的一个环境,这个环境又是什么呢?
想一想,我们在函数中常用的arguments、this等,我们并没有显示定义它们却可以使用,这大概就是这个环境带给我们的。

JavaScript中存在三种执行上下文:

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval函数执行上下文(使用较少,暂不讨论)

注:全局执行上下文和函数执行上下文拥有同样的属性,区别在于属性值不同,后面一一讨论。

执行上下文包括两个组件:

  1. 词法环境(LexicalEnvironment)组件
  2. 变量环境(VariableEnvironment)组件

所以一个执行上下文看起来长这样:

ExecutionContext = {
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

注:变量环境也是一个词法环境,所以它与词法环境拥有相同的属性。之所以存在两个环境,主要是为了区分定义变量的不同方式块级作用域(let和const)和var,前者存储在词法环境中,而后者存储在变量环境中。我们重点讨论词法环境,变量环境自行理解。

现在我们重点说说词法环境。

词法环境

每个词法环境都有三个属性组成:

  1. 环境记录(Environment Record)
  2. 外部环境的引用(outer)
  3. this

一个词法环境看起来长这样:

LexicalEnvironment = {
  EnvironmentRecord: {
    Type: "Object" // 全局执行上下文中
 // Type: "Declarative" 如果是函数执行上下文
  },
  outer: <null>,
  this: <global object>
}

我们分别来解释这三个属性。

1. 环境记录

环境记录是用于存储变量和函数声明时的位置,它也有两种类型:

  1. 声明式环境记录:存在于函数执行上下文中,该记录默认包含一个arguments对象。
  2. 对象环境记录:存在于全局执行上下文中,该记录默认包含一个window对象(浏览器中)。

另外,我们在上面说到过,JavaScript中四种声明变量的方式let、const、var和函数声明,只有var声明的变量存储在变量环境的环境记录中,其他三种都存储在词法环境的环境记录中。

我们看看如下代码:

function foo(x) {
  var a = 2
  let b = 3
  const c = 5
  console.log(a + b + c + x)
}

foo(10)

函数foo的执行上下文看起来像这样:

fooExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: { 0: 10, length: 1 },
      b: <uninitialized>, // uninitialized表示未初始化,此时访问该变量会导致错误,临时死区(TDZ)原理与此
      c: <uninitialized>  // 同上
    },
    ...
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      a: undefined
    },
    ...
  }
}

2. 外部环境的引用(outer)

在上例中,当全局执行上下文被创建时,一个内部属性 [[Environment]]被保存在函数foo上,它用于引用当前执行上下文中的词法环境,作用和之前[[Scope]](ES6中被删除)类似,因此,在函数foo执行上下文被创建时,会得到该属性。这意味着在函数foo的词法环境中没有找到的变量会顺着该属性继续寻找,函数在定义时就保存了该属性,这决定了JavaScript只能是词法作用域。

3. this

全局执行上下文中的this引用的是全局对象,在浏览器中指向window对象。
函数执行上下文的this值,取决于函数的调用方式。例如常见的调用方式:以对象方法的形式调用,会使this的值被设置为该对象;以函数的形式调用会this值被设置为window对象(浏览器中)或者undefined。
但关于this的细节远不于此,感兴趣的可以阅读ECMAScript规范中文中关于this值是如何确定的。

我们需要记住的在各种环境中this的取值:

  1. 对象方法的调用,this指向该对象
  2. 函数方式调用,this指向window(浏览器中)
  3. 构造函数中,this指向返回的实例
  4. call、apply借用方法的调用,this指向传入的对象
  5. bind同上
  6. 箭头函数中,this指向外层上下文中的this

上面的foo函数完整的执行上下文大概长这样:

fooExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: { 0: 10, length: 1 },
      b: <uninitialized>, // uninitialized表示未初始化,此时访问该变量会导致错误,临时死区(TDZ)原理与此
      c: <uninitialized>  // 同上
    },
    outer: <globalLexicalEnvironment>,
    this: <global object or undefined>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      a: undefined
    },
    outer: <globalLexicalEnvironment>,
    this: <global object or undefined>
  }
}

综上,我们解释了词法环境中的三个属性,现在通过一个例子来巩固一下函数整个执行过程吧。

为了减少创建上下文的复杂度,以及未来声明变量的最佳实践,例子中只用let、const声明变量,这意味着执行上下文中只存在一个词法环境(LexicalEnvironment)组件(前面说过,只有var声明会储存在变量环境组件中),代码默认在浏览器环境中执行。

let a = 2

function foo() {
  let b = 3
  
  function bar(c) {
    return a + b + c
  }
  
  return bar(5)
}

foo() // 10

分析以上代码:

  1. 扫描全局代码,创建全局执行上下文,同时内部属性[[Environment]]被保存在函数foo
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      global: <window object>,
      a: <uninitialized>,
      foo: <fn>
    },
    outer: <null>,
    this: <window>
  }
}
  1. 全局上下文入栈,执行全局代码,a被赋值为2

执行栈情况:

ECStack = [
  GlobalExectionContext
]
  1. 执行foo函数,扫描函数中的代码,创建foo函数的执行上下文,同时内部属性[[Environment]]被保存在函数bar
fooExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: { length: 0 },
      b: <uninitialized>,
      bar: <fn>
    },
    outer: <GlobalLexicalEnvironment>,
    this: <window>
  }
}
  1. foo上下文入栈,执行foo函数中的代码,b被赋值为3

执行栈情况:

ECStack = [
  fooExectionContext,
  GlobalExectionContext
]
  1. foo中调用bar函数,扫描bar中的代码,创建bar函数的执行上下文
barExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: { 0: 5, length: 1 }
    },
    outer: <fooLexicalEnvironment>,
    this: <window>
  }
}
  1. bar上下文入栈,执行bar函数中的代码,计算a + b + c,自己词法环境中没有变量a,于是顺着外部环境引用outer找到foo的词法环境,发现也没有,最终在全局环境中找到了该变量,值为2;同理,在foo的词法环境中找到了b,值为3;在自己的词法环境中找到了c,值为5,最终返回结果:10。

执行栈情况:

ECStack = [
  barExectionContext,
  fooExectionContext,
  GlobalExectionContext
]
  1. bar执行完毕,上下文从执行栈中弹出

执行栈情况:

ECStack = [
  fooExectionContext,
  GlobalExectionContext
]
  1. foo执行完毕,上下文从执行栈中弹出

执行栈情况:

ECStack = [
  GlobalExectionContext
]

至此,代码全部执行完毕

总结

ES6的出现,重写了某些概念,比如:变量对象、作用域链等,在文中都没有提到。如果细心观察,和以前的概念对比你会发现,其实,变量对象大概就是词法环境中的变量记录器,也就是环境记录,作用域链大概就是外部环境记录outer。而和以前的区别主要是关于变量的声明方式,let、const和var进行了区别对待。
所以,这位大佬的JavaScript深入之执行上下文可以继续食用,并且更易于理解。

@chenyong9528 chenyong9528 changed the title JavaScript之执行上下文 JavaScript之执行上下文和执行栈 Jul 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant