javascript执行上下文和变量对象

时间:2023-03-09 15:30:23
javascript执行上下文和变量对象

执行上下文(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); //
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"
}