Javascript基础系列之执行上下文

前言

本文翻译自what-is-the-execution-context-in-javascript

概述

当Javascript代码执行的时候,在哪个执行环境中是非常重要的。它决定了当前作用域链VO(变量对象)this指向。在本文中,目的就是为了更深入了解执行上下文,并且看完以后对解释器工作原理有个更清晰的认识

执行上下文

执行上下文形成一般有以下几种方式(不考虑es6块级作用域)

  • Global Code :默认的全局环境(代码最开始执行的环境)
  • Function Code: 函数内部环境
  • Eval Code:暂不考虑

也就是说,在代码中存在两种环境,全局环境(作用域)和函数环境(作用域)。下面,看个例子(网络图)

EC_01.jpg

代码中存在一个全局作用域和对个函数作用域,函数内可以获取全局作用域中变量,反之则不行。这和解析器中执行栈有关系

执行栈

在浏览器中,Javascript是单线程的,也就是说同时只有一个事件发生,其他的事件暂时保存一个地方,这个地方就是执行栈

是数据结构中的一种,具有先进后出的特点,入栈出栈都是在栈的一端(栈顶)操作,效率高[时间复杂度为O(1)]

下图就是一个抽象的单线程栈图(网络图)

EC_02.jpg

正如大家所知,浏览器第一次加载脚本的时候,默认就入的就是全局上下文,接着全局上下文入栈(一直在栈低)。当执行代码进入到内部函数的时候,又会创建一个新的上下文,接着新的上下文入栈(在栈顶)。而浏览器每次只会执行栈顶上下文(当前上下文),一旦执行结束,就被弹出栈,如此循环,知道栈清空为止。下面例子展示一个递归函数以及他的调用栈

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

执行栈中新的上下文入栈前,会记录当前上下文执行情况的相关信息,以便下次执行的时候继续执行

执行上下文细节

我们已经知道每当一个新的函数被调用,一个新的上下文会被创建并压入执行栈。然而在Javascirpt解析器内部,一个上下文创建有两个阶段

  • 创建阶段(进入上下文阶段)
  • 激活/执行代码(执行阶段)

1. 创建阶段

  • 创建作用域链(chain scope)
  • 创建变量,函数,arguments对象,参数
  • 决定this指向

2. 激活/执行代码

  • 变量赋值,函数引用,执行代码

从概念上,我们可以把上下文看成一个拥有三个属性的对象

executionContextObj = {
    'scopeChain': {/*变量对象和父上下文的变量*/},
    'variableObject': { /*函数arguments和参数,内部变量和函数声明*/ },
    'this': {}
}

Variable Object(VO)

executionContextObj对象当函数调用时候被创建,此时函数还没有执行。此时解析器创建executionContextObj对象,并且扫描函数的传入的parameters(形参)和arguments(实参)、函数声明,变量声明,扫描结果都保存在executionContextObj对象的variableObject

整个解析器执行代码步骤如下

    1. 找到当前调用函数的代码
    1. 在执行函数代码之前,创建执行上下文(execution context)
    1. 进入创近阶段
    • 初始化作用域链
    • 创建VariableObject对象
      • 创建arguments对象,检查上下文找中的参数,初始化属性和属性值
      • 扫描上下文中的函数声明
        • 每找到一个函数声明,就在VariableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
        • 如果上述函数名已经存在于VariableObject下,那么对应的属性值会被新的引用所覆盖
      • 扫描上下文中的变量声明
        • 每找到一个变量声明,就在VariableObject下面用变量名建立一个属性,属性值为undefined
        • 如果变量名已经存在VariableObject中,直接忽略
    • 确定上下文中this指向
    1. 代码执行阶段
    • 执行函数体中的代码,一行一行地运行代码,给VariableObject中的变量属性赋值

看看下面一个例子

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

    };
    function c() {

    }
}

foo(22);

当调用foo(22)时,调用阶段executionContextObj看起来如下

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

当创建阶段完毕,进入执行代码阶段,代码执行完后,executionContextObj看起来如下

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

变量声明提升是怎么实现的

在一些文章中提到,在Javascipt中变量和函数声明会被提升到当前作用域顶端。但是并没有解释详细细节,但是此时我们应该是很清晰地知道原因。看下面示例代码

(function() {
    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined
    var foo = 'hello',
        bar = function() {
            return 'world';
        };
    function foo() {
        return 'hello';
    }
}());​

上面自执行函数调用的时候,就会创建执行上下文,例子中最上面可以访问foo以及bar变量,值分别为一个函数引用和undefined

  • 为什么我们可以在声明f变量前就可以获取foo
    • 因为在进入执行上下文创建阶段,代码还没有执行,这个时候就处理了arguments,函数声明和变量声明,被初始化他们的值,函数声明就在VariableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用,变量声明就在VariableObject下面用变量名简历一个属性,值为undefied。所以当执行代码的时候,其实foo和bar已经声明,所以可以拿到
  • foo被声明了两次,为什么foo是执行一个函数而不是undefined
    • 尽管foo被声明了两次,但是在Variable Object中函数声明先被创建,同时因为Variable Object中如果属性名已经存在,会面声明就会被忽略,所以foo是函数引用,而不是undefind

总结

现在我们对Javascipt中解析器执行代码已经有一个很清晰的概念,也知道了执行上下文和执行栈,这样可以帮助我们写出更高质量的Javascipt代码

参考文档

  • what-is-the-execution-context-in-javascript

相关文章

  • chapter-2-variable-object/
  • chapter-2-variable-object中文版/
  • Identifier Resolution, Execution Contexts and scope chains

你可能感兴趣的:(Javascript基础系列之执行上下文)