[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 会怎么样 , 答案是
- 我们需要先定义再使用
- 函数亦如此 , 先定义再使用
对于第一点 , 相信大家都没有什么问题
但是第二点 , 就不行了, 这样我们写了函数需要在函数下面才能使用 , (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 , 究竟怎么这个问题 , 就是最需要讨论的问题
这里引出两个概念
- 函数执行上下文(Function Execution Context )
- 全局执行上下文( 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 内部的代码以前),会按照顺序做以下三件事:
- 把参数放到VO 里面并设定好值,传什么进来就是什么,没有值的设成undefined
- 把function 宣告放到VO 里,如果已经有同名的就覆盖掉
- 把变数宣告放到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 , 知其然知其所以然
网友评论