美文网首页工作生活
前端基础查漏补缺(三):变量对象详解

前端基础查漏补缺(三):变量对象详解

作者: AizawaSayo | 来源:发表于2019-07-04 00:17 被阅读0次

    在JavaScript中,我们肯定需要声明变量和函数,可是JS解析器是如何找到这些变量的呢?我们还得对执行上下文有一个进一步的了解。

    我们已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。

    • 创建阶段:执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

    • 代码执行阶段:创建完成后执行代码,会完成变量赋值,函数引用,以及执行其他代码。

      执行上下文生命周期
    变量对象(Variable Object)

    变量对象的创建,依次经历了以下几个过程。

    1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
    2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
    3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
      看下下面这样的场景,既然变量声明的foo遇到函数声明的foo会跳过,可是为什么最后foo的输出结果仍然是被覆盖了?
    function foo() { console.log('function foo') }
    var foo = 20;
    
    console.log(foo); // 20
    

    因为上面的三条规则仅仅适用于变量对象的创建过程。也就是执行上下文的创建过程。而foo = 20(赋值而不是定义)是在执行上下文的执行过程中运行的,输出结果自然会是20。对比下例。

    console.log(foo); // function foo
    function foo() { console.log('function foo') }
    var foo = 20;
    
    // 真正的执行顺序为:
    
    // 1.首先将所有函数声明放入变量对象中
    function foo() { console.log('function foo') }
    
    // 2.其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
    // var foo = undefined;
    
    // 然后开始执行阶段代码的执行
    console.log(foo); // function foo
    foo = 20;
    

    根据这个规则,理解变量提升就变得十分简单了。因为创建过程都是会先去寻找是否有声明,赋值则是第二步执行时期才会发生的事。同时我们也能得出函数声明的优先级要大于var 声明的结论。

    // demo01
    function test() {
        console.log(a);
        console.log(foo());
    
        var a = 1;
        function foo() {
            return 2;
        }
    }
    
    test();
    

    在上例中,我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示

    // 创建过程
    testEC = {
        // VO 为 Variable Object的缩写,即变量对象
        VO: {
            arguments: {...},  //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
            foo: <foo reference>  // 表示foo的地址引用
            a: undefined
        },
        scopeChain: {}//作用域链,下一篇讲
    }
    

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

    这样,如果再面试的时候被问到变量对象和活动对象有什么区别,就又可以自如的应答了,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。

    // 执行阶段
    VO ->  AO   // Active Object
    AO = {
        arguments: {...},
        foo: <foo reference>,
        a: 1,
        this: Window
    }
    

    demo1真正的执行顺序就变成如下这样:

    function test() {
        function foo() {
            return 2;
        }
        var a;
        console.log(a);
        console.log(foo());
        a = 1;
    }
    
    test();
    
    // demo2
    function test() {
        console.log(foo);
        console.log(bar);
    
        var foo = 'Hello';
        console.log(foo);
        var bar = function () {
            return 'world';
        }
    
        function foo() {
            return 'hello';
        }
    }
    
    test();
    

    真正的执行顺序:

    function test() {
        function foo() {
            return 'hello';
        }
        //var foo;变量foo声明的同名属性已存在,不覆盖前面同名属性的值
        //如果是函数声明再次声明同名属性,则覆盖前面同名属性的值
        var bar;
        console.log(foo);//ƒ foo() {return 'hello';}
        console.log(bar);//undefined
    
        foo = 'Hello';
        console.log(foo);//Hello
        bar = function () {
            return 'world';
        }
    }
    
        test();
    
    全局上下文的变量对象

    以浏览器中为例,全局对象为window。
    全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window。

    // 以浏览器中为例,全局对象为window
    // 全局上下文
    windowEC = {
        VO: Window,
        scopeChain: {},
        this: Window
    }
    

    除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。

    扩展:声明变量有哪些方式
    address2='333';
    var address3='111'
    console.log(window.address2);//333
    console.log(window.address3);//undefined  是变量对象里的属性,而不是window下的直接属性
    

    直接使用:num=1;并不是声明了一个全局变量,而是创建了一个全局对象(window)的属性。
    var 声明变量,全局环境下会存储到window的变量对象里,是window全局变量的属性,而不是全局对象直接的属性,所以window.address3访问不到

    console.log(myName);//undefined
    console.log(webName);//undefined
    console.log(address);//address is not defined
    var myName='Nick';
    var webName;
    address;
    

    我们都知道var声明的变量有变量提升的功能,既可以声明之前访问到。由上面例子可以看出,如果address也是变量,就不会报未定义的错。
    明确一点,在ES规范明确规定只有用var、let或者const声明的才是变量或者常量。
    不使用var创建的address是全局对象的一个属性,属性并没有前置现象(变量提升),所以会报错。
    既然webName是变量,那为什么可以使用window.webName访问此变量,好像webName也是一个属性。其实并不是,变量在执行上下文中的变量对象(VO)中存储,是变量对象的一个属性,在全局执行上下文中,全局对象恰好是变量对象,于是全局作用域中的变量可以使用全局对象window访问。这也是为什么delete无法删除使用var声明的变量,可以删除不是用var声明的"变量"的原因(eval执行上下文除外)。

    由于变量声明自带不可删除属性,比较var num = 1 跟 num = 1,前者是变量声明,带不可删除属性,因此无法被删除;后者为全局变量的一个属性,因此可以从全局变量中删除。

    相关文章

      网友评论

        本文标题:前端基础查漏补缺(三):变量对象详解

        本文链接:https://www.haomeiwen.com/subject/tvszcctx.html