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执行上下文和执行栈 #1

Open
youdeliang opened this issue Nov 23, 2019 · 0 comments
Open

深入浅出JavaScript执行上下文和执行栈 #1

youdeliang opened this issue Nov 23, 2019 · 0 comments

Comments

@youdeliang
Copy link
Owner

前言

深入了解事物的背后原理,是进阶过程中必须要做和非常重要且值得花时间的事情。作为前端开发来说,JavaScript不言而喻是必备技能了,我想作为一个合格前端来说知道JavaScript程序的内部执行机制也是必须的,而执行上下文和执行栈是其中的关键概念之一,也是难点之一。理解它们同样有助于我们对事件循环机制、闭包、作用域等概念的理解。

执行栈(Execution Stack)

JavaScript是单线程的,所有这决定了同一时间只能做一件事情,其他的活动或事情只能排队等候了,于是就生成出一个等候队列的执行栈(Execution Stack)。

执行栈图:
执行栈

  • 首先创建一个全局执行上下文(globalContext),入栈进入栈底。
  • 每当执行到一个函数调用时都会创建一个可执行上下文(execution context)EC,并压入栈中(红色箭头方向)。
  • 当函数调用完成,Js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境(绿色箭头方向)。 这个过程反复进行,直到执行栈中的代码全部执行完毕。

当然这里执行栈要区别于内存中的栈,当JavaScript代码执行的时候会将不同的变量存于内存不同的位置:堆(Heap)、栈(Stack)中来加以区分。其中,堆里存放着一些对象,而栈中则存放着一些基础类型变量以及对象的指针。

JavaScript内存模型图:
内存模型

  • 调用栈(Call Stack):用于主线程任务的执行。
  • 堆(Heap):用于存放非结构数据,如程序分配的变量和对象。
  • 任务队列(Queue): 用于存放异步任务。

下面举个栗子来分析执行栈

function fun3() {
    console.log('fun3')
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();

上面代码中声明三个函数,函数fn1嵌套fn2fn2嵌套fn3,最后调用fn1函数。按照执行栈图,步骤如下:

1、首先会创建全局执行上下文。

ECStack = [
    globalContext
];

2、执行fun1函数,创建fun1函数执行上下文,fun1函数执行上下文被压入执行栈。

 ECStack = [
    fun1,
    globalContext
 ];

3、依次执行fun2fn3函数,重复步骤2。最终形成执行栈。

ECStack = [
    fun3,
    fun2,
    fun1,
    globalContext
];

4、fun3执行完毕,从执行栈中弹出,依次重复直到fun1

执行上下文(Execution Context)

通过上面分析:Js的运行采用栈(执行上下文栈,上下面都简称为执行栈)的方式对执行上下文进行管理,栈底始终是全局上下文,栈顶始终是正在被调用执行的函数的执行上下文。

基本概念

概念解释:执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念, JavaScript中运行任何的代码都是在执行上下文中运行。

简单理解:执行的上下文可以抽象的理解为一个对象。每一个执行的上下文都有一系列的属性:变量对象(variable object)this指针(this value)作用域链(scope chain)

执行上下文类型

  • 全局级别的代码:这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
  • 函数级别的代码:当执行一个函数时,运行函数体中的代码。
  • Eval的代码 :在Eval函数内运行的代码。

执行上下文生命周期

执行上下文生命周期分为创建阶段、执行阶段、执行完毕,掌握理解了执行上下文的声明周期过程,也就理解执行上下文了。如下图:


执行上下文是代码执行的一种抽象,而代码执行除了整个Js开始执行之外,代码的执行都是通过函数调用执行的,所以执行上下文生命周期的各个阶段其实是可以分别对应函数被调用时的初始化、执行、执行完毕阶段的。下面会详细的解释每个阶段的过程。

创建阶段

当函数被调用,但未执行任何其内部代码之前,会做以下三件事:创建变量对象建立作用域链确认this指向

1、变量对象(variable object)

变量对象的定义

如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。

变量对象的作用

可以说变量对象是与执行上下文相关的数据作用域(scope of data) 。它是与执行上下文关联的特殊对象,用于存储被定义在执行上下文中的变量(variables)、函数声明(function declarations) 、arguments

变量对象的创建过程

通过下面简单栗子来了解过程:

function add(num){
    var sum = 5;
    return sum + num;
}
var sum = add(4);

根据上面代码,创建变量对象的流程是:

  1. 检查当前执行环境上的参数列表,建立Arguments对象,并作为add VOarguments属性值。
  2. 检查当前执行环境上的function函数声明,每检查到一个函数声明,就在变量对象中以函数名建立一个属性,属性指向函数所在的内存地址。
  3. 检查当前执行环境上的所有var变量声明。每检查到一个var声明,如果VO中已存在function属性名则跳过,如果没有就在变量对象中以变量名新建一个属性,属性值为undefined

当进入全局上下文时,全局上下文的变量对象可表示为:

VO = {
    add: <reference to function>,
    sum: undefined,
    Math: <...>,
    String: <...>
    ...
    window: global //引用自身
}

2、作用域链 (Scope Chain)

函数上下文的作用域链在函数调用时创建的,包含活动对象AO和这个函数内部的[[scope]]属性。

实例

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); 

在这段代码中我们看到变量y在函数foo中定义(意味着它在foo上下文的AO中)z在函数bar中定义,但是变量x并未在bar上下文中定义,相应地,它也不会添加到barAO中。乍一看,变量x相对于函数bar根本就不存在。

函数bar如何访问到变量x?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。
[[scope]]是所有父级变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

根据上面代码我们逐步分析:

  1. 代码初始化时,创建全局上下文的变量对象。
globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};
  1. foo创建时,foo[[scope]]属性是:
foo.[[Scope]] = [
  globalContext.VO
];
  1. foo激活时(进入上下文),foo上下文的活动对象。
fooContext.AO = {
  y: 20,
  bar: <reference to function>
};
  1. foo上下文的作用域链为:
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];
  1. 内部函数bar创建时,其[[scope]]为:
bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];
  1. bar激活时,bar上下文的活动对象为:
barContext.AO = {
  z: 30
};
  1. bar上下文的作用域链为:
bar.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

3、确认this指向

首页,我们要明白this是执行上下文的一部分,而执行上下文需要在代码执行之前确认,而不是定义的时候,所以this指向是在执行的时候才能确认。

this指向的几种情况:

  • this总是指向直接调用它的对象,如果没有对象调用则指向全局window
  • 对于构造函数来说(new命令),this指向的是构造函数中空的对象。
  • 对于箭头函数来说,this继承箭头函数外层的函数,如果没有外层函数则指向全局window
  • callapplybind方法this指向的方法的第一个参数。

this指向问题应该说是基础中的基础问题了,这里就不详细举例说明,如果还不了解的童鞋,可以先阅读关于this、call、applay和bind关于箭头函数和普通函数这两篇文章。

执行阶段

活动对象(Activation Object)

当函数被调用者激活时,这个特殊的活动对象(activation object) 就被创建了。它包含普通参数(formal parameters) 与特殊参数(arguments)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。


根据上图,简单解释:在没有执行当前环境之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。

根据上面变量对象的实例。当add函数被调用时,add函数执行上下文被压入执行上下文堆栈的顶端,add函数执行上下文中活动对象可表示为

AO = {
    num: 4,
    sum: 5,
    arguments:{0:4}
}

最后,执行代码,调用执行栈进行管理。

总结

希望还没有理解掌握的童鞋可以多多学习,如果觉得这篇文章对你有所帮助欢迎给个 ❤❤。大家加油努力!!!

参考文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant