JavaScript 执行上下文和执行栈

执行上下文和执行栈

开始之前,我们先看以下代码。

console.log(a)
// Uncaught ReferenceError: a is not defined
console.log(a)
// undefined
var a = 10

第一段代码报错很好理解,a 没有声明。所以抛出错误。

第二段代码中 a 的声明在使用 a 之后,打印 a 的值是 undefined。

也就是说在使用 a 的时候,a 已经被声明了。这就很奇怪了,明明 a 的声明在这一行下面,为什么这个时候就已经被声明了呢?

其实这就是变量提升的概念。本质上是因为当代码真正执行之前就已经做了一些准备工作。而这些工作跟执行上下文有着紧密的联系,我们需要先来了解什么是执行上下文。

执行上下文

简单来说执行上下文(Execution Context)就是执行代码的环境。所有的代码都在执行上下文中执行。

上面的例子都是在全局上下文中执行的,其实执行上下文可以分为下面这三种

  1. 全局执行上下文 (Global Execution Context)
  • 这是最基础或者默认的执行上下文,是代码一开始运行就会创建的上下文。
  • 一个程序中只会有一个全局执行上下文
  • 所有不在函数内部的代码都在全局执行上下文之中
  1. 函数执行上下文 (Functional Execution Context)
  • 当一个函数被调用时, 会为该函数创建一个上下文
  • 每个函数都有自己的执行上下文
  1. Eval 函数执行上下文 (Eval Function Execution Context)
  • 执行在 eval 函数内部的代码也会有它属于自己的执行上下文

下面有一个例子

var v = 'global_context'
function f1() {
    var v1 = 'f1 context'
    function f2() {
        var v2 = 'f2 context'
        function f3() {
            var v3 = 'f3 context'
            console.log(v3)
        }
        f3()
        console.log(v2)
    }
    f2()
    console.log(v1)
}  
f1()
console.log(v)
image

最外侧的是全局执行上下文,它有 f1 和 v 这两个变量,f1、f2、f3内部是三个函数执行上下文(Eval 函数执行上下文不是很常用,在这里不做介绍)。

通过上面我们了解了每个函数都对应一个执行上下文,实际代码中肯定会有很多的函数,甚至函数会嵌套函数,这些执行上下文是如何组织起来的呢?代码又是如何运行的呢?

其实这些都是执行栈的工作。

执行栈

执行栈,其他语言中被称为调用栈,与存储变量的那个栈的概念不同,它是被用来存储代码运行时创建的所有执行上下文的栈。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

Javascript 是一门单线程的语言,这就意味着同一个时间只能处理一个任务。因此引擎只会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

我们在上面的代码的执行过程可以归结为下面这个图:

image

文字版总结如下:

  1. 全局上下文压入栈顶
  2. 每执行某一函数就为其创建一个执行上下文,并压入栈顶
  3. 栈顶的函数执行完之后它的执行上下文就会从执行栈中弹出,将控制权交给下一个上下文
  4. 所有函数执行完之后执行栈中只剩下全局上下文,它会在应用关闭时销毁

执行上下文的创建

如果执行上下文抽象成为一个对象的话它是如下的对象

executionContextObj = {
    'scopeChain': { /* 变量对象(variableObject)+ 所有父级执行上下文的变量对象 */ },
    'variableObject': { /* 函数 arguments/参数,内部变量和函数声明 */ },
    'this': {}
}

其中 variableObject 不是一成不变的,按照时间顺序可以分为 VO 和 AO

  • VO 变量对象(Variable Object)

    • 它是执行上下文中都有的对象。
    • 执行上下文中可被访问但是不能被 delete 的函数标示符、形参、变量声明等都会被挂在这个对象上
    • 对象的属性名对应它们的名字,对象属性的值对应它们的值。
    • 该对象不能直接访问到
  • AO 活动对象(Activation object)

    • 当函数开始执行的时候,这个执行上下文儿中的变量对象就被激活,这时候 VO 就变成了 AO

因此执行上下文创建的具体过程如下:

  1. 找到当前上下文调用函数的代码
  2. 执行代码之前,先创建执行上下文
  3. 创建阶段:
    1. 创建变量对象:
      1. 创建 arguments 对象,和参数
      2. 扫描上下文的函数申明:
        1. 每扫描到一个函数什么就会用函数名创建一个属性,它是一个指针,指向该函数在内存中的地址
        2. 如果函数名已经存在,对应的属性值会被新的指针覆盖
      3. 扫描上下文的变量申明:
        1. 每扫描到一个变量就会用变量名作为属性名,其值初始化为 undefined
        2. 如果该变量名在变量对象中已经存在,则直接跳过继续扫描
    2. 初始化作用域链
    3. 确定上下文中 this 的指向
  4. 代码执行阶段
    1. 执行函数体中的代码,给变量赋值

注意:

  1. 全局上下文的变量对象初始化是全局对象
  2. 全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。
  3. 作用域链(scopeChain) 和 this 的指向我们后面再详细了解

我们看一个例子

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

在调用了 foo(22) 的时候,创建阶段如下所示

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

激活阶段如下

fooExecutionContext = {
    scopeChain: { ... },
    activationObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

注意

创建需要注意以下几点

  1. 创建阶段的创建顺序是:函数的形参声明并赋值 ==>> 函数声明 ==>> 变量声明
  2. 创建阶段处理函数重名和变量重名的策略不同,简单来说就是函数优先级高。
function foo(a){
    console.log(a)
    var a = 10
}
foo(20) // 20

function foo(a){
    console.log(a)
    function a(){}
}
foo(20) // ƒ a(){}

function foo(){
    console.log(a)
    var a = 10
    function a(){}
}
foo() // f a(){}

变量提升

通过上面的介绍我们其实就知道了变量提升这一现象的出现的根本原因就是执行上下文在创建的时候就会扫描上下文中的变量将其声明出来,并设置为 VO 的属性。

我们分析下面的代码来加深印象。

(function() {

    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };

    function foo() {
        return 'hello';
    }

}());​

我们来回答以下问题

  1. 为什么我们能在 foo 声明之前访问它?

回想 VO 的创建阶段,foo 在该阶段就已经被创建在变量对象中。因此可以访问它。

  1. foo 被声明了两次, 为什么 foo 展现出来的是 functiton,而不是undefined 或者 string

在创建阶段,函数声明是优先于变量被创建的。而且在变量的创建过程中,如果发现 VO 中已经存在相同名称的属性,则不会影响已经存在的属性。

因此,对 foo() 函数的引用首先被创建在活动对象里,并且当我们解释到 var foo 时,我们看见 foo 属性名已经存在,所以代码什么都不做并继续执行。

  1. 为什么 bar 的值是 undefined?

bar 采用的是函数表达式的方式来定义的,所以 bar 实际上是一个变量,但变量的值是函数,并且我们知道变量在创建阶段被创建但他们被初始化为 undefined。

参考

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

推荐阅读更多精彩内容