美文网首页
深究我所不知道JavaScript变量提升hoisting

深究我所不知道JavaScript变量提升hoisting

作者: RexingLeung | 来源:发表于2020-03-23 23:06 被阅读0次

    [TOC]

    前言

    在之前很想对JavaScript里面的变量提升hoisting做一次总结 , 直到最近的刷题 , 再一次刷到关于hoisting的问题 , 发现自己对于整个hoisting缺乏系统性的总结 , 这次终于有时间做了 ;
    当然如果只是基本的变量提升hoisting , 就是简单声明提升到最前 , 以及关于let和const的问题 ; 这些就是基本的hoisting , 基本上没有深究就有这些 ;
    那么问题来了 , 同样是考hoisting , 一旦深究下去 , 就有一大堆东西需要弄懂 , 下面就由我带着大家去探究一下什么是hoisting
    但是如果只是了解hoisting基本用法也可以应对平时的工作和刷题 , 此篇为深究偏

    什么是变量提升hoisting

    我们先看看下面代码

    // 例子1
    console.log(a) 
    // ReferenceError: a is not defined
    
    // 例子2
    console.log(a) // undefined
    var a
    

    我们知道js代码运行是一行一行同步执行的( 这里单纯的说同步代码 ) ;
    那么提出问题 : 例子2 , 为什么a是undefined , 前面没有声明a呀 ?
    回答 : js的变量提升呀 , 这么简单 !

    bingo ! 答对了

    例子2代码可以理解为

    // 例子2
    var a
    console.log(a) // undefined
    // var a 这里的a被js提升了
    

    再来一个例子

    // 例子3
    console.log(a) // undefined 
    var a = 'rexingleung'
    

    同样的 a 依然是undefined ;

    但是为什么是undefined ?

    上面的代码我们都可以想象成

    var a
    console.log(a) // undefined 
    a = 'rexingleung'
    

    注意 , 我们只是想象js引擎是这样做的 , 但是实际上 , js引擎并不会帮我们做这些事情!!!

    var a = 'rexingleung';
    

    我们可以想象 var a = 'rexingleung' ; 先 var a ; 然后 a = 'rexingleung' ; 其中var a; 被某种力量提升到console.log了

    再来看一个例子

    // 例子4
    function b(a){ 
        console.log(a) 
        var a = 'rexingleung'
    } 
    b('rexing')
    

    根据上面的理论可以将例子4 函数变形

    function b(a){ 
        var a 
        console.log(a) 
        a = 'rexingleung'
    } 
    b('rexing')
    

    那答案会是 : a是undefined ! yeah so easy
    但并不是 , 真正答案是rexing

    引出问题1

    例子4问题分析 : 上面变量提升以及变形是正确的 , 那么是什么呢 ?

    那就是函数传进来的参数a , 那么我们可以把例子4看成这样

    function b(a){ 
        var a = 'rexing'
        var a 
        console.log(a) 
        a = 'rexingleung'
    } 
    b('rexing')
    
    引出问题2

    这时候又有一个问题 , 就是在console之前 , 重新声明了 a , 这样a不会被覆盖成undefined吗 ?

    那么我们再来看一个例子6

    // 例子6
    var a = 'rexingleung'
    var a
    console.log(a)
    

    这里答案 a 是 rexingleung 而不是 undefined , 这时候我们需要结合到变量提升hoisting , 可以将例子6变形

    // 例子6-1
    var a
    var a
    a = 'rexingleung'
    console.log(a)
    

    根据例子6-1 , 我们就相当清晰明了了

    以上还只是入门
    我们再来一个例子7

    // 例子7
    console.log(a) // ƒ a(){}
    var a 
    function a(){}
    

    上面例子需要知道的时候 , 由于提升的优先权 , function的提升优先权是高于变量的 , 所以 , 输出是ƒ a(){}而不是undefined

    然后我们再来一个例子8

    
    // 例子8
    console.log(a)
    var a = function a(){}
    var a = 'rexing'
    
    // 例子8-1
    console.log(b)
    var b = function (){}
    var b = 'rexing'
    
    // 例子8-2
    console.log(c)
    var c=new Function();
    var c = 'rexing'
    

    那么这里的a和b和c又输出什么呢 ?
    先别慌 , 这里都是输出 undefined ; 这又是为什么 ?
    因为例子8-1和例子8-2声明函数的时候使用的 函数表达式声明方式 , 所以 , 跟普通声明是一样的
    到这里入门的变量提升基本上就是这些情况了 , 我们总结一下吧
    1 . 变量提升只能是变量提升 , 赋值不会提升
    2 . 函数类的变量提升 , 需要注意传入的参数
    3 . 函数的提升优先级高于普通变量提升

    JavaScript变量提升hoisting深究部分

    let和const的hoisting

    我们重新看回去例子1 , 且修改例子1如下

    // 例子9
    console.log(a) // Uncaught ReferenceError: a is not defined 
    let a
    

    这时候 , 我们终于可以使用同步的思维去看这份代码了 , 从例子9可以看出 , let没有帮我们做一些很奇怪的事情( 就是变量提升hoisting )

    引出问题 : let没有变量提升真的这么简单吗

    我们看下面例子10

    // 例子10 
    var a = 10 
    function b(){ 
        console.log(a) 
        let a 
    } 
    b()
    

    我们分析一下以上代码 , 按照例子9 , let不会变量提升 , 且外面有使用var a定义了a , 这里会不会输出10呢 ?
    答案 : Uncaught ReferenceError: Cannot access 'a' before initialization
    emmm ( 手动黑人三问号 ) ??? 上面又是什么 ?

    Uncaught ReferenceError: Cannot access 'a' before initialization 错误是a未定义 , 就被使用了

    先别晕 , 我们再来看一个例子11

    // 例子11 , 还是跟例子10 差不多
    var a = 10 
    function b(){ 
        console.log(a) 
        const a 
    } 
    b()
    
    // 例子11-1
    var a = 10 
    function b(){ 
        const a 
        console.log(a) 
    } 
    b()
    

    其实熟悉const变量的一眼就看出来了 , const一定要赋值 , 但是他们都同样的错
    Uncaught SyntaxError: Missing initializer in const declaratio , 意思是 , const定义的变量一定要赋值

    那么问题又来了 , 例子11 const不是在console下面吗 , 如果console能够识别到a是const未赋值 , 那么就说明const变量有被提升了 ; 但是真的是这样吗 ?

    我们来再看例子12

    // 例子12
    var a = 10
    function b(){
        console.log(a) 
        const a = 11
    }
    b()
    

    那么例子12会是 undefined 吗 ? 然而并不会 , 这里是跟 例子10 报错是一样的 Uncaught ReferenceError: Cannot access 'a' before initialization

    好了 , 很多文章说到这里 , 基本上就结束了 , 讲到了let const 以及普通的hoisting , 但是并非只有这些
    到这里就可能会有疑问 , 那么我只需要把这些规则背熟就好了 , 还有什么难度的

    但是我们需要知道的是

    • JavaScript为什么需要hoisting
    • hoisting 具体做实现的

    JavaScript为什么需要变量提升hoisting

    当提出这个问题的时候 , 我们就要知道 , 如果没有变量提升hoisting 会怎么样 , 答案是

    1. 我们需要先定义再使用
    2. 函数亦如此 , 先定义再使用
      对于第一点 , 相信大家都没有什么问题
      但是第二点 , 就不行了, 这样我们写了函数需要在函数下面才能使用 , (emm , 怪怪的)
      例如
    // 例子13
    function a(){}
    a()
    

    emmm ? 其实例子13还能接受哦

    那么下面呢

    // 例子14
    function a(){}
    function b(){
        return a()
    }
    function c(){
        b()
    }
    c()
    

    例子14就很别扭 , 因为如果我们a , b , c函数如果打乱了 , 那么就执行不了了

    以上例子14为了避免函数相互调用 , 所以变量提升是相当重要以及必要的

    变量提升hoisting , 究竟怎么做

    攻坚时刻到了 , 变量提升hoisting , 究竟怎么这个问题 , 就是最需要讨论的问题
    这里引出两个概念

    1. 函数执行上下文(Function Execution Context )
    2. 全局执行上下文( Global Execution Context ) ( 这两个概念后面会详细讲 )
      这里简单说一下
      Global Execution Context
      又称为默认执行环境。执行环境在建立时,会经历两个阶段 , 分别是:
      Creation Phase 创造阶段
      Execution Phase 执行阶段
    • 一旦全局执行结束创造阶段、进入执行阶段,它就会开始由上到下、一行一行地执行代码,并自动跳过函数里的代码,这也是合理的,毕竟你只是进行函数声明,并没有打算立即执行它。如果你的代码里完全没有任何的Function Call,那么全局执行环境是你唯一会遇到的执行环境。
    • 就是说执行上下文在逻辑上形成一个堆栈 , 此逻辑堆栈上的顶部执行上下文是正在运行的执行上下文。
      每个执行上下文都与一个变量对象相关联。代码中声明的变量和函数将作为变量对象的属性添加。对于函数代码,参数作为变量对象的属性添加。

    每个EC( Execution Context ) 都会有相对应的variable object(以下简称VO),在里面宣告的变数跟函式都会被加进VO 里面,如果是function,那参数也会被加到VO 里。
    那么 var a = 10;

    • var a:在VO 里面新增一个属性叫做a(如果没有a 这个属性的话)并初始化成undefined
    • a = 10:先在VO 里面找到叫做a的属性,找到之后设定为10( 这也在《你不知道的JavaSctirpt》找到 )

    如果vo里面找不到就会沿着作用域链( scope chain ) 不断往上寻找,如果每一层都找不到就会抛出错误 ( 其实这里又引出一个概念就是作用域链( scope chain ) 寻找过程之后会说 )

    在执行上下文的时候 , 哪个对象用作变量对象,哪些属性用于属性 , 取决于代码的类型,但其余行为是泛型的。在输入执行上下文时,按一定顺序将属性绑定到变量对象

    简单来说就是对于参数,它会直接被放到VO 里面去,如果有些参数没有值的话,那它的值会被初始化成undefined。

    关于VO

    举例来说,假设我function 长这样:

    function test(a, b, c) {} test(10)
    

    对应的VO
    就是

    
    { 
        a: 10, 
        b: undefined, 
        c: undefined 
    }
    

    对于function声明

    对于function声明,一样在VO 里面新增一个属性,至于值的话就是创建 function 完之后回传的东西(可以想成就是一个指向function 的指针)

    再来是重点:「如果VO 里面已经有同名的属性,就把它覆盖掉」,举个小例子:

    
    function test(a){ 
        function a(){} 
    } 
    test(1)
    

    这里的vo就会是

    
    { 
    a: function a 
    }
    

    变量的声明处理

    对于变量的声明处理 , 当我们在进入一个执行上下文的时候(你可以把它想成就是在执行function 后,但还没开始跑function 内部的代码以前),会按照顺序做以下三件事:

    1. 把参数放到VO 里面并设定好值,传什么进来就是什么,没有值的设成undefined
    2. 把function 宣告放到VO 里,如果已经有同名的就覆盖掉
    3. 把变数宣告放到VO 里,如果已经有同名的则忽略

    在你看完上面后并且稍微理解以后,你就可以用这个理论来解释我们前面看过的代码了:

    function test(v){
      console.log(v)
      var v = 3
    }
    test(10)
    

    每个function 你都可以想成其实执行有两个阶段,第一个阶段是进入EC,第二个阶段才是真的一行行执行程式。

    在进入EC 的时候开始建立VO,因为有传参数进去,所以先把v 放到VO 并且值设定为10,再来对于里面的变数宣告,VO 里面已经有v 这个属性了,所以忽略不管,因此VO 就长这样子:

    {
      v: 10
    }
    

    进入EC 接着建立完VO 以后,才开始一行行执行,这也是为什么你在第二行时会印出10 的缘故,因为在那个时间点VO 里面的v 的确就是10 没错。

    如果你把程式码换成这样:

    function test(v){
      console.log(v)
      var v = 3
      console.log(v)
    }
    test(10)
    

    那第二个印出的log 就会是3,因为执行完第三行以后, VO 里面的值被换成3 了。

    以上就是ES3 的规格书里面提到的执行流程,你只要记得这个执行流程,碰到任何关于hoisting 的题目都不用怕,你按照规格书的方法去跑绝对没错。

    总结 : 对于什么是hoisting , 以及hoisting的实现过程 , 如果你只是简单了解hoisting , 这篇文章可以不看也可以想象到hoisting如何实现 , 但是我们需要知道为什么hoisting , 知其然知其所以然

    相关文章

      网友评论

          本文标题:深究我所不知道JavaScript变量提升hoisting

          本文链接:https://www.haomeiwen.com/subject/cyxnyhtx.html