引入: 对于js来说,我们普遍认为是顺序执行但是如下却又不是"顺序执行"
function fn(){
console.log('1')
}
fn()
function fn(){
console.log('2')
}
fn()
如果是正常的顺序执行,那么本该输出 1 2,但是实际却是输出 2个2 ,为什么呢?这就涉及js的预编译(执行上下文)机制.即代码被执行前需要做的准备就称为执行上下文或者预编译
执行上下文的种类
JS执行上下文种类就3种
- 全局上下文
- 函数上下文
- eval函数(这里不讨论)
下面介绍2个名词
GO和AO
JS在执行之前就会产生一个GO(即全局作用域),当一个方法被调用时候就会形成一个AO(函数作用域)
预编译过程
-
创建GO/AO对象
-
确认this的指向
-
装载数据(即形参定义以及赋值)
-
函数声明提升
-
找var变量声明,将其作为GO/AO属性名,值为 undefined
-
再开始执行代码
eval作用域中的变量和函数不会被提升
if内语句同样会被变量提升(无论条件)
看几个例子
1
console.log(b) //undefined 变量提升
var b =2; // 如果执行环境是GO,那么 等价于 window.b =2
2
console.log(b) // 报错 ,b未被声明
3
注意: 如果函数名和变量名重名,则函数的声明会被提升到变量声明上方
console.log(foo)
function foo(){
}
var foo =1 //var foo 变量提升 ,但是foo函数提升到var变量之前
4
function fn(a) {
console.log(a); //f (){ var d=123}
var a = 123;
console.log(a); // 123
function a() {
var d =123
}
console.log(a); // 123
console.log(b); // undefined 由于b是函数表达式创建
var b = function () { }
console.log(b); // f () {}
}
fn(1);
一步一步执行来看
-
创建GO对象,无var变量无变量提升,
-
fn函数声明
-
创建AO对象,this的指向确定以及形参声明和赋值 此时
AO:{ a: 1 }
-
再寻找var 变量声明,变量提升并且赋值undefined 此时
AO:{ a: undefined b: undefined }
-
再寻找函数声明
AO:{ a: function (){} b: undefined(因为b本质是变量,以函数表达式赋值函数,所以不存在函数声明提升) }
-
开始执行代码
本质上原代码等价于下方
function fn(a) { var a =1 var a a = function(){var d =123} var b console.log(a); //f (){ var d=123} a = 123; console.log(a); // 123 console.log(a); // 123 console.log(b); // undefined var b = function () { } console.log(b); // f () {} } fn(1);
执行上下文 栈
我们所写的函数那么多,那么该如何去管理执行上下文呢
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
- JS开始解释执行代码时候最先开始的就是全局执行上下文,所以初始化时候必定向执行上下文栈中压入一个全局执行上下文,并且只有当整个程序结束时候才会被弹出,栈销毁
- 当执行函数时候,就会创建一个执行上下文,并将其压入栈中,当执行完该函数后,就将其从栈中弹出并销毁
如下
function fun2() {
}
function fun1() {
fun2();
}
fun1();
// 伪代码
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中调用了fun2,此时创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
另一例子
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()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
让我们模拟第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
让我们模拟第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
利用浏览器来观察栈调用信息
打开浏览器F12 Sources开发工具,如下图
参考