title: JavaScript 变量提升(Hoisting)详解 layout: post categories: JavaScript tags: 提升 hoisting
变量提升是 JavaScript 的一种执行机制,大致就是字面意思,将声明的变量提前,但并不是指在编译时改变语句的顺序,而是将变量提前放入内存中,供后续操作,下面通过实例进行分析;
在 JavaScript 中,声明一个函数并执行的话,通常会是以下形式:
function fn() {
console.log('run');
}
fn(); // run
上面是正常的思维顺序,但是包括其他一些编程语言在内,通常会使用如下形式:
fn();
function fn() {
console.log('run');
}
// run
这样做在执行上是没用问题的,同时可以在包含大量语句和函数申明的情况下,也可以使用这种特性将普通语句和函数申明分开,提高可读性;
以上情况便是一种常见的提升(Hoisting),即编译时提前将当前执行上下文包含的申明的函数,提前放入内存中,供全文语句执行时调用,为了方便理解而抽象成一种提升行为;
但是如果使用下面的方式申明函数并执行:
fn();
var fn = function() {
console.log('run');
}
// TypeError: fn is not a function
这里就没有像上面一样的结果了,这属于下面将介绍的变量提升行为;
当然函数只是一种类型的变量,还存在其他的变量类型,例如考虑以下语句:
var a = 1;
console.log(a);
// 1
逻辑和执行都是正常的,输出结果也是预期的,但是如果变一下顺序:
console.log(a);
var a = 1;
这种情况,通常可能会认为第一行调用了一个未定义的变量,然后输出 Uncaught ReferenceError: a is not defined
这样的错误,但是呢,并非如此,输出信息如下:
undefined
没错,就只有一个单独的 undefined
,这种输出情况就类似于以下代码的执行:
var a;
console.log(a);
// undefined
从这里便可以大致分析出,前面的顺序怪异的代码,相当于在编译时提前将后面出现的变量申明提前,然后执行就输出了一个已申明但未 初始化(赋值) 的值,这便是其他类型的变量的提升行为,即在当前执行上下文中,将后面申明的变量提前放入内存,供前面的语句调用;
注意,前面的代码最后一行的语句是 var a = 1
,即对变量进行了申明并赋值,但是最后输出仍然是 undefined
而不是 1
,证明变量提升行为只会对变量进行申明操作,并不会对其初始化赋值,不管原语句是否有赋值操作;
然后便能解释之前的代码:
fn();
var fn = function() {
console.log('run');
}
// TypeError: fn is not a function
这种情况便是将变量 fn
提升,值为 undefined
,所以执行 fn()
语句会提示 fn is not a function
而不是 fn is not defined
,与使用关键字 function
申明函数情况不一样;
另外值得一提的是,我们都知道 return
是函数内代码执行结束的标志,其后代码不会执行,但是提升行为却不受此限制,例如:
function fn() {
console.log(a);
fnn();
return ;
var a = 1;
function fnn() {
console.log('exist.')
}
}
fn();
// undefined
// exist.
上面提到两种提升行为,那么它们的优先级顺序是如何的呢?还是通过代码说明:
function fn1() {
console.log(a);
var a = 1;
function a(){};
}
function fn2() {
console.log(a);
function a(){};
var a = 1;
}
fn1(); // f a() {}
fn2(); // f a() {}
结果证明函数的提升优先级始终高于普通变量的提升;
再来看一种情况:
function fn() {
fnn();
var a = 1;
function fnn() {
console.log(a);
}
}
fn(); // undefined
这里按照正常的逻辑,申明函数 fnn()
之前就已经申明了变量 a
,所以会感觉函数 fnn 应该可以访问变量 a,但是最后输出的并不是 1
,输出 undefined
说明函数 fnn 并没有访问到赋值后的 a,并且所访问的 a 也触发了提升机制,因为输出的不是 RefferenceError
,那么就能大致梳理出提升真
正的执行顺序了:
因此上面的代码相当于是以下面的顺序执行的:
function fn() {
console.log(a);
var a = 1;
}
fn();
JavaScript 中申明变量的方式以及对应效果如下:
a = 0; // 全局变量
var b = 1; // 局部作用域变量(当前上下文)
let c = 2; // 块级作用域变量(当前块级上下文)
const d = 3; // 常量
这里解释一下,变量 a 申明时没有带任何关键字,默认其为全局变量;变量 b 申明带有关键字 var
,为当前上下文的局部作用域,如果用在全局则为全局变量;变量 c 使用关键字关键字 let
,d 使用关键字 const
,二者都是ES6中新增的块级作用域申明,只不过 const
申明的是常量,值不可更改;
通过例子看一下它们的区别;
var a = '全局';
function fn() {
var aa = '局部'
console.log(aa);
}
if (true) {
var b = '全局';
let bb = '块级';
const bbb = '块级';
}
for (i = 0; i < 1; i++) {
var c = '全局'
let cc = '块级';
const ccc = '块级';
}
console.log(a); // “全局”
fn(); // “局部”
console.log(aa); // aa is not defined
console.log(b, c); // “全局” “全局”
console.log(bb, cc); // bb is not defined cc is not defined
console.log(bbb, ccc); // bbb is not defined ccc is not defined
可以看出,var 的局部限于全局或者函数内部上下文,而 let 和 const 的块级的意思则是被 块(block) 所包含的上下文,也就是包含在花括号 {}
内部的作用域中,所以也包括函数在内,加上 if, for, while, switch 等情况,且不能被外部作用域访问;
首先看申明全局变量时的提升行为:
console.log(a);
a = 0;
// ReferenceError: a is not defined
证明不带关键字的申明全局变量,似乎并没有执行变量的提升行为,与以下代码的执行无异:
console.log(a); // a 前面未申明
// ReferenceError: a is not defined
使用关键字 var
申明的情况:
function fn() {
console.log(aa);
var aa = 1;
}
console.log(a);
var a = 1;
// undefined
fn();
// undefined
前面已解释,不再赘述,只是需要注意下面这种情况:
if (false) {
var a = 1;
}
console.log(a);
// undefined
正常思维可能会理解 if 条件判断为假所以不会执行内部语句,最后会输出 a is not defined
,然而并非如此,仍然将申明的变量执行了提升机制;这里可以简单理解为存在即提升,也就是为了避免以上问题的影响,所以出现了块级变量申明 let
与 const
;
使用 let
与 const
的情况:
if (true) {
console.log(a);
let a = 1;
console.log(aa);
}
// ReferenceError: Cannot access 'a' before initialization
// ReferenceError: aa is not defined
if (true) {
console.log(b);
const b = 1;
console.log(bb);
}
// ReferenceError: Cannot access 'b' before initialization
// ReferenceError: bb is not defined
可以看出,块级变量申明似乎也执行了类似提升的机制,但是处理却与 var
有区别,这里是直接以错误的形式处理输出,提示该变量未进行初始化,而没有变量的申明语句的情况,则是提示未定义的错误,且 let
与 const
的处理情况一致;