• js基础梳理究竟什么是变量对象,什么是活动对象?


    首先,回顾下上篇博文中js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?的执行上下文的生命周期:

    3.执行上下文的生命周期

    3.1 创建阶段

    • 生成变量对象(Variable object, VO)
    • 建立作用域链(Scope chain)
    • 确定this指向

    3.2 执行阶段

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

    1.什么是变量对象(Variable Object)

    在写程序的时候会定义很多变量和函数,那js解析器是如何找到这些变量和函数的?

    变量对象是与执行上下文对应的概念,在执行上下文的创建阶段,它依次存储着在上下文中定义的以下内容:

    1.1 函数的所有形参(如果是函数上下文中):

    建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。没有实参的话,属性值为undefined。

    1.2. 所有函数声明:(FunctionDeclaration, FD)

    检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果变量对象已经存在相同名称的属性,则完全替换这个属性。

    1.3. 所有变量声明:(var, VariableDeclaration)

    检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

    2.什么是活动对象?(activation object, AO)

    • 只有全局上下文的变量对象允许通过VO的属性名称来间接访问,在其他上下文(后面干脆直接讲函数上下文吧,我们并没有分析eval上下文)中是不能直接访问VO对象的。

    • 在函数上下文中,VO是不能直接访问的,此时由活动对象AO继续扮演VO的角色。

    未进入执行阶段前,变量对象中的属性都不能访问!但是进入到执行阶段之后,变量对象转变成了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。

    因此,对于函数上下文来讲,活动对象与变量对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于执行上下文栈栈顶的函数执行上下文中的变量对象,才会变成活动对象。

    3.举个例子

    说了一堆概念,有点懵,对吗?请看这个例子:

    var a = 10;
    function b () {
        console.log('全局的b函数')
    };
    function bar(a, b) {
        console.log('1', a, b) 
        var a = 1
        function b() {
            console.log('bar下的b函数')
        }
        console.log('2', a, b) 
    }
    bar(2, 3)
    console.log('3', a, b)
    

    要想知道为什么会这样打印,首先,从执行上下文的创建阶段来分析变量对象:

    // 创建阶段:
    // 第一步,遇到了全局代码,进入全局上下文,此时的执行上下文栈是这样
    ECStack = [
        globalContext: {
            VO: {
                // 根据1.2,会优先处理全局下的b函数声明,值为该函数所在内存地址的引用
                b: <reference to function>,
                // 紧接着,按顺序再处理bar函数声明,此时根据1.1,因为是在全局上下文中,并不会分析bar函数的参数
                bar: <refernce to function>,
                // 根据1.3,再处理变量,并赋值为undefined
                a: undefined
            }
        }
    ];
    // 第二步,发现bar函数被调用,就又创建了一个函数上下文,此时的执行上下文栈是这样
    ECStack = [
        globalContext: {
            VO: {
                b: <reference to function b() {}>, 
                bar: <refernce to function bar() {}>,
                a: undefined
            }
        },
        <bar>functionContext: {
            VO: {
                // 根据1.1,优先分析函数的形参
                arguments: {
                    0: 2,
                    1: 3,
                    length: 2,
                    callee: bar
                },
                a: 2,
                // b: 3,
                // 根据1.2, 再分析bar函数中的函数声明b,并且赋值为b函数所在内存地址的引用, 它发现VO中已经有b:3了,就会覆盖掉它。因此上面一行中的b:3实际上不存在了。
                b: <refernce to function b() {}>
                // 根据1.3,接着分析bar函数中的变量声明a,并且赋值为undefined, 但是发现VO中已经有a:2了,因此下面一行中的a:undefined也是会不存在的。
                // a: undefined
            }
        }
    ]
    

    以上就是执行上下文中的代码分析阶段,也就是执行上下文的创建阶段。再看看执行上下文的代码执行阶又发生了什么。

    // 执行阶段:
    // 第三步:首先,执行了bar(2, 3)函数,紧接着,在bar函数里执行了console.log('1', a, b)。全局上下文中依然还是VO,但是函数上下文中VO就变成了AO。并且代码执行到这,就已经修改了全局上下文中的变量a.
    ECStack = [
        globalContext: {
            VO: {
                b: <reference to function b() {}>, 
                bar: <refernce to function bar() {}>,
                a: 10,
            }
        },
        <bar>functionContext: {
            AO: {
                arguments: {
                    0: 2,
                    1: 3,
                    length: 2,
                    callee: bar
                },
                a: 2,
                b: <refernce to function b() {}>
            }
        }
    ]
    
    // 因此会输出结果: '1', 2, function b() {console.log('bar下的b函数')};
    
    // 第四步:执行console.log('2', a, b)的时候, 发现里面的变量a被重新赋值为1了。
    ECStack = [
        globalContext: {
            VO: {
                b: <reference to function b() {}>, 
                bar: <refernce to function bar() {}>,
                a: 10,
            }
        },
        <bar>functionContext: {
            AO: {
                arguments: {
                    0: 2,
                    1: 3,
                    length: 2,
                    callee: bar
                },
                a: 1,
                b: <refernce to function b() {}>
            }
        }
    ]
    // 因此会输出结果: '2', 1, function b() {console.log('bar下的b函数')};
    
    // 第五步,执行到console.log('3', a, b)的时候,ECStack发现bar函数已经执行完了,就把bar从ECStack给弹出去了。此时的执行上下文栈是这样的。
    
    ECStack = [
        globalContext: {
            VO: {
                b: <reference to function b() {}>, 
                bar: <refernce to function bar() {}>,
                a: 10,
            }
        }
    ]
    
    // 因此会输出结果: '3', 10, function b() {console.log('全局的b函数')}
    

    总结一下,变量对象会有以下四种特性:

    1. 全局上下文的变量对象初始化是全局对象(其实这篇文章并没有介绍这个特性,不过它也很简单就这么一句话而已)
    2. 函数上下文的变量对象初始化只包括Arguments对象
    3. 在进入执行上下文的时候会给变量对象添加形参,函数声明,变量声明等初始的属性值
    4. 在代码执行阶段,会再次修改变量对象的属性值。

    理解了这些,是不是发现再有一些函数提升,变量提升什么的是不是都很简单了。例如,你可以思考下这三段代码分别发生了什么。

    foo() 
    var foo = function() {console.log(1)}
    function foo() {console.log(2)}
    
    foo() 
    function foo() {console.log(2)}
    var foo = function() {console.log(1)}
    
    var foo = function() {console.log(1)}
    function foo() {console.log(2)}
    foo() 
    
  • 相关阅读:
    Spring中的@Transactional以及事务的详细介绍
    Shiro缓存使用Redis、Ehcache、自带的MpCache实现的三种方式实例
    使用shiro缓存用户身份信息的时候报:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource
    rocketmq 延时消息
    用区块链技术做一个 不可被修改的 恋爱记录 app 我叫<<誓言>>
    java 调用区块链 发布和调用智能合约
    centos 以太坊多节点私链搭建
    数据库的死锁原因 和 处理办法
    聚簇索引
    Java 容易疑惑的一些杂记录
  • 原文地址:https://www.cnblogs.com/hezhi/p/10053025.html
Copyright © 2020-2023  润新知