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

前言

在之前很想对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 , 知其然知其所以然

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值