前言
在讲「变量提升」之前,建议你先要去了解「变量对象」与「作用域」的概念,关于这两点,你可以 点此查看 我前面的两篇文章。
我们知道,变量对象有创建和执行两个生命周期,而变量提升,就发生在变量对象创建的时候。
接下来我们复习一下,变量对象的创建过程。
一、变量对象(VO)的创建过程
1. 为函数的所有形参赋值
在进入函数执行上下文时,会首先检查实参个数,接着对实参对象和形参进行赋值,如果传入的实参数量小于形参数量,则会将没有被赋值的形参赋值为 undefined
。
// 函数执行上下文
function bar(a,b,c){
console.log(a,b,c); // 1 4 undefined
}
bar(1,4);
// 变量对象
VO = {
a: 1,
b: 4,
c: undefined;
}
2. 检查当前执行上下文中的函数声明
此时会检查当前执行上下文中是否存在函数声明,如果存在,则会将函数名与函数引用地址组成的键值对存入变量对象。
如果当前环境中已经存在同名函数,则后面的会覆盖之前的。
console.log(bar); // function bar() { console.log('bar')}
bar(1); // 'bar'
function bar(a,b) {
function foo() {
console.log(foo);
}
console.log('bar');
}
在上面的例子中,我们的函数声明是在第三行,但在前两行中都可以对 bar 进行打印或调用。原因就在于,在代码执行之前,全局执行上下文中的变量对象中,已经保存了 bar
函数的声明。
// 全局执行上下文的变量对象
GlobalVO = {
bar: <ref>,
}
// bar 函数的执行上下文变量对象
barVO = {
a: 1,
b: undefined,
foo: <ref>, // 函数引用
}
3. 检查当前执行上下文中的变量声明
接着,会检查当前环境中是否有变量声明,什么叫变量声明?就是以 var
、let
或者 const
开头的语句。如果找到以 var
开头的变量声明,则会将变量赋值为 undefined
后存入变量对象中。
如果已经存在同名变量或函数,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
console.log(a); // undefined
var a = 1;
创建阶段的变量对象:
GlobalVO = {
a: undefined
}
以上,就是变量对象在创建阶段要做的事情,这些事情都是在代码未执行时就已经完成的工作。大家看完是不是觉得,好简单啊。
嘿嘿,看看下面的题目再说这句话吧。
二、从题目中看变量提升
如果说看到上面的概念就认为自己掌握了变量提升,还为时尚早,下面我们来几道小题,认真体会,相信会对你有很大的收获。
题目一
console.log(a); // ?
if(false) {
var a = 1;
}
请诚实的告诉我,有没有人觉得第一行会报出 a is not defined
。但实际答案是 undefined
。为什么?
大家可能会认为,if
中的语句未执行,所以 a
是为定义的。的确,赋值操作是没有执行,但在执行 console
之前,在全局作用域中的所有 var
声明的变量,都已经被赋值为 undefined
后添加到了变量对象中。
上面的例子,实际过程中更像是这样:
var a;
console.log(a);
if(false) {
a = 1
}
题目二
console.log(a);
function bar() {
var a = 3;
}
bar()
上面那个题我们知道答案了,看看这道题目呢?会打印出 undefined
还是 3?
答案是会报错:a is not defined
为什么?
我们看下,与上一题相比,这道题的 a
是在函数内声明的,函数是会形成自己的函数作用域的,在函数作用域内声明的变量在外部是无法访问到的。
因此,在全局作用域中打印 a
,注定是要报错的呀!
题目三
b(); // ?
var a = 1;
var b = function (){
console.log('欢迎关注我的微信公众号,web独白');
}
有同学看到这个题目可能会认为第一行函数会打印出内容,却没想到浏览器会报错:b is not a function
。为啥来?
原因在于这些同学没有分清「函数声明」与 「函数表达式」的区别。
只有以 function
开头声明的函数才是函数声明,其他形式的函数赋值,均是函数表达式。
只用函数声明会按照变量对象创建过程的第二步进行,而函数表达式实际上是一个变量声明,会按照第三步进行。
上面的例子中,实际的顺序大概是这样:
var a;
var b;
console.log(b); // b is not a function
a = 1;
b = function() {...}
题目四
var a = 4;
function a() {
console.log('web 独白');
}
a(); // ?
这题会打印出什么呢?
大家可能会认为会打印出函数内容,但实际上会报错。
有些人可能会有疑问,明明是先进行函数声明,接着进行变量声明,如果已经存在同名变量或函数,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。那执行的时候 a
就应该是函数啊。
其实还是因为大家没有好好理解上面这句话。我们说了,如果存在同名函数或变量,变量声明则会忽略,但是,赋值是执行后的操作,这是不会忽略的。
上面代码真正的执行是像这样:
function a() {}; // 函数声明提升至顶部
var a ; // 同名的变量声明将会跳过
a = 4; // 开始执行赋值操作
a(); // 对数字执行函数操作,肯定会报错
好了,相信这几道小题让你对变量提升有了更深的认识。另外,要记住变量提升要考虑它所在的执行环境的。
三、一道小题
最后,照例留一道小小的题目,看看自己对本节内容的掌握程度。
bar(a)
function a() {
console.log('outA1')
}
function bar(a,b) {
a();
function a() {
console.log('innerA')
}
}
var a = function () {
console.log('outA2')
}
- 第一行
bar(a)
中的a
是什么? - 整段代码最终会打印出什么结果?为什么是这个结果?