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

图片描述

不是很准确的草图,大概了解就好。

  • 首先JS解释器(引擎)开始解释代码,构建执行环境栈(Execution Context Stack),并根据执行环境的不同生成不同的执行上下文(Execution Context)

  • 栈底永远是全局上下文(后面说),当遇到test(),确认调用函数,就创建生成test函数自己的上下文,然后将函数执行上下文入栈(push on)到执行环境栈中。

  • testEC(test Execution Context)将会开始创建变量对象,我们知道,当调用一个函数(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。

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

    2. 代码执行阶段:
      创建完成后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

  • 所以testEC将会生成一个变量对象,与之还有作用域链、this(这里先不讨论),注意,这里是变量对象的创建阶段,用VO简示。

    变量对象的创建,经历以下过程。

    1. 建立arguments对象。检查上下文中的参数,建立该对象下的属性与属性值(key-value)

    2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

    3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

所以,于是就有了图中的VO引用的对象的图示,这就是变量提升的真正原因,对于函数声明,它的优先级是大于变量声明的,所以在创建阶段,函数声明的函数名就持有了函数的引用。而变量赋值需要在后面的执行阶段才被赋值。

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

  • 进入执行阶段,这时可称之为活动对象了(Active Object),将进行赋值操作。于是这时的变量就能被自由访问了。

关于arguments对象,是个类数组结构

目前我的测试:

  • 函数内设有形参,但是不传实参,遍历arguments对象时没有任何值

  • 函数内不设形参,但是传实参,遍历arguments对象有值对应的索引

    function test(a,b,c,d) {
        for (var i in arguments) {
            console.log(i);
        }
        console.log(arguments.length+"个");
    };
    test(1,2,3,4);
    // 0 1 2 3
    // "4个"
    function test2(a,b,c,d) {
        for (var i in arguments) {
            document.write(i+"<br />")
        }
        document.write(arguments.length+"个");
    };
    test2();
    // 什么也没有
    // 0个

脑子里有这副图后,我们在来看另一个栗子:


      function test() {
        console.log(foo());
        console.log(bar);
        
        var foo = "Hello";
        console.log(foo);
        var bar = function () {
            return "world";
        };
        function foo() {
            return "hello";
        };
    };
    test();
    // "hello"
    // "undefined"
    // "Hello"

主要讨论同变量声明与同变量声明与函数声明的情况,这也是变量对象在创建时所做的工作;

检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

  • 如代码中的foo,在test函数的执行上下文创建变量对象后,创建阶段,foo就是变量对象中的的键名(Key),而键值就是函数的地址指针,因为函数声明的优先级大于变量声明,所以foo此时就持有了函数的引用,而var foo的变量声明就被跳过。

  • 执行阶段,进行赋值操作,foo被重新赋值"Hello",同时变量bar从未赋值(undefined)到持有一个函数的引用,这就是变量对象=>活动对象,执行栈,执行上下文所发生的操作。


关于全局上下文

    var globla = 10;
    function test() {
        return globla++;
    };
    test();


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

以浏览器中为例,全局对象为window
全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window
除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。


Queen
139 声望20 粉丝