最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JavaScript 中的变量提升

    正文概述 掘金(bugvvd)   2021-02-19   603
    foo(); 
    function foo(){
        console.log(a) // undefined
    };
    var a = 1;  
    

    从这个例子来看,至少有两个奇异的地方:

    1. 我们在定义函数 foo 之前就执行了它,却没有报错。
    2. 我们在定义变量 a 之前就访问了 a,却返回了 undefined 也没有报错,说明在执行 foo 的时候 a 就已经存在了,只是没有被赋值。

    这就涉及到 JavaScript 的提升(Hoisting)。

    提升只是个描述

    虽然上述代码可以被看作:

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

    但这是对 JavaScript 引擎处理的一种具象描述,仿佛变量和函数声明都被提升到了当前代码块的顶部。但是事实上代码并没有发生变化,只是引擎在幕后做了一些别的准备工作。

    在代码执行前的编译阶段,JavaScript 引擎会扫描我们的代码并收集所有的函数声明及变量声明,然后将他们添加到一个被称为词法作用域(Lexical Environment)下的环境记录器的数据结构中。

    词法作用域由两部分构成:

    1. 环境记录器(Environment Record)
    2. 一个对外部词法环境的引用(如不存在则引用为空,possibly null reference to an outer Lexical Environment)

    环境记录器所做的是所谓:标识符 - 变量的映射(identifier - variable mapping),它的数据结构可以理解为某种形式的一组键值对。但需要注意的是,此处“标识符”(identifier)就是变量名或函数名,“变量”指的是基本数据(primitive value)或者对引用类型(reference,函数、类等)的引用。概念上可以这样描述:

    // lexicalEnvDemo
    lexicalEnvironment = {
        outerLexicalEnvironment: null,
        environmentRecord: {
            a:  <value>,
            funcA:  <reference to funcA>,
            objB:  <reference to objB>,
        }
    }
    

    词法作用域在执行前创建且生命贯穿整个执行阶段,这也就解释了为什么看上在 foo 函数声明前就能使用它,和在变量 a 被定义前就能访问它。这就是整个”提升“的过程。在整个执行过程中,代码所需要的变量和函数都在词法作用域当中搜索,自然而然会有两个结果:

    1. 找到了 identifier,返回值或引用,如果没有赋值就返回 undefined
    2. 找不到 identifier, 报错并打印 Uncaught ReferenceError: <identified> is not defined

    提升的过程

    当 JavaScript 引擎在编译阶段找到一个声明,它就会将这个声明添加到环境记录器。这个过程分为 2 步,只涉及创建和初始化:

    1. 创建:将 identifier 添加到环境记录器中,开辟一个 identifier: <uninitialized> 位置
    2. 初始化:
      • 函数:identifier: <reference to aFunction>
      • 变量:identifier: undefined (毕竟引擎在此时无从知晓我们的代码到底会给 a 一个什么类型)

    从一开始的例子中我们也可以隐约发现,引擎对变量 a 和函数 foo 的处理不太一样。变量 a 在被提升后,值却没有被提升,而是给了个 undefined。函数 foo 却全面被提升了,以至于 foo() 可以正确执行。

    最后,当引擎在执行阶段遭遇对 identifier 的赋值语句的时候,它会最终将值或引用添加到环境记录器,identifier: <reference to someObject>a: <someValue>

    网上的许多教程认为提升变量声明的时候还有第 3 个赋值的步骤,把 identifier: undefined 变成 identifier: <value> 。对此,我不太认同,原因正是因为开头的代码:

    foo(); 
    function foo(){
        console.log(a) // undefined
    };
    var a = 1;  
    

    foo() 的执行说明已经到了执行阶段,a 依然是 undefined。所以我认为赋值在提升过程当中是不存在的。我可能大错特错,望指正。

    提升函数声明(function

    function 语句的函数声明的提升可以看作有最高的优先级,且不会被同名的变量声明干扰。这就解释了 foo 函数在顶部可以正常执行的原因。

    提升变量声明(var

    var a = 1;
    

    会被引擎分拆处理

    var a;  // 归编译期处理
    a = 1;  // 归执行期处理
    

    函数表达式与类表达式

    var a = function (){};
    var b = new Object();
    

    引擎对函数表达式和类表达式的处理可以看作是变量。

    /* 归编译期处理 */
    var a;
    var b;
    /* 归执行期处理 */
    a = function (){};
    b = new Object();
    

    提升与赋值是发生在两个不同时期的事情,不相互影响。

    重名处理

    1. 如果出现同名的函数,后到的会覆盖之前的。
    foo(); // 2
    function foo(){
        console.log(1)
    }; 
    function foo(){
        console.log(2)
    };
    
    1. 如果出现同名的变量,它们不会互相影响,以为它们的初始化结果都是 undefined
    2. 如果出现同名的函数和变量,由于函数有提升的最高优先级, identifier 所指向的一定是函数,undefined 不会覆盖对函数的引用。

    存在变量提升优先级吗?

    网上有着五花八门对提升优先级的说法。比方说,Mabishi Wakio 这篇教程让我收益良多,但我认为其在优先级(Order of precedence)这部分的讲述不够严谨。文章认为提升声明是有优先级的,变量赋值 > 函数声明,而函数声明 > 变量声明。理由如下两段代码所示:

    var double = 22;
    function double(num) {
      return (num * 2);
    }
    console.log(typeof double); // Output: number
    

    变量赋值 > 函数声明,所以 doublenumber 类型。

    var double;
    function double(num) {
      return (num * 2);
    }
    console.log(typeof double); // Output: function
    

    而函数声明 > 变量声明,所以 doublefunction 类型。 但这样的优先级无法解释下面的输出

    double(4);
    var double = 22;
    function double(num) {
      console.log(num * 2, typeof double); // 8, "function"
    }
    

    显然函数声明优先于变量赋值了。所以我认为没有这么复杂,应该只有一种优先级,即 funciton 语句的声明优先于 var 语句的声明被环境记录器所记录。同名的 double 变量声明不会用 undefined 覆盖同名的 doublefunction double(){} 的引用。对 double 的赋值的确会覆盖函数引用,那已经是执行阶段的事情了,与提升阶段无关了。

    // 提升了先占位
    function double(num) {
      return (num * 2);
    }
    // 提升而未覆盖
    var double;
    double = 22;
    console.log(typeof double); // Output: number
    

    这样的解释我觉得更简洁一些。Again, 我也可能大错特错,望指正。

    let 存在变量提升

    var a = 1;
    function foo(){
        // beginning of TDZ
        console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
        /* ... */
        // end of TDZ
        let a;
    }
    foo();
    

    这里例子说明被 let 声明的变量在其作用域中一定是被提升创建的但不提升初始化和赋值。要不然 foo 里面的 a 应该是全局的 1。只不过从代码块开始到 alet 声明之前该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

    仅限于提升方面的区别,let 可以近似看作严格模式下的 var,不声明就不能使用。 const 则更严格,const 声明的变量必须直接初始化。

    const a; // Uncaught SyntaxError: Missing initializer in const declaration
    

    总结

    在 ES6 推出后,letconst 的应用基本上可以终结提升与否的迷思,它们不再像 var 这般暧昧。相当于说别折腾了,没声明就是用不了。

    参考资料

    1. Hoisting
    2. JavaScript Hoisting
    3. Demystifying JavaScript Variable Scope and Hoisting
    4. Annotated ECMAScript 5.1
    5. javascript的执行环境、词法环境、变量环境
    6. Lexical environment and function scope
    7. Hoisting in Modern JavaScript — let, const, and var
    8. es6中let存在变量声明提升

    起源地下载网 » JavaScript 中的变量提升

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元