执行上下文(execution context):

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。

js语言是一段一段的顺序执行,这个“段”其实就是我们说的这个执行上下文,分为:全局执行上下文,函数执行上下文,Eval函数执行上下文(很少用)

执行上下文由以下几个属性构成:

executionContext:{

  variable objects:var、function[、arguments]

  scope chain:variable objects + all parents scope

  thisValue:content object

}

执行上下文的代码分为两个阶段:

  1. 进入执行上下文
  2. 代码执行

进入执行上下文后初始化的规则如下(且按如下顺序执行):

  1. 函数的所有形参(这一条是在函数上下文中才用到):
    • 由名称和对应值组成的一个变量对象的属性被创建
    • 如果没有传实参,属性值将置为undefined
  2. 函数声明
    • 由名称和该函数体组成的一个变量对象的属性被创建
    • 如果有两个同名函数声明,后者会替换前者
  3. 变量声明:
    • 由名称和undefined组成的一个变量对象的属性被创建
    • 如果变量名称和已经声明的形参或者是函数名相同,则变量声明不会替代已经存在的这类属性
        function test() {
            console.log(a);    // a is not defined
            a = 1;
        }

        test();

        function test2() {
            b = 1;
            console.log(b);    // 1,因为执行这句的时候b已经自动升级成了全局变量所以打印1
        }

        test2(); 

例子1:执行test()报错是因为:没有var声明的变量不会发生变量提升!!

 

        funA;  // undefined
        var funA = function () {
            console.log('输出a1');
        }

        funA();  // 输出a1

        var funA = function () {
            console.log('输出a2');
        }

        funA();  // 输出a2

例子2主要是变量提升

var funA;

var funA = ...

funA()

var funA = ...

funA()

预编译阶段先初始化得到var funA=undefined,所以第一个funA输出undefined;

然后顺序执行,先把function(){ console.log('输出a1') }赋值给funA,然后执行funA();

然后顺序执行,再用function(){ console.log('输出a2') }替换当前funA的值,然后再执行。

        funA();  // 输出a2
        function funA() {
            console.log('输出a1');
        }

        funA();  // 输出a2

        function funA() {
            console.log('输出a2');
        }

        funA();  // 输出a2

例子3是函数提升

function funA

funA()

funA()

funA()

预编译阶段初始化的时候解析到function,后面的funA会替换前面的,因此,这三个函数执行都执行的是后一个funA。

 

        funA();  // 输出a2
        var funA = function () {
            console.log('输出a1');
        }

        funA();  // 输出a1

        function funA() {
            console.log('输出a2');
        }

        funA();  // 输出a1

例子4表示函数声明的优先级大于变量声明

var funA

function funA

funA()   【执行的是函数funA】

var funA = function(){}

funA() 【执行的是变量赋值后的funA】

funA()  【同上】

 

        console.log(number);  // ƒ number() {console.log('test')}
        function number() {
            console.log('test')
        }
        var number = 1;


        var number2 = 2;
        console.log(number2);  // 2
        function number2() {
            console.log('test')
        }

        function number3(x) {
            console.log(x);  // ƒ x() { }
            function x() { }
        }
        number3(5)

例子5

第一个demo是演示了函数提升和变量提升,但是由于function number()最先被提升,后面var number的提升会被忽略,所以第一个会输出函数体

第二个demo是因为预编译结束之后,直接给number2赋值,所以输出的是赋值后的number2

第三个demo说明函数声明的提升会覆盖函数参数。函数参数其实属于变量的一种形式,它的优先级最高,但是同样会受到函数声明的影响!

 

小结一下~

初始化规则是先处理函数声明,再处理变量声明

变量提升和函数提升通俗点说就是将变量和函数移动到代码顶,在创建阶段,js解释器会找到需要提升的变量和函数,并给他们在内存中开辟好空间,变量只声明并且赋值undefined,而函数会整个存入内存中!

在提升过程中,相同函数名的函数会覆盖前面的,函数提升会优先于变量提升

 

执行栈:

也称之为调用栈,是LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

JavaScript引擎首次读取脚本时,首先将全局执行上下文push到当前执行栈,每当发生函数调用,引擎会给该函数创建一个函数执行上下文并将它push到当前执行栈的栈顶,当栈顶的函数执行完成后,栈顶的函数执行上下文会从执行栈中pop出,交由下一个执行上下文,so程序结束之前,执行栈最底部永远是globalContext

作用域链(scope chain):

它在js解释器进入到一个执行环境时初始化完成,并将其分配给当前执行环境。每个执行环境的作用域链由当前环境的VO和父级环境的作用域链构成

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

上面这两个例子都输出“local scope”,两者的差别在于:执行栈的变化不一样!两者的流程如下:

demo1:

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

demo2:

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

具体的流程分析见JavaScript深入之执行上下文

 

执行上下文的创建:

执行上下文分为两个阶段创建:1.创建阶段; 2.执行阶段

1.创建阶段

在JavaScript代码执行前,执行上下文处在创建阶段,在创建阶段会确定如下三个事情:

  1. 确定this的值(即This Binding)
  2. 创建词法环境(LexicalEnvironment)
  3. 创建变量环境(VariableEnvironment)

 LexicalEnvironment和VariableEnvironment的区别:前者是存储function声明和let/const绑定,后者仅用于存储var绑定

对照概念理解:

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);
GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

只有遇到multiply函数调用时才会创建该函数执行上下文!

注意:(在声明之前访问变量的区别

let和cons定义的变量在创建阶段会保持未初始化状态,没有任何和它相关联的值,所以在声明之前访问let和const定义的变量会提示引用错误!

而var定义的变量会在声明的时候被置为undefined,所以在声明之前访问var定义的变量会输出undefined。

 

2.执行阶段

 完成对所有变量的分配,最后执行代码

 

变量对象(VO)

每一个执行上下文都会有一个相关联的变量对象,变量对象的属性由在执行上下文中定义的变量(variables)函数声明(function declaration)构成。

变量对象和当前作用域息息相关,不同作用域的变量对象互不相同!!

注意!!!函数声明会加到变量对象中,但是函数表达式则不会

// 函数声明
function a() {  
    ...
}

// 这个是函数表达式
var a = function funA(){  // a会作为变量存在VO中,但是funA不会存在VO中
    ...  
}

  在全局上下文中:当js编译器开始执行时会初始化一个Global Object,在浏览器端,Global Object == Windows对象 == 全局环境的VO。VO对于程序而言是不可读的,只有编译器才有权访问变量对象,因此Global Object对于程序而言是唯一可读的VO。

  在函数上下文中:参数列表(parameters)也会被加入到变量对象中作为属性。用活动对象(AO)来表示变量对象,活动对象是在进入函数上下文的时刻被创建,这时候对象上的各种属性才能被访问。

活动对象(activation object

调用函数时,会创建一个活动对象分配给执行上下文。AO由局部变量arguments初始化而成,所有作为参数传入的值都是该arguments数组的元素。随后,AO被当做VO用于变量初始化。

以我学习变量对象的例子为例对照记忆:

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"
}