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

前言

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

本文已经同步至GitHub博客,欢迎帅哥美女前来Star!!!

执行栈(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函数内运行的代码。

执行上下文生命周期

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

image

执行上下文是代码执行的一种抽象,而代码执行除了整个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)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。

image

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

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

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

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

总结

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

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335