壹 ❀ 引
ES6
中因为const let
的变量声明方式,以及暂时性死域的作用,声明提前这个问题已经得到很大的改善了。但考虑到不同公司面试水平参差不齐,也许还是有公司会考ES5
中的声明提前,因此本文重点在于解释这个概念,以及变量与函数提升的顺序是怎么样的。另外,声明提前的本质是执行上下文在作怪,你可以先了解声明提前,之后再阅读博主执行上下文文章,那么本文开始。
贰 ❀ 从一道面试题说起
console.log(a)//?
a();//?
var a =3;
function a(){
console.log(10);
}
console.log(a);//?
a = 6;
a();//?
四处分别输出什么?为什么?读完本文,最少也能在你心中激起一丝波澜了。
叁 ❀ 什么是变量声明提前
先来了解一个函数作用域的概念:**变量在声明它们的函数体以及这个函数体嵌套的任意函数体内始终可见。**说直白点,在声明一个变量的前后,你都可以直接使用它,并不会报错。举个例子:
(function(){
console.log(a);//undefined
var a ="听风";
console.log(a);//听风
}())
前面已经说了,变量在声明它们的函数体内始终可见,尽管第一个console
输出在声明a
之前,但它依旧能输出,并不会报错,那是因为声明统一提前,赋值原地不变。上面代码等同于:
(function(){
var a;
console.log(a);//声明了但未赋值,所以输出undefined;
a ="听风";
console.log(a);//上一步赋值了,所以输出 听风
}())
声明提前了,只是没有赋值,赋值仍保留远处不变,所以说变量a
在function
每一处都是可用的,就是这么个怪逻辑。
肆 ❀ 什么是函数声明提前(函数体提前)
函数声明提前的原理与变量声明提前情况类似,需要提醒的是,只有函数声明格式的函数才会存在函数声明提前,比如函数表达式,构造函数,都不存在函数声明提前。
函数创建的三种写法:
a.函数声明:function fn(a){ console.log(a) };
(只有这个家伙存在函数声明提前)
b.函数表达式:var fn = function(a){ console.log(a) };
c.构造函数:var fn = new Function( "a", console.log(a) );
直接上个例子:
num()//1
console.log(num)//函数本身
function num(){
console.log(1);
}
num();//1
console.log(num)//函数本身
有疑问的应该就是函数之前的函数调用与console
了,前面说过了,函数声明的情况与变量声明类似,你可以理解为,在同一作用域内函数声明后,此函数会跑到本作用域的最前面。上面的代码等同于:
function num(){
console.log(1);
}
num()//1
console.log(num)//函数本身
num();//1
console.log(num)//函数本身
那么再来看看函数表达式是否会函数体提前:
num()//报错
console.log(num)//undefined
var num = function (){
console.log(1);
}
num();//1
console.log(num)//函数本身
第一个num()
就会报错,后面三个是看不到输出的,这里是假设不受num()
报错影响本应输出的情况。为什么会这样呢,还记得前面变量声明提前的原理吗,这里只是将后面的普通赋值换成了函数,所以以上代码等同于:
var num;
num()//报错,这时候都没有函数声明
console.log(num)//undefined,因为已经声明了num
num = function (){
console.log(1);
}
num();//1,有函数了啊,可以调用了
console.log(num)//函数本身,有函数了。
声明提前,赋值不变,前面只声明了num,并不存在函数,又怎么能调用num函数呢,所以第一个就报错了,这里总该明白了吧。
伍 ❀ 变量声明提前,函数声明提前顺序
这里就有个问题了,函数声明提前,变量声明也提前,到底谁会更提的更前?假设两者都用的同一命名声明,到底最后会输出啥,我们来看个例子:
console.log(a);
var a = "听风";
function a(){ console.log("echo"); }
在解释前先引入一个概念,你不知道的JavaScript(上卷)一书的第40页中写到:**函数会首先被提升,然后才是变量。**也就是说,同一作用域下提前,函数会在更前面。以上代码等同于:
function a(){
console.log("echo");
}
var a;//由于上面函数已声明a,相同的变量名声明会被直接忽略
console.log(a);//输出函数本体
a = "听风";
为啥函数提前之后又var a
;了怎么不输出undefined
,因为这里只是再次声明a
,并未修改现有a
的值,做个简单测试就可以了:
var a=1;
var a;
console.log(a);//1
变量a
已经声明过了,而且也赋值了,后面再次声明只是声明并未修改值,这种声明方式会被直接忽略,所以还是输出1.
这里讨论了变量声明提前,函数声明提前以及提前先后顺序,那么我们再回头,改写文章开头的笔试题,那么它等同于:
正确的修改:
function a(){
console.log(10);
}
var a;//再次声明a,并未修改a的值,忽略此处声明
console.log(a)//输出函数本体
a();//函数声明提前,可调用,输出10
a =3;//这里修改值了,a=3,函数已不存在
console.log(a);//输出3
a = 6;//再次修改为6,函数已不存在
a();//a已经为6,没有函数所以没法调用,直接报错
错误的修改:
var a;//再次声明a,并未修改a的值
function a(){
console.log(10);
}
console.log(a)//输出函数本体
a();//函数声明提前,调用输出10
a =3;//这里修改值了,a=3,不在是函数了
console.log(a);//输出3
a = 6;//再次修改为6
a();//a已经为6,不存在函数了,所以没法调用,报错
可能很多人的思路是,var a = 3
在前,函数声明在后,var a
先提前,然后函数再次提前覆盖了var a
;你们也能发现上面两种改写结果都是一样的,因为我在上面的橙色解释中说了,但其实它们的提前是有固定的先后顺序的,这里希望大家能清楚。
本文只是解释了什么是变量提升,准确来说变量提升是执行上下文搞得鬼,代码在执行前都会做一番准备工作,也就是创建执行上下文,如果大家对于变量提升是何原理有兴趣,可以读读博主 一篇文章看懂JS执行上下文 这篇文章。对于你加深理解一定有所帮助。