(译文)JavaScript中的执行上下文和执行栈

JavaScript中的执行上下文和执行栈(原文地址)

本文将深入探讨JavaScript中最重要的基础知识之一:执行上下文。通过对此篇文章的阅读,对以下几个方面的知识你将会有更加清晰的认识:

  • 解释器的执行机制
  • 为何函数和变量可以在声明前使用以及它们的值究竟是如何确定的
什么是执行上下文?

当代码在JS中运行时,代码的执行环境非常重要,JavaScript中可执行的代码分为以下几类:

  • 全局代码:代码首次执行时所进入的默认执行环境
  • 函数代码:函数体内的代码
  • Eval代码:eval内部的代码

我们可以在网上找到很多与作用域相关的文档等,本文为了便于知识点的理解,将执行上下文看作是当前代码执行所处的环境/作用域。下面是一个包括全局和函数上下文的代码示例:
img1
以上示例代码结构很简明,一个由紫色实线包裹的全局上下文和三个分别由绿色、蓝色和橙色实线包裹的函数上下文。每个程序中只能有一个可被其他程序所访问的全局上下文。

函数上下文可以有任意多个,并且每个函数在调用的时候都会产生一个新的函数上下文和一个私有的作用域,当前作用域中所声明的任何变量都不能被外部所直接访问或调用。上例中,函数可直接访问当前上下文外部声明的变量,但是外部函数上下文不能访问内部声明的变量或者函数。为何会出现这种情况呢?代码到底是怎么执行的呢?

执行环境栈

浏览器中JavaScript解释器的运行是单线程的。这也就意味着在浏览器中同一时刻只能做一件事情,其他行为或者事件需要在执行栈中排队等待。下图是对单线程的抽象展示:
img2
当浏览器首次加载脚本语言的时候,会默认进入全局执行上下文。如果在全局代码中调用其他函数,当前程序的时序会自动进入所调用的函数中,与此同时会创建一个新的执行上下文并将其压入执行栈的顶部。

如果在当前函数内部调用其他函数,执行过程如上所述。代码的执行流程会进入到内部函数中,创建一个新的执行上下文并将它压入执行栈的顶部。浏览器永远执行位于栈顶的执行上下文,并且一旦当前函数执行上下文执行结束,它将从栈顶弹出,执行控制权也会回到当前栈的新栈顶。这样,执行环境栈中的上下文就会被依次执行和弹出栈顶,直到回到全局上下文,下例所示:

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

代码自调用三次,i的值不断从1自增。每次函数foo被调用的时候,一个新的执行上下文就会创建。一旦当前上下文执行结束,它就会从栈顶弹出,回到栈顶的新的上下文,直到再次回到全局上下文。

img3

执行栈中需要记住的5个关键点:
  • 单线程
  • 同步执行
  • 唯一的一个全局上下文
  • 不限个数的函数上下文
  • 每个函数的调用都会产生一个新的执行上下文,即使是函数对自己的调用
详解执行上下文

截至目前我们已经知道每当一个函数被调用的时候,就会产生一个新的执行上下文。但是,在JavaScript解释器中,对每个执行上下文的调用都分为以下两个阶段:

  • 创建阶段[当函数被调用,内部代码被执行之前的阶段]:
    • 创建作用域链
    • 创建变量、函数、参数
    • 确定this的值
  • 激活/代码执行阶段:
    • 确定函数的值和引用,然后执行代码

因为可以将执行上下文概念性的描述为含有三个属性的对象:

executionContextObj = {
    'scopeChain':{/*variableObject+所有父类执行上下文的variableObject*/},
    'variableObject':{/*函数形参/实参,内部的变量和函数声明*/},
    'this':{}
}
激活/变量对象[AO/VO]

执行上下文对象是在函数被调用,但是在函数被执行前所产生的。也就是上文所述的阶段1—创建阶段。比部分中,解释器对执行上下文对象的创建主要是通过浏览函数的实参和形参、当前函数内部的变量声明和函数声明。这部分的浏览结果会成为执行上下文对象中的变量对象。

解释器对代码执行的伪逻辑概述:
  • 查找函数调用的代码
  • 在执行代码前,创建执行上下文
  • 进入创建上下文阶段:
    • 初始化作用域链
    • 创建变量对象:
      • 创建参数对象,检查上下文中的参数,初始化参数名称和值并创建引用副本
      • 浏览上下文中的函数声明:
        • 每找到一个函数,就在变量对象中添加一个新的属性,该属性命名为当前函数名,指向函数在内存中的引用
        • 如果函数名已经存在,所对应的属性值将被重写,指向新的函数引用
      • 浏览上下文中的变量声明:
        • 每找到一个变量声明,在变量对象中添加一个新的属性,该属性命名为当前变量名,并给该属性赋值为undefined
        • 如果变量名已经在变量对象中存在,将不进行任何操作,继续浏览当前上下文
      • 确定上下文中this的指向
    • 代码执行阶段:
      • 分配变量值并且逐行执行当前上下文中的代码

