JS执行上下文

微信图片_20210615164036.jpg

我们在学习作用域或者闭包时,总是绕不开执行上下文,执行栈等术语,那到底什么是执行上下文呢?

一、什么是执行上下文

执行上下文(Execution Context),简称EC。

网上有很多关于执行上下文定义的描述,简单理解一下,其实就是作用域,也就是运行这段JavaScript代码的一个环境。

二、执行上下文的组成和分类

1. 组成

对于每个执行上下文EC,都有三个重要的属性:

  1. 变量对象Variable Object(变量声明、函数声明、函数形参)
  2. 作用域链 Scope Chain
  3. this指针

2. 分类

执行上下文分为3类

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval执行上下文(几乎不用,暂时不做解释)

【全局执行上下文】

术语理解

代码开始执行前首先进入的环境。

特点

全局执行上下文有且只有一个。客户端中一般由浏览器创建,也就是window对象。

注意点

(1)使用var声明的全局变量,都可以在window对象中访问到,可以理解为windowvar声明对象的载体。

(2)使用let声明的全局变量,用window对象访问不到。

【函数执行上下文】

术语理解

函数被调用时,会创建一个函数执行上下文。

特点

函数执行上下文可以有多个,即使调用自身,也会创建一个新的函数执行上下午呢。

以上是对全局执行上下文和函数执行上下文的区别。

下面再来看看执行上下文的生命周期。

三、执行上下文的生命周期

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

  1. 创建阶段
  2. 执行阶段
  3. 回收阶段

1. 创建阶段

发生在当函数被调用,但是在未执行内部代码之前。

创建阶段主要做的事情是:

(1)创建变量对象 Variable Object(创建函数形参、函数声明、变量声明)

(2)创建作用域链 Scope Chain

(3)确定this指向 This Binding

我们先用代码来更直观的理解下创建阶段的过程:

function foo(i){
    var a = 100;
    var b = function(){};
    function c(){}
}
foo(20);

当调用foo(20)的时候,执行上下文的创建状态如下:

ExecutionContext:{
    scopeChain:{ ... },
    this:{ ... },
    variableObject:{
        arguments:{
            0: 20,
            length: 1
        },
        i: 20,
        c:<function>,
        a:undefined,
        b:undefined
    }
}

2. 执行阶段

创建完成后,程序自动进入执行阶段,执行阶段主要做的事情是:

(1)给变量对象赋值:给VO中的变量赋值,给函数表达式赋值。

(2)调用函数

(3)顺序执行代码

还是以上面的代码为例,执行阶段给VO赋值,用伪代码表示如下:

ExecutionContext:{
    scopeChain:{ ... },
    this:{ ... },
    variableObject:{
        arguments:{
            0: 20,
            length: 1
        },
        i: 20,
        c:<function>,
        a:100,
        b:function
    }
}

3. 回收阶段

所有代码执行完毕,程序关闭,释放内存。

上下文出栈后,虚拟机进行回收。
全局上下文只有当关闭浏览器时才会出栈。

根据以上内容,我们了解到执行上下文的创建需要创建变量对象,那变量对象到底是什么呢?

四、变量对象 VO 和 活动对象 AO

1. VO 概念理解

变量对象Variable Object,简称VO。简单理解就是一个对象,这个对象存放的是:全局执行上下文的变量和函数。

VO === this === Global

VO的两种特殊情况:

(1)未经过var声明的变量,不会存在VO

(2)函数表达式(与函数声明相对),也不在VO

2. AO 概念理解

活动对象Activation Object,也叫激活对象,简称AO
激活对象是在进入函数执行上下文时(函数执行的前一刻)被创建的。

函数执行上下文中,VO是不能直接访问,所以AO扮演了VO的角色。

VO === AO,并且添加了形参类数组和形参的值

Arguments Object是函数上下文AO的一个对象,它包含的属性有:

(1)callee:指向当前函数的引用

(2)length:真正传递参数的个数

(3)properties-indexes:函数的参数值(按照参数列表从左到右排列)

3. VO 的初始化过程

(1)根据函数参数,创建并初始化arguments

变量声明var、函数形参、函数声明

(2)扫描函数声明

函数声明,是变量对象的一个属性,其属性名和值都是函数对象创建出来的。若变量对象已经包含了相同名字的属性,则替换它的值。

(3)扫描变量声明

变量声明,即变量对象的一个属性,其属性名即变量名,其值为undefined。如果变量名和已经声明的函数名或者函数的参数名相同,则不影响已经存在的属性。

注:函数声明优先级高于变量声明优先级

五、示例分析

1. 如何理解函数声明中“若变量对象已经包含了相同名字的属性,则替换它的值”

用代码来理解一下:

function fun(a){
    console.log(a); // function a(){}
    function a(){}
}
fun(100);

我们调用了fun(100),传入a的值是100,为什么执行console语句后结果却不是100呢?别急,我们接着分析~

创建阶段:

步骤 1-1:根据形参创建arguments,用实参赋值给对应的形参,没有实参的赋值为undefined
AO_Step1:{
    arguments:{
        0: 100,
        length:1
    },
    a: 100
}

步骤 1-2:扫描函数声明,此时发现名称为a的函数声明,将其添加到AO上,替换掉已经存在的相同属性名称a,也就是替换掉形参为a的值。
AO_Step2:{
    arguments:{
        0: 100,
        length:1
    },
    a: 指向function a(){}
}

步骤 1-3:扫描变量声明,未发现有变量。

执行阶段:

步骤 2-1:没有赋值语句,第一行执行console命令,而此时a指向的是funciton,所以输出function a(){}

2. 如何理解变量声明中“如果变量名和已经声明的函数名或者函数的参数名相同,则不影响已经存在的属性”

