执行上下文
在说到js执行上下文前先看一个例子
function f1() {
console.log('听雨是雨');
};
f1(); // echo
function f1() {
console.log('echo');
};
f1(); // echo
换种写法,输出结果又会改变
var f1 = function () {
console.log('听雨是雨');
};
f1(); // 听雨是雨
var f1 = function () {
console.log('echo');
};
f1(); // echo
以上例子说明代码在执行前发生了某些微妙的变化,JS引擎究竟做了啥呢?这就不得不提JS执行上下文的了。
执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。
执行上下文总共有三种类型:全局执行上下文,函数执行上下文,Eval函数执行上下文(一般不会使用)。
全局执行上下文
全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象,我们能通过this直接访问它。
全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局变量,都可以通过window对象来获取。(通过let 和 const声明的变量不可以)
var a = 1;
console.log(window.a); // 1
let b = 3;
console.log(window.b); // undefined
函数执行上下文
函数执行上下文存在无数个,每当一个函数被调用的时候都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。
执行上下文栈
执行栈,在其他编程语言中也被叫做调用栈,具有先进后出结构,用于存储在代码执行期间创建的所有执行上下文。当同一函数被多次调用,就是通过执行上下文栈来管理的。
JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈,之后每当有函数被调用,都会创建一个新的函数执行栈上下文并压入栈内;由于执行栈LIFO(last in first out后进先出)的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1(); // 3 2 1
通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,我们假想执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈。
//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
执行上下文创建阶段
执行上下文创建分为创建阶段和执行阶段,较为难理解的是创建阶段,我们先说创建阶段。
JS执行上下文的创建阶段主要负责三件事:确定this---创建词法环境组件(LexicalEnvironment)---创建变量环境组件(VariableEnvironment)
创建过程:
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 创建词法环境组件
LexicalEnvironment = {},
// 创建变量环境组件
VariableEnvironment = {}
};
确定this
官方称呼为This Binding,在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象,否则this一般指向全局对象window或者undefined(严格模式)
词法环境组件
词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用。
词法环境由环境记录和外部环境引入记录两个部分组成。其中环境记录用于存储当前环境中的变量和函数声明的实际位置;外部环境引入记录很好理解,它用于保存自身环境可以访问的其他外部环境。
词法环境也分为全局词法环境和函数词法环境两种。
- 全局词法环境组件
对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还记录了当前环境下的所有属性、方法位置。 - 函数词法环境组件
包含了用户在函数中定义的所有属性方法外,还包含了一个arguments对象。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境。
// 全局环境
GlobalExectionContext = {
// 全局词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Object', // 类型为对象环境记录
// 标识符绑定在这里
},
outer: < null >
}
};
// 函数环境
FunctionExectionContext = {
// 函数词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Declarative', // 类型为声明性环境记录
// 标识符绑定在这里
},
outer: < Global or outerfunction environment reference >
}
};
变量环境组件
变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录和外部环境引入。在ES6中唯一的区别在于词法环境用于存储函数声明和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 = {
// this绑定为全局对象
ThisBinding: <Global Object>,
// 词法环境
// 词法环境组件
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标志符绑定在这里 let const 创建的变量 a b在这
a: <uninitialized>,
b: <uninitialized>,
multiply: <func>
},
// 全局环境外部环境引入为null
outer: <null>
},
// 变量环境组件
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 var创建的c在这
c: undefined,
},
// 全局环境外部环境引入null
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {
// 由于函数是默认调用this绑定同样是全局对象
ThisBinding: <Global Object>,
// 词法环境
// 词法环境组件
LexicalEnvironment: {
Environment: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 arguments对象在这
Arguments: {0: 20, 1: 30, length:2}
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
}
}
小结:在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数, 而let const被设置为未初始化。这也就是为什么var定义的变量具有变量提升的特点,函数声明也具有函数提升的特点。上下文执行阶段,会根据之前的环境记录对应赋值,比如早期var在创建阶段为undefined,如果有值就对应赋值,像let const值为未初始化,如果有值就赋值,无值则赋予undefined。
函数提升优先级高于变量提升。
总结
有的人理解上下文都是谈作用域,变量对象和活动对象,变量对象和活动对象是ES3提出的概念,从ES5开始就用词法环境和变量环境替代了。变量对象和活动对象都是变量对象,变量对象是执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。这正好对应了全局词法记录和函数词法记录
利用堆栈信息快速定位问题
在前端,我们会经常通过window.onerror事件来捕获未处理的异常。假设捕获了一个异常,上报了一个异常如下:
TypeError: Cannot read property 'module' of undefined
at Object.exec (https://my.cdn.com/dest/app.efe91e855d7432e402545e7d6c25d2d9.js:16:29828)
at HTMLLIElement.<anonymous> (https://my.cdn.com/dest/app.efe91e855d7432e402545e7d6c25d2d9.js:25:6409)
at HTMLDivElement.dispatch (https://my.cdn.com/dest/vendor.eb28ded1876760b8e90973c9f4813a2c.js:1:248887)
at HTMLDivElement.y.handle (https://my.cdn.com/dest/vendor.eb28ded1876760b8e90973c9f4813a2c.js:1:245631)
然后查找为什么会出现undefined的情况。
需要注意的是:代码顺序不代表执行顺序,只有在调用时才有执行上下文栈。
常见面试题:
- 以下代码打印结果
console.log('global begin: ' + i);
var i = 1;
foo(1);
function foo(i) {
if (i == 4) {
return;
}
console.log('foo() begin: ' + i);
foo(i+1);
console.log('foo() end: ' + i);
}
console.log('global end: ' + i);
// 执行结果
global begin: undefined
foo() begin: 1
foo() begin: 2
foo() begin: 3
foo() end: 3
foo() end: 2
foo() end: 1
global end: 1