zoukankan      html  css  js  c++  java
  • 转:JS高级学习笔记(8)- JavaScript执行上下文和执行栈

    必看参考:

    请移步:博客园 JavaScript的执行上下文

    深入理解JavaScript执行上下文和执行栈

    JavaScript 深入之执行上下文

    写在开头

    入坑前端已经 13 个月了,不能再称自己为小白,那么现在就来学习一下 JS 的执行相关的知识。

    自己吹过的牛皮,含着泪跪着也要实现它! 比如,先定一个小目标:成为高级前端。加油!

    废话少说,进入正题

    执行上下文

    执行上下文(Execution context,EC)就是 JS 代码的执行环境,也称执行上下文环境。

    在 JS 中有三种代码运行环境:

    Clobal Code环境:JS代码默认的环境

    Function Code环境:代码进入函数时的环境

    Eval Code环境:使用eval()执行环境(不常用)

     当 JavaScript 代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。

    执行上下文栈

    JavaScript 引擎创建了执行上下文栈(Execution Context Stack)来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

    从上面的流程图,我们需要记住几个关键点:

    • JavaScript执行在单线程上,所有的代码都是排队执行

    • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。

    • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。

    • 浏览器的JS引擎总是访问栈顶的执行上下文

    • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

    执行上下文的生命周期

    执行上下文的声明周期包括三个阶段:创建阶段-执行阶段-回收阶段

    创建阶段

    当函数被调用时,但是未执行任何其内部代码之前,会做以下三件事:

    • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。

    • 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JS始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳到上一层父作用域中查找,直到找到该变量。

    • 初始化this

    JS在执行之前需要被解析,解析的时候会先创建一个全局执行环境,完成一系列变量提升,函数声明提升等操作;

    当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。

    一个函数在执行之前,也会创建一个函数执行环境,和全局上下文差不多,但是会多出 this、argument和函数的参数。

    this的值是在执行的时候才能确认,定义的时候不能确认!

    执行阶段

    设置变量的值、函数的引用,然后解释/执行代码

    回收阶段

    执行上下文出栈等待虚拟机回收执行上下文。

    测试例子

        var a = "global var";
        function foo() {
          console.log(a);
        }
        function outerFunc() {
          var b = "var in outerFunc";
          console.log(b);
          function innerFunc() {
            var c = "var in innerFunc";
            console.log(c);
            foo();
          }
          innerFunc();
        }
        outerFunc()
        /* 执行结果:
         * var in outerFunc
         * var in innerFunc
         * global var
         */

    分析:代码首先进入全局执行上下文(Clobal Execution context) ,然后函数调用,依次进入 outerFunc、innerFunc 和 foo 执行上下文中,执行上下文就可以表示如下:

    当JS代码执行的时候,第一个进入的总是默认的 全局执行上下文(Clobal Execution context),所以说他总是在 执行上下文栈(Execution context stack,ECS)最底层。

    在每一个上下文中,都有三个重要的属性:变量对象(Variable object,VO)作用域链(Scope chain)this。除了这三个比较重要的属性,Execution Context还可以有一些附加属性。

    变量对象 和 活动对象

    变量对象

    从上面的例子中看,在执行上下文中,会保存变量对象(Variable Object,VO)变量对象是执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般变量对象中会包含以下信息:

    • 变量(var, Variable Declaration)

    • 函数声明(Function Declaration,FD)

    • 函数的形参

    当JS代码运行中,如果试图寻找一个变量的时候,就会首先查找VO。对于例子一种的代码,Global Execution Context中的VO如下:

     

    注意,如果在上面例子中添加有下面语句,Global VO不会变化,这两句属于window中的变量。

    (function bar(){}) 
    baz = "property of global object"

    也就是说,对于VO,是有两种特殊情况的:

    • 函数表达式(与函数声明相对)不包含在VO之中

    • 没有使用var声明的变量(这种变量是,“全局”的声明方式,只是给Global添加一个属性,并不在VO中)

    活动对象

    只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时有活动对象(Activation object)扮演VO的角色。活动对象 是在进入函数上下文时刻被创建,它通过函数的arguments属性初始化。

    Argument Object是函数上下文里的激活对象AO中的内部对象,它包括下列属性:

    • callee:指向当前函数的引用

    • length:真正传递的参数的个数

    • properties-indexes:就是函数的参数值(按照参数列表从作到右排列)

    对于VO和AO的关系可以理解为:VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO,但是在函数Execution Context中,AO就会被创建。

    上面的例子开始执行outerFunc的时候,会有一个outerFunc的 活动对象 被创建:

    接下来需要学习JS解释器是怎么执行这一段代码的,以及怎么设置VO和AO的。

    创建VO/AO的一些细节

    当一段JS代码执行时,JS解释器会创建Execution Context,其实这里会有两个阶段:

    • 创建阶段(当函数被调用时,但是开始执行内部代码之前)

      • 创建 Scope Chain

      • 创建VO/AO

      • 设置 this 的值

    • 激活/代码执行阶段--就是执行阶段

      • 设置变量的值、函数的引用,然后解释/执行代码

    这里详细介绍一下“创建VO/AO”中的一些细节,因为这些内容将直接影响代码的运行行为。

    • 第一步,根据函数的参数,创建并初始化arguments object

    • 第二步,扫描函数内部的代码,查找函数声明(Function declaration)

      • 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中

      • 如果VO/AO有同名的函数,那么就进行覆盖

    • 第三步,扫描函数内部代码,查找变量声明(Variable declaration)

      • 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined"

      • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

    用下面的例子来认识“创建VO/AO”的细节

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

    对于上面的代码,在“创建阶段”,可以得到下面的Execution Context object:

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

    在"激活/代码执行阶段",Execution Context object就被更新为:

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

    例子分析

    Example 1

    (function(){
        console.log(bar);
        console.log(baz);
        
        var bar = 20;
        
        function baz(){
            console.log("baz");
        }
        
    })()

    在Chrome中运行代码运行后将输出:

    代码解释:匿名函数会进入“创建结果”,JS解释器会创建一个"Function Execution Context",然后创建Scope chain,VO/AO和this。根据前面的介绍,解释器会扫描函数和变量声明,如下的AO会被创建:

    所以,对于bar,我们会得到"undefined"这个输出,表现的行为就是,我们在声明一个变量之前就访问了这个变量。这个就是JavaScript中"Hoisting(提升)"。

    Example 2

    接着对上面的例子,进行一些修改:

    (function(){
        console.log(bar);
        console.log(baz);
        
        bar = 20;
        console.log(window.bar);
        console.log(bar);
        
        function baz(){
            console.log("baz");
        }
        
    })()

    运行这段代码会得到"bar is not defined(…)"错误。当代码执行到console.log(bar)的时候,会去AO中查找"bar"。但是,根据前面的解释,自调用函数中的"bar"并没有通过var关键字声明,所有不会被存放在AO中,也就有了这个错误。 因为在创建阶段是扫描函数内部的代码,而bar = 20;不是函数内部的代码。

    注释掉"console.log(bar);",再次运行代码,可以得到下面结果。"bar"在"激活/代码执行阶段"被创建。

     

    Example 3

    (function(){
        console.log(foo); // undefined
        console.log(bar); // func...
        console.log(baz); // func...
        
        var foo = function(){};
        
        function bar(){
            console.log("bar");
        }
        
        var bar = 20;
        console.log(bar); // 20
        
        function baz(){
            console.log("baz");
        }
        
    })()

    代码的运行结果为:

    代码中,最"奇怪"的地方应该就是"bar"的输出了,第一次是一个函数,第二次是"20"。

    其实也很好解释,回到前面对"创建VO/AO"的介绍,在创建VO/AO过程中,解释器会先扫描函数声明,然后"foo: <function>"就被保存在了AO中;但解释器扫描变量声明的时候,虽然发现"var bar = 20;",但是因为"foo"在AO中已经存在,所以就没有任何操作了。

    但是,当代码执行到第二句"console.log(bar);"的时候,"激活/代码执行阶段"已经把AO中的"bar"重新设置了。

    总结

    通过对VO/AO在"创建阶段"的具体细节,如何扫描函数声明和变量声明,就可以对JavaScript中的"Hoisting"有清晰的认识。

    所以说,了解JavaScript解释器的行为,以及相关的概念,对理解JavaScript代码的行为是很有帮助的。

  • 相关阅读:
    ActiveMQ 即时通讯服务 浅析
    Asp.net Mvc (Filter及其执行顺序)
    ActiveMQ基本介绍
    ActiveMQ持久化消息的三种方式
    Windows Azure Virtual Machine (27) 使用psping工具,测试Azure VM网络连通性
    Azure China (10) 使用Azure China SAS Token
    Windows Azure Affinity Groups (3) 修改虚拟网络地缘组(Affinity Group)的配置
    Windows Azure Storage (22) Azure Storage如何支持多级目录
    Windows Azure Virtual Machine (26) 使用高级存储(SSD)和DS系列VM
    Azure Redis Cache (2) 创建和使用Azure Redis Cache
  • 原文地址:https://www.cnblogs.com/houfee/p/10756227.html
Copyright © 2011-2022 走看看