下面看一个例子:

function foo(i){
    var a = 'hello',
    var b = function privateB(){
        
    },
    function c(){
        
    }
}
foo(22);

当调用函数foo的时候,创建阶段如下所示:

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

正如所示,创建阶段确定了属性的名称,除了实参和形参以外并没有给他们赋值。一旦创建阶段完成,执行流进入函数内部并且激活/执行代码阶段,执行后的代码如下所示:

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

网上很多关于JavaScript中变量提升的定义,定义中指出变量和函数的声明会被提升至当前函数作用域的顶部。但是,并没有解释为什么会存在变量提升以及解释器如何创建激活对象,其实原因很简单,以下面的代码为例:

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

对于疑问和解答如下:

  • 为什么我们可以在声明foo前访问它?
    • 回顾创建阶段,变量在函数执行前已经被创建。因此在函数执行前,foo已经在激活对象中创建。
  • foo被声明了两次,为什么foo的类型是function而不是undefined或者string?
    • 尽管foo被声明两次,在创建阶段中,函数先于变量在激活对象中创建,并且如果激活对象中已经存在属性名,则不会影响已经存在的属性。
    • 所以,对于函数foo的引用首先在激活对象中已经创建,并且当解释器到达var foo语句,解释器发现在变量对象中foo已经被创建,因此就会跳过然后继续后续操作。
  • 为什么bar的值是undefined?
    • bar实际上是一个值为函数的变量,在创建阶段变量会被初始化为undefined 。
注:以上部分译自此文,如有侵权请告知;如有翻译不妥,还请各位读者指正。以下是我对本文知识点的简要总结。
简要总结
  • 每个函数被调用的时候,都会创建一个新的执行上下文,并将当前执行上下文压入栈顶
  • 每个执行上下文可以看作是具有以下3个属性的对象:
    • 作用域链
    • 变量对象/激活对象(VO/AO)
    • this
  • 每个执行上下文的建立分为两个阶段:创建阶段和执行阶段
  • 执行上下文创建阶段,变量对象VO初始化的先后顺序:函数参数、函数声明、变量声明。关于此部分两个常见问题的解答如下:
    • 1、"函数声明过程中,变量对象中如果已存在同名的属性,则替换它的值"这句话如何理解?以下述代码为例:
    function foo(i){
        console.log(i); // function pointer
        var i = function (){
            
        }
    }
    foo(2);
    
    变量对象初始化第一步:函数参数
    
    executionContextObj = {
        'scopeChain':{...},
        'variableObject':{
            arguments:{
                0:2,
                length: 1,
            },
            i:2
        }
    }
    
    变量对象初始化第二步:函数声明
       函数声明过程中,变量对象中已存在同名的属性i,将其值由"1"替换为新值"function"
    
    executionContextObj = {
        'scopeChain':{...},
        'variableObject':{
            arguments:{
                0:2,
                length: 1,
            },
            i: function (){
                
            }
        }
    }
    
    • 2、"变量声明过程中,变量对象中如果已存在同名的属性,则不进行任何操作"这句话如何理解?以下述代码为例:
  function foo(i){
      console.log(i); // function pointer
      var i = function (){
          
      },
      var i = 9;
  }
  foo(2);
  变量对象初始化第一步:函数参数
executionContextObj = {
    'scopeChain':{...},
    'variableObject':{
        arguments:{
            0:2,
            length: 1,
        },
        i:2
    }
}
  变量对象初始化第二步:函数声明
      函数声明过程中,变量对象中已存在同名的属性i,将其值由‘1’替换为新值‘function’
executionContextObj = {
    'scopeChain':{...},
    'variableObject':{
        arguments:{
            0:2,
            length: 1,
        },
        i: function (){
            
        }
    }
}
  变量对象初始化第三步:变量声明
      变量声明过程中,变量对象中已存在同名的属性i,不进行任何操作。
executionContextObj = {
    'scopeChain':{...},
    'variableObject':{
        arguments:{
            0:2,
            length: 1,
        },
        i: function (){
            
        }
    },
    'this':{...}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值