Loading

调用堆栈(二)-执行上下文和变量对象

变量提升

JS是单线程的语言,执行顺序肯定是顺序执行,但是JS引擎并不是一行一行地分析和执行程序,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。

1、变量声明提升
变量声明提升我们在JS基础部分已经说过,再简单看一下例子吧:

  console.log(a); // undefined
  var a = 10;
  console.log(a); // 10

这里第一行代码并不会报错,因为var声明的变量会进行提升到顶部,实际上的执行过程是:

  var a ; // 声明变量a
  console.log(a);
  a = 10;
  console.log(a)

2、函数声明提升
定义函数也有两种方法:

  • 函数声明: function foo() {};
  • 函数表达式: var foo = function() {};

第二种函数表达式的声明方式更像是给一个变量foo赋值一个匿名函数。
那这两种方式有什么区别呢?

eg1:

  console.log(f1)  //f f1(){}
  function f1() {}  // 函数声明
  console.log(f2)   // undefined
  var f2 = function() {}  // 函数表达式

可以看到,使用function函数声明的方式会将整个函数都提升到作用域的最顶部,因此打印出来的是整个函数。
而使用函数表达式声明则类似于变量声明提升,将var f2提升到了顶部并赋值undefined。

eg2:

  console.log(f1);
  f1(); 
  function f1() {
        console.log('1')
  }
  console.log(f2);
  f2();
  var f2 = function() {
        console.log('2')
  }

我们可以看到f1()在function f1() {}之前,但是却可以正常执行;而f2()却会报错,原因在上面的例子里也介绍了,因为在调用f2时,f2还只是undefined并没有被赋值为 一个函数,因此会报错;因此在使用变量声明函数的时候,一定要在声明之后才能调用。

eg3:

  foo(); // foo2
  function foo() {
        console.log('foo1');
  }

  foo(); // foo2

  function foo() {
        console.log('foo2');
  }
  
  foo(); // foo2

通过这个例子我们可以看到,如果同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。

eg4:

  foo();  // foo2
  var foo = function() {
        console.log('foo1')
  }

  foo(); // foo1, foo重新赋值

  function foo() {
        console.log('foo2')
  }

  foo(); // foo1

从这个例子中我们可以看到,函数声明优先级高于变量声明,也就是两种声明方式都存在时,使用function关键字的函数声明会先声明,使用变量的是后声明,所以这个例子也就会出现了第一次执行时打印foo2,而当执行到后面两次的foo()函数时,使用变量声明的foo函数覆盖了使用function 声明的foo函数。

执行上下文栈

执行上下文栈在调用堆栈(一)中已经进行了一些介绍,因为JS引擎创建了很多的执行上下文,所以JS引擎创建了执行上下文栈(Execution context stack, ECS)来管理执行上下文。

当JS初始化的时候会向执行上下文栈压入一个全局执行上下文,我们用globalContext表示它,并且只有当整个应用程序结束的时候,执行栈才会被清空,所以程序结束之前,执行栈最底部永远有个globalContext。

  ESStack = [ //使用数组模拟栈
        globalContext
  ]

下面看两段代码:

代码1:

  var scope = "global scope";
  function checkscope() {
        var scope = "local scope";
        function f() {
              return scope;
        }
        return f();
  }
  checkscope();

代码2:

  var scope = "global scope";
  function checkscope() {
        var scope = "local scope";
        function f() {
              return scope;
        }
        return f
  }
  checkscope()();

这两段代码执行结果是一样的,但是它们的执行上下文栈的变化不一样。

第一段代码:

  ECStack.push(<checkscope> functionContext);
  ECStack.push(<f> functionContext);
  ECStack.pop();
  ECStack.pop();

第二段代码:

  ECStack.push(<checkscope> functionContext);
  ECStack.pop();
  ECStack.push(<f> functionContext);
  ECStack.pop();

函数上下文

变量对象(VO):也就是variable object,创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中。
活动对象(AO):也就是activation object,进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活了。

活动对象和变量对象的区别在于:

  • 1、变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
  • 2、当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。

执行过程

首先来看看一个执行上下文(EC)被创建和执行的过程:
1、创建阶段
* 创建变量、参数、函数arguments对象;
* 创建作用域链;
* 确定this的值
2、执行阶段
变量赋值、函数引用,执行代码。

进入执行上下文

这个时候还没有执行代码
此时的变量对象会包括(如下顺序初始化):

  • 1、函数的所有形参(only函数上下文):没有实参,属性值设为undefined。
  • 2、函数声明:如果变量对象已经存在相同的属性,则完全替换这个属性。
  • 3、变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。

看个例子吧:

  function foo(a) {
        var b = 2;
        function c() {}
        var d = function() {};

        b = 3;
  }
  foo(1)

对于上面的代码,这个时候的AO是

  AO = {
        arguments: {
              0: 1,
              length: 1
        },
        a: 1,
        b: undefined,
        c: reference to function c(){},
        d: undefined
  }

形参arguments这时候已经有赋值了,但是变量还是undefined,只是初始化的值。

代码执行

这个阶段会顺序执行代码,修改变量对象的值,执行完成后AO如下

  AO = {
        arguments: {
              0: 1,
              length: 1
        },
        a: 1,
        b: 3,
        c: reference to function c(){},
        d: reference to FunctionExpression 'd'
  }

总结如下:

1、全局上下文的变量对象初始化是全局对象
2、函数上下文的变量对象初始化只包括Arguments对象
3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
4、在代码执行阶段,会再次修改变量对象的属性值

另外需要注意的是:
ES6支持新的变量声明方式 let 、const,规则与 var 完全不同,他们是在上下文的执行阶段开始执行,避免了变量的提升带来的一系列问题。

参考:
https://m528964214.blog.csdn.net/article/details/87909673
https://muyiy.cn/blog/1/1.2.html#参考

posted @ 2020-11-22 17:21  Yang-0394  阅读(132)  评论(0)    收藏  举报