文章开始之前,我们先搞清楚两个概念

  1. 执行上下文(execution context)
  2. 变量对象(variable object)

执行上下文

我们都知道JavaScript的作用域一共分三种

  • 全局作用域
  • 函数作用域
  • eval作用域
  • 实际上每一次的函数调用都会有一个对应的执行环境,这个执行环境也叫做执行上下文。执行上下文是一个抽象的概念,函数每调用一次就会产生一个新的执行上下文。
  • 下面我们通过一段代码来理解下执行上下文的顺序:
  1. var fun0 = 'global context';
  2. console.log(fun0);
  3. function fun1(){
  4. console.log('fun1');
  5. function fun2(){
  6. console.log('fun2');
  7. function fun3(){
  8. console.log('fun3');
  9. }
  10. fun3();
  11. }
  12. fun2();
  13. }
  14. fun1(); // global context fun1 fun2 fun3

从上面代码结构我们可以看到在fun1内包含fun2,fun2中包含fun3,实际上是一层一层嵌套的。在代码执行的过程中,会先进入全局的fun0,当我们在调用fun1的时候控制权会从全局的fun0到fun1的这样一个执行上下文中,然后再调用fun2的时候控制权会从fun1到fun2,以此类推。当fun3调用结束以后会退回到fun2,当fun2调用结束之后会退回到fun1,当fun1调用结束后退回到fun0,等到整个代码都结束后就会依次输出global context fun1 fun2 fun3。这几个函数的每一次调用的时候都会产生一个对应的新的执行上下文。

变量对象

变量对象是一个抽象概念中的”对象”,不是JavaScript中真正意义上的对象。它用于存储执行上下文中函数声明、函数参数和变量。
VO按照如下的顺序存储:

  • 函数参数(若未传入,初始化该参数值为undefined)
  • 函数声明(若发生命名冲突,会覆盖)
  • 变量(初始化变量值为undefined,若发生命名冲突,会被忽略)
  • 在函数中有点特殊的是:函数中有一个概念叫做激活对象(AO),AO就是在函数调用的时候会有一个特殊的arguments,arguments在初始化阶段会被放置到AO对象中,初始化之后AO对象又会被叫做VO对象。
  • 下面我们来看一个例子:
  1. function textVo(x,f){
  2. var a = 1;
  3. var b = 2;
  4. function b(){};
  5. function f(){};
  6. var c = 'VO';
  7. var e = function(){};
  8. }
  9. textVo(3);

上面代码再解析过程当中,按照VO存储的顺序会先将函数的参数x存储到AO中,然后是参数f,然后是函数b和函数f,再然后是变量a、b、c、e。
注意:
由于函数声明若发生命名冲突会覆盖,所以参数f会被函数f所覆盖,最后存入到AO对象中的是函数f
由于变量命名冲突会被忽略,所以变量b不会被存储到AO中
解析过程如下:

  1. AO(textVo)={
  2. x:3,
  3. b:<ref to func "b">,
  4. f:<ref to func "f">,
  5. a:undefined,
  6. c:undefined,
  7. e:undefined
  8. }

执行过程如下:

  1. VO['a'] = 1;
  2. VO['b'] = 2;
  3. VO['c'] = 'VO';
  4. VO['e'] = function e(){};
  5. AO(textVo)={
  6. x:3,
  7. b:2,
  8. f:<ref to func "f">,
  9. a:1,
  10. c:'VO',
  11. e:function e(){};
  12. }

JavaScript的执行过程其实可以理解为赋值的过程,由于函数f已经在解析过程中处理完了,在执行的过程中我们就可以直接忽略掉。
注意:在执行过程也就是赋值的过程中,发现b的值是2,这个时候会把值直接赋给已经存在AO中的函数b,最后输出的结果是b为2,函数b被替换掉了,这一块要注意下。
概念搞清楚之后,我们来看下面这道面试题。

  1. alert(a);
  2. a();
  3. var a=3;
  4. function a(){
  5. alert(a);
  6. alert(1);
  7. }
  8. alert(a);
  9. a=6;
  10. a();

第一眼看到这道题的感觉是不是:???(((φ(◎ロ◎;)φ)));现在我们掌握了执行上下文和变量对象之后再来看这道题就会变的很清晰。
解析:代码在解析阶段首先去找参数,我们这里没有参数所以直接忽略,接下来找到函数声明a放进VO中,然后找到变量a被忽略;
执行:

  • 第一步:进入全局执行上下文,运行alert(a),因为在解析阶段a已经存在于VO中,所以弹出的是函数a的源代码(alert里面的a是函数a的引用,是一个指针指向函数a);然后弹出全局上下文。
  • 第二步:第二个a是调用函数a,进入函数a的执行上下文,执行函数a内部的alert(a),这个时候的a还是函数a的指针,所以还是弹出函数a的源代码,然后弹出1;弹出函数a的执行上下文。
  • 第三步:进入全局执行上下文,给a赋值为3。
  • 第四步:执行alert(a),这个时候VO中的a已经被赋值为3了,所以弹出3。
  • 第五步:给a赋值为6。
  • 第六步:由于现在VO中的a已经被赋值为6了,所以在调用a()的时候会报错:Uncaught TypeError: a is not a function。
  • 最后执行的结果为
  1. ƒ a(){
  2. console.log(a);
  3. console.log(1);
  4. }
  5. ƒ a(){
  6. console.log(a);
  7. console.log(1);
  8. }
  9. // 1
  10. // 3
  11. // Uncaught TypeError: a is not a function

以上就是我对执行上下文和变量对象的理解,不合理的地方望指出。