用代码来理解一下

情景1:变量与参数名相同

function fun2(a){
    console.log(a); // 100
    var a = 10;
    console.log(a) // 10
}

fun2(100);

// 分析步骤:

创建阶段:
步骤 1-1:根据arguments创建并初始化AO
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

步骤 1-2:扫描函数声明,此时没有额外的函数声明,所以AO还是和上次一致
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,所以不修改已存在的属性。
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

执行阶段:
步骤 2-1:按顺序执行console语句,此时AO中的a是100,所以输出100.
步骤 2-2:执行到赋值语句,对AO中的a进行赋值,此时a是10。
步骤 2-3:按顺序执行,执行console语句,此时a是10,所以输出10。

情景2:变量与函数名相同

function fun3(){
    console.log(a); // function a(){}
    var a = 10;
    function a(){}
    console.log(a) // 10
}
fun3();

// 分析步骤:

创建阶段:
步骤 1-1:根据arguments创建并初始化AO
AO={
    arguments:{
        length:0
    }
}

步骤 1-2:扫描函数声明,此时a指向函数声明(Function Declaration)
AO={
   arguments:{
        length:0
   }, 
   a: FD
}

步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,则跳过,不影响已存在的属性。
AO={
   arguments:{
        length:0
   }, 
   a: FD
}


执行阶段:
步骤 2-1:执行第一行语句console,此时a指向的是函数声明,所以输出函数声明。
AO={
   arguments:{
        length:0
   }, 
   a: FD
}

步骤 2-2:执行第二句对AO中的变量对象进行赋值,所以a的值改为10。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

步骤 2-3:执行第三句,是函数声明,在执行阶段不会再将其添加到AO中,直接跳过。所以AO还是上次的状态。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

步骤 2-4:执行第四句,此时a的值是10,所以输出10。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

根据以上的示例,我们已经大致明白了EC以及EC的生命周期。

同时,我们知道函数每次调用都会产生一个新的函数执行上下文。

那么,如果有若干个执行上下文呢,JavaScript是怎样执行的?

这就涉及到 执行上下文栈 的相关知识。

六、执行上下文栈

1. 术语理解

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

简单理解就是若干个执行上下文组成了执行上下文栈。也称为执行栈、调用栈。

2. 作用

用来存储代码执行期间的所有上下文。

3. 特点

我们知道栈的特点是先进后出。可以理解为瓶子,先进来的东西永远在最底部。

所以

执行上下文栈的特点就是LIFO(Last In First Out)
也就是后进先出。

4. 存储机制

  1. JS首次执行时,会将全局执行上下文存入栈底,所以全局执行上下文永远在最底部。
  2. 当有函数调用时,会创建一个新的函数执行上下文存入执行栈。
  3. 永远是栈顶处于当前正在执行状态,执行完成后出栈,开始执行下一个。

5. 示例分析

我们用代码简单理解一下

示例1:

function f1(){
    f2();
    console.log(1)
}
function f2(){
    f3();
    console.log(2)
}
function f3(){
    console.log(3)
}
f1(); // 3 2 1

根据执行栈的特点进行分析:

(1)我们假设执行上下文栈是数组ECStack,则ECStack=[globalContext],存入全局执行上下文(我们暂且叫它globalStack

(2)调用f1()函数,进入f1函数开始执行,创建f1的函数执行上下文,存入执行栈,即ECStack.push('f1 context')

(3)f1函数内部调用了f2()函数,则创建f2的函数执行上下文,存入执行栈,即ECStack.push('f2 context')f2执行完成之前,f1无法执行console语句

(4)f2函数内部调用了f3()函数,则创建f3的函数执行上下文,存入执行栈,即ECStack.push('f3 context')f3执行完成之前,f2无法执行console语句

(5)f3执行完成,输出3,并出栈,ECStack.pop()

(6)f2执行完成,输出2,并出栈ECStack.pop()

(7)f1执行完成,输出1,并出栈ECStack.pop()

(8)最后ECStack只剩[globalContext]全局执行上下文

示例2:

function foo(i){
    if(i == 3){
        return 
    }
    foo(i+1);
    console.log(i) 
}
foo(0); // 2,1,0

分析:

(1)调用foo函数,创建foo函数的函数执行上下文,存入EC,传0i=0if条件不满足不执行,

(2)执行到foo(1),再次调用foo函数,创建一个新的函数执行上下文,存入EC,此时传入的i1if条件不满足不执行,

(3)又执行到foo(2),又创建新的函数执行上下文,存入EC,此时i2if条件不满足不执行

(3)又执行到foo(3),再次创建新的函数执行上下文,存入EC,此时i3if满足直接退出,EC弹出foo(3)

(4)EC弹出foo(3)后执行foo(2)剩下的代码,输出2foo(2)执行完成,EC弹出foo(2)

(5)EC弹出foo(2)后执行foo(1)剩下的代码,输出1foo(1)执行完成,EC弹出foo(1)

(6)EC弹出foo(1)后执行foo(0)剩下的代码,输出0foo(0)执行完成,EC弹出foo(0),此时EC只剩下全局执行上下文。

七、总结

  1. 全局执行上下文只有一个,并且在栈底。
  2. 当浏览器关闭时,全局执行上下文才会出栈。
  3. 函数执行上下文可以有多个,并且函数每调用执行一次(即使是调用自身),就会生成一个新的函数执行上下文。
  4. JS是单线程,所以是同步执行,执行上下文栈中,永远是处于栈顶的是执行状态。
  5. VO或是AO只有一个,创建过程的顺序是:参数声明>函数声明>变量声明
  6. 每个EC可以抽象为一个对象,这个对象包含三个属性:作用域链、VO/AOthis
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容