前言

在讲「变量提升」之前,建议你先要去了解「变量对象」与「作用域」的概念,关于这两点,你可以 点此查看 我前面的两篇文章。

我们知道,变量对象有创建和执行两个生命周期,而变量提升,就发生在变量对象创建的时候。

接下来我们复习一下,变量对象的创建过程。

一、变量对象(VO)的创建过程

1. 为函数的所有形参赋值

在进入函数执行上下文时,会首先检查实参个数,接着对实参对象和形参进行赋值,如果传入的实参数量小于形参数量,则会将没有被赋值的形参赋值为 undefined

  1. // 函数执行上下文
  2. function bar(a,b,c){
  3. console.log(a,b,c); // 1 4 undefined
  4. }
  5. bar(1,4);
  6. // 变量对象
  7. VO = {
  8. a: 1,
  9. b: 4,
  10. c: undefined;
  11. }

2. 检查当前执行上下文中的函数声明

此时会检查当前执行上下文中是否存在函数声明,如果存在,则会将函数名与函数引用地址组成的键值对存入变量对象。

如果当前环境中已经存在同名函数,则后面的会覆盖之前的。

  1. console.log(bar); // function bar() { console.log('bar')}
  2. bar(1); // 'bar'
  3. function bar(a,b) {
  4. function foo() {
  5. console.log(foo);
  6. }
  7. console.log('bar');
  8. }

在上面的例子中,我们的函数声明是在第三行,但在前两行中都可以对 bar 进行打印或调用。原因就在于,在代码执行之前,全局执行上下文中的变量对象中,已经保存了 bar 函数的声明。

  1. // 全局执行上下文的变量对象
  2. GlobalVO = {
  3. bar: <ref>,
  4. }
  5. // bar 函数的执行上下文变量对象
  6. barVO = {
  7. a: 1,
  8. b: undefined,
  9. foo: <ref>, // 函数引用
  10. }

3. 检查当前执行上下文中的变量声明

接着,会检查当前环境中是否有变量声明,什么叫变量声明?就是以 varlet 或者 const 开头的语句。如果找到以 var 开头的变量声明,则会将变量赋值为 undefined 后存入变量对象中。

如果已经存在同名变量或函数,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

  1. console.log(a); // undefined
  2. var a = 1;

创建阶段的变量对象:

  1. GlobalVO = {
  2. a: undefined
  3. }

以上,就是变量对象在创建阶段要做的事情,这些事情都是在代码未执行时就已经完成的工作。大家看完是不是觉得,好简单啊。

嘿嘿,看看下面的题目再说这句话吧。

二、从题目中看变量提升

如果说看到上面的概念就认为自己掌握了变量提升,还为时尚早,下面我们来几道小题,认真体会,相信会对你有很大的收获。

题目一

  1. console.log(a); // ?
  2. if(false) {
  3. var a = 1;
  4. }

请诚实的告诉我,有没有人觉得第一行会报出 a is not defined。但实际答案是 undefined。为什么?

大家可能会认为,if 中的语句未执行,所以 a 是为定义的。的确,赋值操作是没有执行,但在执行 console 之前,在全局作用域中的所有 var 声明的变量,都已经被赋值为 undefined 后添加到了变量对象中。

上面的例子,实际过程中更像是这样:

  1. var a;
  2. console.log(a);
  3. if(false) {
  4. a = 1
  5. }

题目二

  1. console.log(a);
  2. function bar() {
  3. var a = 3;
  4. }
  5. bar()

上面那个题我们知道答案了,看看这道题目呢?会打印出 undefined 还是 3?

答案是会报错:a is not defined

为什么?

我们看下,与上一题相比,这道题的 a 是在函数内声明的,函数是会形成自己的函数作用域的,在函数作用域内声明的变量在外部是无法访问到的。

因此,在全局作用域中打印 a,注定是要报错的呀!

题目三

  1. b(); // ?
  2. var a = 1;
  3. var b = function (){
  4. console.log('欢迎关注我的微信公众号,web独白');
  5. }

有同学看到这个题目可能会认为第一行函数会打印出内容,却没想到浏览器会报错:b is not a function。为啥来?

原因在于这些同学没有分清「函数声明」与 「函数表达式」的区别。

只有以 function 开头声明的函数才是函数声明,其他形式的函数赋值,均是函数表达式。

只用函数声明会按照变量对象创建过程的第二步进行,而函数表达式实际上是一个变量声明,会按照第三步进行。

上面的例子中,实际的顺序大概是这样:

  1. var a;
  2. var b;
  3. console.log(b); // b is not a function
  4. a = 1;
  5. b = function() {...}

题目四

  1. var a = 4;
  2. function a() {
  3. console.log('web 独白');
  4. }
  5. a(); // ?

这题会打印出什么呢?

大家可能会认为会打印出函数内容,但实际上会报错。

有些人可能会有疑问,明明是先进行函数声明,接着进行变量声明,如果已经存在同名变量或函数,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。那执行的时候 a 就应该是函数啊。

其实还是因为大家没有好好理解上面这句话。我们说了,如果存在同名函数或变量,变量声明则会忽略,但是,赋值是执行后的操作,这是不会忽略的。

上面代码真正的执行是像这样:

  1. function a() {}; // 函数声明提升至顶部
  2. var a ; // 同名的变量声明将会跳过
  3. a = 4; // 开始执行赋值操作
  4. a(); // 对数字执行函数操作,肯定会报错

好了,相信这几道小题让你对变量提升有了更深的认识。另外,要记住变量提升要考虑它所在的执行环境的。

三、一道小题

最后,照例留一道小小的题目,看看自己对本节内容的掌握程度。

  1. bar(a)
  2. function a() {
  3. console.log('outA1')
  4. }
  5. function bar(a,b) {
  6. a();
  7. function a() {
  8. console.log('innerA')
  9. }
  10. }
  11. var a = function () {
  12. console.log('outA2')
  13. }
  1. 第一行 bar(a) 中的 a 是什么?
  2. 整段代码最终会打印出什么结果?为什么是这个结果?