JavaScript中的执行上下文和堆栈

什么是执行上下文(execution context)和堆栈(stack)?

在本文中,我会深入分析Javascript中最基本的一个部分,执行上下文。读完本文,你会明白解析器是怎么去解析的,为什么有些函数和变量在没有申明之前就可以被调用。

什么是执行上下文?

Javascript代码在执行过程中,所处的执行环境是很重要的,这些执行环境可以分为以下几类:

  • 全局代码(Global code) - 代码开始执行的默认环境
  • 函数代码(Function code) - 函数体内部代码的执行环境
  • Eval代码(Eval code) - Eval函数内部代码的执行环境

这里的执行环境就是作用域,以下我们看一个列子:

img1.jpg (554×447)

这里有1个紫色的全局上下文(Global Context), 有1个绿色,1个蓝色和1个黄色组成的函数上下文(Function Context). 全局上下文永远只有1个,程序里面任何一个上下文都可以访问全局上下文。

函数上下文可以有很多,每次调用函数,就会创建一个新的函数上下文,并且这个新创建的函数上下文是私有的,外部无法直接访问。在上面的例子中,一个函数可以直接访问那些声明在当前上下文之外的变量,外部上下文无法直接访问申明在内部上下文的函数和变量。为什么会这样呢?

执行上下文堆栈

Javascript解析器在浏览器中是单线程的。也就是说,浏览器同一时间只能做一件事情,其它的事件和动作会排队在执行堆栈(Execution Stack)中。下面是一个单线程堆栈图:

当浏览器载入Javascript脚本的时候,它默认先进入全局执行上下文(Global execution context)。如果全局代码里面有调用函数,那么执行流程就会进入到函数内部,创建一个相应的执行上下文,并推入到执行堆栈的最顶端。

如果当前函数里面调用另外一个函数,那同样的,执行流程会进入到内部函数里面,创建一个相应的执行上下文,并把它推入到执行堆栈的最顶端。浏览器永远只会执行堆栈里面最上面的当前执行上下文。一旦函数执行完毕当前上下文,它会从堆栈最顶端弹出,并将控制权交给下面一个上下文。下面是一个递归函数及其堆栈:

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));

这个函数通过++i调用自己3次,每次调用foo函数,就会创建一个新的执行上下文。一旦上下文执行完毕,它会从堆栈中弹出,并将控制权交给下面一个上下文,如此循环直到回到全局上下文。

5个执行堆栈的要点:

  • 单线程
  • 同步执行
  • 全局上下文只有一个
  • 函数上下文没有个数限制
  • 每次调用函数(包括自调用),都会创建一个执行上下文

执行上下文详解

在Javascript解析器中,每次调用执行上下文,都会经过2个阶段:

1. 创建阶段(Creation Stage) [次阶段处在函数被调用,但函数内部代码还未被执行的时候]

  • 创建作用域链
  • 创建变量,函数和参数
  • 检测关键字this的值

2. 激活/代码执行阶段(Activation/Code Execution Stage) 

  • 赋值,引用函数,解析/执行代码

可以用一个带有3个property的object来表示执行上下文:

executionContextObj = {
    scopeChain: { /* variableObject + all parent execution context's variableObject */ },
    variableObject: { /* function arguments / parameters, inner variable and function declarations */ },
    this: {}
}

Activation / Object Variable [AO/VO]

这里executionContextObj是在创建阶段生成的,也就是在函数被调用,但是函数内部代码还未被执行的时候。解析器通过扫描传入函数的参数,函数内部声明的变量和函数来生成executionContextObj里面的variableObject。

下面我们总结一下解析器是如何解析运行代码的:

  1. 找到调用函数的代码
  2. 在执行该函数代码之前,创建执行上下文
  3. 进入创建阶段
    • 初始化作用域链
    • 创建变量对象(variable Object)
      • 创建参数对象(arguments object), 检查传入上下文的参数,初始化名值对,并创建一个引用副本
      • 扫描上下文中的函数声明
        • 每次找到函数,都会在变量对象中创建一个与函数名称一样的property,并让这个property指向该函数在内存中的位置
        • 如果该函数名称已经存在了,那就覆盖指针,使property指向新函数在内存中的位置
      • 扫描上下文中的变量声明
        • 每次找到变量,都会在变量对象中创建一个与变量名一样的property, 并初始化默认值undefined
        • 如果该变量已经存在,则跳过
    • 检测this关键字在当前上下文中的值
  4. 进入激活/代码执行阶段

让我们看下面一个列子:

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: { ... }
}

可以看到,除了形参 i 以外,创建阶段只是定义了变量对象的property,并没有给他们赋值。在创建阶段之后,程序流程进入激活/代码执行阶段,下面是函数执行完毕之后的样子:

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

谈谈Hoisting

网上有很多关于Javascript中Hoisting的定义,变量和函数声明都会被吊(hoisted)到函数作用域的最前端。但是没有解释为什么会这样,解析器是如何创建激活对象(activation object)的。其实这个不难解释,让我们看下面一个例子:

(function() {

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

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

    function foo() {
        return 'hello';
    }

}());

我们现在可以回答下面几个问题:

  • 为什么我们在没有声明foo之前就可以访问它?
    • 从创建阶段开始,我们知道在激活/代码执行阶段之前,变量已经被创建好了。所以在函数开始执行的时候,foo已经在激活对象中定义好了。
  • foo被声明了2次,为什么foo被解析成function而不是undefined或者string?
    • 尽管foo被声明了2次,但从创建阶段我们知道,函数会先于变量被创建出来。所以foo先以函数的形式存在于激活对象中。而在声明变量foo的时候,该名字已经在激活对象中存在了,就跳过了。所以最后foo是function。
  • 为什么bar是undefined?
    • bar其实是一个变量,赋值为一个函数。在创建阶段,bar被创建出来并赋值为undefined。

总结

希望现在你能领会Javascript解析器是如何解析代码的,从执行上下文和堆栈的角度去理解代码的运行结果。

 

原文:What is the Execution Context & Stack in JavaScript?

转载于:https://www.cnblogs.com/liangzi4000/p/3782994.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值