执行上下文(execution context):
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。
js语言是一段一段的顺序执行,这个“段”其实就是我们说的这个执行上下文,分为:全局执行上下文,函数执行上下文,Eval函数执行上下文(很少用)。
执行上下文由以下几个属性构成:
executionContext:{
variable objects:var、function[、arguments]
scope chain:variable objects + all parents scope
thisValue:content object
}
执行上下文的代码分为两个阶段:
- 进入执行上下文
- 代码执行
进入执行上下文后初始化的规则如下(且按如下顺序执行):
- 函数的所有形参(这一条是在函数上下文中才用到):
- 由名称和对应值组成的一个变量对象的属性被创建
- 如果没有传实参,属性值将置为undefined
- 函数声明
- 由名称和该函数体组成的一个变量对象的属性被创建
- 如果有两个同名函数声明,后者会替换前者
- 变量声明:
- 由名称和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代码执行前,执行上下文处在创建阶段,在创建阶段会确定如下三个事情:
- 确定this的值(即This Binding)
- 创建词法环境(LexicalEnvironment)
- 创建变量环境(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" }