JS预编译(执行上下文)

958 阅读3分钟

引入: 对于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开发工具,如下图

参考

JavaScript深入之执行上下文栈