Js 执行机制之变量提升

297 阅读5分钟

什么是变量提升?

JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”,变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined;

变量提升提升的是什么?

提升的是变量和函数的声明,注意变量提升只是提升变量和函数的声明,并不会直接执行赋值初始化的过程,还有非声明的语句,Javascript 是直接转换成字节码,待后续执行阶段执行;

Javascript 的执行流程是怎样的?

实际上变量和函数声明在代码里的位置是不会改变的,他们会在编译阶段被 JavaScript 引擎放入内存中,将那些用 var 声明的变量设置为活动对象的属性,默认值为“undefined”,并且将那些以 function 定义的函数也添加为活动对象的属性,而且他们的值正是函数的定义,匿名函数将不被解析。变量初始化过程即赋值过程发生在解释执行期,而不是编译期

  • 语法分析

        Js 引擎检查代码是否有低级的语法错误

  • 预编译(编译结束会产生以下两部分内容)

        在内存中开辟一些空间,存放一些数据
    • 执行上下文(执行一段代码的运行环境)
      • 变量环境对象(Viriable Environment Object)
        • 变量提升的内容,如:

foo2();
var foo = 1;
function foo2(){
    console.log(foo);
}

/* 上面代码经过编译后如下 */

VariableEnvironment:
     foo -> undefined, 
     foo2 -> function : {console.log(foo)}

      • 词法环境(实现块级作用域时需要在词法环境中操作变量)

    • 可执行代码

  • 解释执行(解释一行执行一行)

    • 同名覆盖

function foo(){ 
    console.log('我是a');
}
foo();
function foo(){ 
    console.log('我是2b');
}
foo();
打印结果:


    • 更多例子

/* 1、变量提升之同名值覆盖 */
function foo(){ 
    console.log('我是a');
}
foo();
function foo(){ 
    console.log('我是2b');
}
foo();

/* 2、变量提升之函数声明方式 */
foo2();
/* 用函数表达式进行的变量声明 */
var foo2 = function() {
    console.log(2)
}
/* 函数声明 */
function foo2() {
    console.log(1)
}
/* 
    分析过程
    编译阶段:
        1、第一个 foo2 变量是用函数表达式进行的变量声明,会提升变量foo2并赋值为undefined
        2、第二个 foo2 是函数声明,会整体提升,并将函数的引用类型保存在堆空间中,将堆空间的地址赋值给提升的foo2函数
    执行阶段:
        预计打印结果为1
*/

/* 3、变量和函数提升优先级 */
console.log(foo3);
foo3();
var foo3 = 3;
function foo3(){
    console.log(10);
}
console.log(foo3);
foo3 = 6;
foo3();
/* 
    分析过程
    编译阶段:
        1、第 1、2 行代码不涉及声明,编辑阶段直接生成字节码
        2、第 3 行对 foo3 变量声明,需要提升,并赋值为undefined => var foo3 = undefined;
        3、第 3 行对 foo3 进行函数声明,需要提升,并且与前面的 foo3 同名,故 foo3 值被覆盖为该函数在堆内存中的地址
    执行阶段:
        1、第 1 行打印结果预计为 foo3 的函数体
        2、第 2 行执行 foo3 的函数,打印为 10
        3、第 2 行 foo3 再次被覆盖为 3,第 7 行的结果为 3
        4、第 8 行 foo3 再次被覆盖为 6
        5、第 9 行 由于此时 foo3 为值 6 执行 foo3() 会报错
*/

变量提升会导致的一些问题

  • 外层变量在不知不觉中被内部代码覆盖
  • 该销毁的变量未被销毁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>作用域</title>
</head>
<body>
    <h1 id="title">
        <span>作用域</span>
    </h1>
    <script>
        /* 变量提升导致的问题之一:外层变量在不知不觉中被内部代码覆盖 */
        var str = "我是全局变量";
        function foo(){
            console.log(str);
            if(true){
                var str = "我是函数作用域中的变量"
            }
            console.log(str);
        }
        foo();
        /* 
            执行过程分析: 
                1、函数调用前,在预编译阶段创建全局执行上下文,并压入调用栈底

                2、执行 foo 函数调用,预编译 foo 函数内部代码,创建函数执行山下文并入压入调用栈中

                3、所以根据作用域链的查找规则,在 foo 中(22 行)的打印会从当前执行上下文先查找 str 变量,
                发现当前执行上下文中有此变量并且,由于此时 str 经过了变量提升提升为 str -> undefined,
                所以第一个答应结果为 undefined
                
                4、由于 Js 在 ES6 之前没有块级作用域,所以 str 会经过 if 块中被赋值,所以第二个打印结果为“我是函数作用域中的变量”
        */

        /* 变量提升导致的问题之二:该销毁的变量未被销毁 */
        function foo(){ 
            for (var i = 0; i < 7; i++) {

            } 
            console.log(i); 
        }
        foo();
        /* 
            执行过程分析: 
                1、函数调用前,在预编译阶段创建全局执行上下文,并压入调用栈底

                2、执行 foo 函数调用,预编译 foo 函数内部代码,创建函数执行山下文并入压入调用栈中

                3、foo for 循环中的 变量 i 被提升至 foo 的函数执行上下文顶部,即使在 for 循环结束后 变量 i 仍然存在于 foo 的作用域中没被销毁               
        */
    </script>
</body>
</html>