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

JS深入浅出 - 执行上下文 (Excution Context) #3

Open
jtwang7 opened this issue May 9, 2021 · 1 comment
Open

JS深入浅出 - 执行上下文 (Excution Context) #3

jtwang7 opened this issue May 9, 2021 · 1 comment

Comments

@jtwang7
Copy link
Owner

jtwang7 commented May 9, 2021

JS深入浅出 - 执行上下文 (Excution Context)

参考链接:
理解JavaScript的执行上下文
[译] 理解 JavaScript 中的执行上下文和执行栈
【你不知道的JavaScript】(三)执行上下文及其生命周期
JavaScript深入之执行上下文栈

1. 概念

执行上下文,可以通俗理解为当前正在执行代码所处的执行环境。

分类

执行上下文分为三类:

  • 全局执行上下文 [开始运行脚本时创建]
  • 函数执行上下文 [调用函数时创建]
  • Eval 执行上下文(不常碰到,后续提及上下文均指以上两种)

三个属性

执行上下文用代码可表示为一个对象,其包含三个重要的属性

两个阶段(生命周期)


执行上下文的生命周期可以分为两个阶段:

  1. 创建阶段
    1.1 生成变量对象
    1.2 创建作用域链
    1.3 绑定 this 指向
  2. 执行阶段
    2.1 变量赋值
    2.2 函数引用
    2.3 执行其他代码

一个结构

JavaScript 是单线程,这就意味着代码需要排队按顺序运行,但代码实际运行过程中,并不会按照代码书写方式从上至下一行一行地运行,而是一块一块地运行代码。这里提到的块级结构就是上述提到的“执行上下文”,而执行上下文运行的顺序,是由 JS 引擎维护的一个栈结构管理的,我们称之为“执行上下文栈”。

执行上下文栈(Execution context stack,ECS)

执行上下文栈,顾名思义,它是一个栈结构,在 JS 代码中用数组 [] 来表示。

ECStack = [];

在 JS 引擎开始运行脚本时,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它。全局执行上下文只有当整个应用程序结束的时候,才会被 ECStack 弹出,所以程序结束之前, ECStack 最底部永远有个 globalContext

ECStack = [globalContext]

执行上下文栈管理执行上下文的方式就是按函数调用顺序,将函数调用时产生的函数执行上下文压入栈顶,将栈顶已运行完毕的函数执行上下文弹出,弹出的函数执行上下文等待被垃圾回收机制回收。
假设 JS 正在运行以下代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

ECStack 内变化如下所示:

// 伪代码
// 调用了 fun1(),产生 fun1 执行上下文,将其压入栈顶。
ECStack.push(<fun1> functionContext);

// 开始执行 fun1 上下文,执行过程中 fun1 调用了 fun2。
// 创建 fun2 的执行上下文,将其压入栈顶。
ECStack.push(<fun2> functionContext);

// 暂停 fun1 的执行,开始执行 fun2 上下文。
// fun2 调用了 fun3,同上处理。
ECStack.push(<fun3> functionContext);

// fun3 执行完毕,弹出栈顶。
ECStack.pop();

// fun2执行完毕,弹出栈顶。
ECStack.pop();

// fun1执行完毕,弹出栈顶。
ECStack.pop();

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

我们可以发现,fun2 执行上下文创建和压入栈顶的时机是在 fun1 上下文的执行阶段。这也证实了函数执行上下文是在函数被调用时才会产生。当 fun2 执行上下文被压入栈顶后,fun1 上下文的执行阶段就会被暂停,运行 fun2 的上下文,这也是正确的,可参考下例:

function fun1 () {
  var i = 2;
  function fun2 () {
    console.log(i)
  }
  fun2();
}

function fun3 () {
  function fun4 () {
    console.log(i)
  }
  fun4();
  var i = 2;
}

fun1();
fun3();

fun1 结果输出:2
fun3 结果输出:undefined
产生上述结果差异的原因就在于:内部函数执行上下文入栈是在其父级函数执行上下文的执行阶段发生的。
过程分析:

  1. JS 运行脚本,全局执行上下文入栈。
ECStack = [globalContext]
  1. 调用 fun1(),创建 fun1() 函数执行上下文。
VO = {
  arguments = {
    length: 0
  },
  i: undefined,
  fun2: <function-object>
}
  1. 构建作用域链。
