• Javascript 的变量提升与预解析


    一、什么是变量提升

      在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。变量提升即将变量声明提升到它所在作用域的最开始的部分

    二、怎么实现变量提升

      Js 运行前有一个预编译的过程,预编译完成后,在一步步执行。那么在预编译的过程中,会执行三个动作:

        1.分析参数,

        2.分析变量声明,

        3.分析函数声明。

      具体步骤如下:

        1.函数在运行的瞬间,生成一个活动对象(Active Object),简称 AO

        2.分析参数

          函数先接收形参,添加到 AO 的属性,并为这个值赋上 undefined,如:AO.age = undifened.

          接收实参,添加到 AO 的属性,覆盖之前的undefined 值。

        3.分析变量声明,如 var age; || var age = 23;

          如果在参数分析时,AO 并没有age 属性,则先为 AO 添加 age 属性为 undefine,即 AO.age = undefine.

          如果此时 AO 中已有age 属性,则不做任何操作。

        4.分析函数的声明

          函数的声明一般有3种形式:

            函数关键词声明:function age() {};

            函数字面量声明:var age = function () {};

            构造函数声明:var age = new Function();

          如果函数是函数关键词声明,即 function age () {}, 则把整个函数提至顶端,覆盖上一步中 AO.age 属性,即此刻 AO.age = function () {};

          如果函数是函数字面量声明,即 var age = function () {}, 则将 age 当成变量声明处理,即 var age。

          第三种形式暂不讨论。

      代码示例1

    <script>
        function t1(age) {
            console.log(age);
            var age = 27;
            console.log(age);
            function age() {}
            console.log(age);
        }
        t1(3);
    </script>
    

      预编译阶段的故事:

        1.创建 AO 对象

        2.分析参数

          接收形参 age,并赋值 undefine。AO.age = undefine

          接收实参 3,覆盖 上一步的 undefine。AO.age = 3

        3.分析变量声明 (var age = 27)

          由于第2步已有 age 属性,固这一步啥也不做。AO.age = 3.

        4.分析函数声明 (function age() {})

          由于是函数关键字声明,函数被提升到最顶端,替代第2步中的 age 属性,即AO.age = function () {}.

      于是,原本的代码预编译后等价于:

    <script>
        function t1(age) {
            var age=     function () {}
            console.log(age);   // function () {}
            age = 27;
            console.log(age);   // 27
        
            console.log(age);   // 27
        }
        t1(3);
    </script>

     

      代码示例2

    <script>
        function t1(age) {
            var age;
            console.log(age);
            age = 23;
            console.log(age);
            function age() {
                console.log(age);
            }
            age();
            console.log(age)
        }
        t1(22)
    </script>

      预编译阶段的故事:

        1.创建 AO 对象

        2.分析参数

          接收形参 age,并赋值 undefine。AO.age = undefine

          接收实参 22,覆盖 上一步的 undefine。AO.age = 22

        3.分析变量声明 (var age;)

          由于第2步已有 age 属性,固这一步啥也不做。AO.age = 22.

        4.分析函数声明 (function age() {console.log(age)})

          由于是函数关键字声明,函数被提升到最顶端,替代第2步中的 age 属性,即AO.age = function () {console.log(age)}.

      于是,原本的代码预编译后等价于:

    <script>
        function t1(age) {
            var age=  function() {
                console.log(age);
            };
            console.log(age); // function () {console.log(age)}
            age = 23;   
            console.log(age);   // 23
           
            age();  // 报错:age is not a function
            console.log(age)
        }
        t1(22)
    </script>

      执行阶段:执行到第10步时,由于age 在第7步被变量覆盖,所以再以函数的方式调时,发生了报错。

      示例代码3

    <script>
        function t1(age) {
           console.log(1,age);
           var age = function() {
               console.log(2,age);
           };
           console.log(3,age);
           age();
          console.log(4,age);
        }
        t1(23)
    </script>

      预编译阶段的故事:

        1.创建 AO 对象

        2.分析参数

          接收形参 age,并赋值 undefine。AO.age = undefine

          接收实参 23,覆盖 上一步的 undefine。AO.age = 23

        3.分析变量声明 

          在这个例子中,没有变量的声明(var age = function () {console.log(age)},属于函数的字面量声明),固啥也不做,AO.age = 23。

        4.分析函数声明 (var age = function () {console.log(age)})

          由于是函数字面量声明,可以当做变量声明分析,即 var age = function age() {console.log(age)} 等价于 var age; age = function () {console.log(age)}。由于第2步中已有age属性,所以这一步什么都不做。AO.age = 23。

      于是,原本的代码预编译后等价于:

    <script>
        function t1(age) {
           age = 23;
           console.log(1,age);       // 1, 23
           age = function() {
               console.log(2,age);   // 2, function() {console.log(2, age)}
           };
           console.log(3,age);    // 3, function() {console.log(2, age)}
           age();
          console.log(4,age);     // 4, function() {console.log(2, age)}
        }
        t1(23)
    </script>

      执行阶段:第一个log打印时,输出的是预编译的结果,即 1,23, 然后在第5行,age 被覆盖成函数,但没有被调用,所以先走第8行的 log,即答应结果为 3,function (){...},然后在第9行,age 函数被调用,运行第6行函数内的log,所以打印出来的结果为 2, function (){...},最后执行第10行的 log,由于age 没有被修改,所以打印的仍为函数,结果为 4,function (){...}。因此,4个log 的打印顺序为:1,3,2,4

       至此,JS 预编译的过程我们差不多走通了。下面我们来总结变量提升的规律。

    三、变量提升的规律  

      1:所有的声明都会提升到作用域的最顶上去。

      2:同一个变量只会声明一次,其他的会被忽略掉。

      3:函数声明的优先级高于变量声明的优先级,并且函数声明和函数定义的部分一起被提升。

    四、最佳实践

      无论变量还是函数,都必须先声明后使用。以此来规范我们的代码,增强可读性和可维护性。

    PS:javascript之词法作用域及函数的运行过程

    PS2: 另有一道 JS 基础的综合训练题,感兴趣的朋友可以研读研读,做做练习。一道常被人轻视的前端JS面试题

  • 相关阅读:
    Mybatis学习随笔3
    Mybatis学习随笔2
    Mybatis学习随笔
    Java校招面试-什么是线程安全/不安全
    装饰器2
    装饰器
    默认传参的陷阱
    处理日志文件
    第二天
    用户登录
  • 原文地址:https://www.cnblogs.com/dyqblog/p/9926450.html
Copyright © 2020-2023  润新知