Scope = [VO, globalVO]
  1. 绑定 this 指向。
  2. 进入 fun1 上下文执行阶段,运行到变量赋值语句时,修改变量对象内的属性值。
AO = {
  arguments = {
    length: 0
  },
  i: 2,
  fun2: <function-object>
}
  1. 调用 fun2(),开始创建 fun2() 函数执行上下文。
  2. 执行 fun2() 函数上下文,遇到 console.log(i),在当前上下文没有找到 i,因此沿着作用域链向上查找,在 fun1 的变量对象中找到了 i === 2,因此输出 2。

fun3 执行过程同上,只是由于 fun4() 在被调用时,此时 fun3 的 var i = 2 还未被执行就被暂停了,因此变量对象中 i === undefined,最终输出 undefined


还有一个很有意思的点,一个函数被调用时,JS 引擎是从其父级变量对象中对函数进行引用的,看例子!

function foo() {
    console.log('foo1');
}

foo();  // foo2

function foo() {
    console.log('foo2');
}

foo(); // foo2

上例中,调用 foo() 时,我们怎么确定调用的是哪个 foo 定义呢?JS 引擎是这样执行的,创建全局上下文入栈,执行全局上下文,此时在执行阶段会运行 foo() 函数引用,JS 引擎会去变量对象中查找,结合变量对象的创建规则,我们知道函数声明会覆盖已存在的同名变量,因此 JS 引擎会从变量对象中查找到第二个函数定义,并执行它。

var foo = function () {
    console.log('foo1');
}

foo();  // foo1

var foo = function () {
    console.log('foo2');
}

foo(); // foo2

上例中,由于变量对象在扫描变量声明时,已存在的变量声明不会被覆盖,因此第二个 foo 变量声明实际会被跳过,而在创建阶段的变量对象中,实际只会存在 foo: undefined。在执行阶段,foo 首先遇到打印 foo1 的赋值语句,然后执行 foo(),然后遇到打印 foo2 的语句,修改 foo 变量,然后再执行第二个 foo()

@jtwang7
Copy link
Owner Author

jtwang7 commented May 10, 2021

建议在仔细阅读下述文章后,阅读本文:

Question:请分析下述代码在 JS 引擎中的执行过程

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    function foo() {
      console.log(scope2)
    }
    return foo;
}
checkscope();
foo();

Answer

  1. 脚本执行,初始化全局执行上下文,入栈。
globalContext = {
  VO: {
    scope: undefined,
    checkscope: <function-object>
  },
  Scope: [globalContext.VO],
  this: window,
}

ECStack = [globalContext ]
  1. 执行全局上下文
globalContext = {
  VO: {
    scope: "global scope",
    checkscope: <function-object>
  },
  Scope: [globalContext.VO],
  this: window,
}
  1. 调用 checkscope 函数,创建 checkscope 函数执行上下文,入栈。
checkscopeContext = {
  VO: {
    arguments: {
      length: 0
    },
    scope2: undefined,
    foo: <function-object>
  },
  Scope: [checkscopeContext.VO, globalContext.VO],
  this: window,
}

ECStack = [globalContext, checkscopeContext]

其中作用域链创建如下:

// 函数定义时,`[[scope]]` 内部属性包括了其父级及以上的变量对象。
checkscope.[[scope]] = [
    globalContext.VO
];

第一步:复制函数 [[scope]] 属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

第二步:

// 添加当前上下文的变量对象至 `[[scope]]` 生成作用域链
Scope = [checkscopeContext.VO].concat([[scope]])
  1. 执行函数上下文,激活 VO,修改 AO 的属性值,运行完后出栈。
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: "local scope",
    foo: <function-object>
  },
  Scope: [checkscopeContext.AO, globalContext.VO],
  this: window,
}

// checkscope 运行完毕,弹出栈顶
ECStack = [globalContext]
  1. 同理,遇到 foo 函数时做同样的分析(此处直接跳至 foo 上下文执行阶段)
fooContext = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [fooContext.AO, checkscopeContext.AO, globalContext.VO],
  this: window,
}

ECStack = [globalContext, fooContext ]
  1. foo 上下文执行阶段,执行 console.log(i),从作用域链中找到 i 值并打印,foo 函数执行完毕,出栈
ECStack = [globalContext]

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