Javascript深入浅出之声明与提升

var 声明了一个变量,同时可以初始化该变量。

1
var name1 [= value1 [, varname2 [, varname3 ... [, varnameN]]]];

单 var 模式

在函数的顶部使用一个单独的 var 语句是非常推荐的一种模式,它有如下一些好处:

  • 在同一个位置可以查找到函数所需的所有变量
  • 避免当在变量声明之前使用这个变量时产生的逻辑错误
  • 提醒你不要忘记声明变量,顺便减少潜在的全局变量
  • 代码量更少(输入更少且更易做代码优化)

var 模式看起来像这样:

1
2
3
4
5
6
7
8
9
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}

单 var 衍生的误区

1
2
3
4
5
6
function test() {
var param1 = param2 = 2;
}
test();
console.log(param1); // Uncaught ReferenceError
console.log(param2); // 2

可能有人就会疑惑,为什么这里的 param1 是局部变量,而 param2 却是全局变量呢?

= 运算从右向左合并,上一段代码其实是这样运行的:

1
2
3
4
5
function test() {
var param1;
param2 = 2; // 全局变量 (严格模式下会抛出错误)
param1 = param2; // 局部变量
}

未声明变量

给一个非声明变量赋值会隐式创建一个全局变量(全局object的一个属性)。

这句话大家应该都很熟悉,即便你是刚入门的新手;但也有人不完全赞同这句话,他们感觉它不符合ECMAScript的基本变量的定义,它只算是全局属性;那我们就来细细探讨一下声明变量与未声明变量的区别:

  1. 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的。
  2. 声明变量在任何代码执行前创建,而非声明变量只有在执行赋值操作的时候才会被创建。

    1
    2
    3
    4
    5
    console.log(a); // Uncaught ReferenceError
    console.log(b); // 2
    a = 1;
    var b = 2;
  3. 声明变量是它所在上下文环境的不可配置属性(non-configurable property),非声明变量是可配置的(例如非声明变量可以被删除)。

    1
    2
    3
    4
    5
    6
    7
    var a =1;
    b = 1;
    console.log(delete a); // false
    console.log(delete b); // true
    console.log(this.a); // 1
    console.log(this.b); // Uncaught ReferenceError

所以,使用未经声明的变量会带来很多潜在的代码危险,也建议大家尽量不要使用;而且严格模式下会抛出错误。

提升

先看一段代码:

1
2
3
a = 2;
var a;
console.log(a);

很多人可能认为这段代码输出的是 undefined ;因为开始 a 被初始化为2,接着又被重新声明并被默认赋值为 undefined

再看下一段代码:

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

这段代码可能又会有人认为它要报错了,因为在执行 console.loga 尚未声明。

其实刚才两段代码会分别输出 2undefined ,那又是为什么呢? 在引擎解释代码之前,编译器会预先对其进行编译,编译工作的其中一块就是将所有的声明预先进行处理。 当你看到 var a = 2 时,你以为直接就是将 2 赋值给 a ,其实它会分成两个阶段进行,第一阶段就是进行 var a 编译(在当前作用域代码执行前); 第二阶段就是执行 a = 2(等待js执行到代码所在行); 所以第一段代码真实的执行顺序是这样的:

1
2
3
var a;
a = 2;
console.log(a);

第一阶段就是编译,第二阶段是执行;这就是 变量提升
类似地,第二段是这样处理的:

1
2
3
var a;
console.log(a);
a = 2;

提升规则

我们先来看看会进行提升的都有哪些,稍后再进行解释:

  1. 函数声明,函数声明优先级最高,忽略重复的变量和形参声明
  2. 形参,比变量声明优先,忽略重复的变量声明
  3. 变量声明

※ 提升只会在其自身执行上下文中进行。什么?不知道执行上下文?下篇文章会介绍!

形参

1
2
3
4
5
function foo(a) {
console.log(a);
}
foo(2); // 2

上面 foo 在执行时,会先进行 隐式的声明赋值 操作 var a = 2 ,然后在执行函数中的代码段;怎么证明呢?我们来改一下上面的代码:

1
2
3
4
5
6
7
function foo(a) {
a = 3;
console.log(a);
}
foo(2); // 3
console.log(a); // Uncaught ReferenceError

如果它没有这个隐式的声明,这里的 a 应该是全局变量(属性),函数体外就可以访问到。

因为它的这一特性,我之前一直以为形参和变量一模一样,只声明不赋值(其实默认赋值undefined),只不过执行时它的代码在函数体所有代码之上,后来发现其实不然。 形参声明的同时会接受实参的赋值,如果没有对应实参,赋值为undefined

1
2
3
4
5
6
function foo(a) {
console.log(a);
function a() {};
}
foo(2); // function a() {}

如果按照我原先的理解,上面这段代码,在所有声明提升之后,执行 console.log(a); 之前会有一个 a = 2 的赋值,最后输出的就不会是 function 而是 2 。所以很明显,形参声明的同时就已经接收了实参的赋值,但优先级不高被函数声明覆盖了。

函数声明

注意我们这里写的是 函数声明 ,不包含 函数表达式

1
2
foo();
function foo() {}

这一段代码可以正常执行,因为 foo 函数的声明已经被提升了。

1
2
3
4
foo(); // Uncaught ReferenceError
a(); // Uncaught TypeError
var a = function foo() {};
foo(); // Uncaught ReferenceError

上面这段代码可以看出来函数表达式不会让函数得到提升。而 a() 为什么会提示Uncaught TypeError呢,因为上面代码实际是这样处理的:

1
2
3
4
5
var a;
foo();
a();
a = function foo() {};
foo(); // Uncaught ReferenceError

执行到 a() 的时候 a 还只是被默认赋值为 undefined ,并不包含对函数的引用。
还要提到的一点就是,函数 foo 只能在其自身作用域内被发现。

函数声明比变量声明优先,忽略重复的变量声明

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

上面第一个 console.log(a); 会输出 function a() {} 是因为函数声明优先,并且忽略了后面同名变量的声明。第二个 console.log(a); 输出 2 是因为执行代码时不会忽略初始化操作。
代码的实际处理情况是:

1
2
3
4
5
6
7
8
function a() {}
// var a; 被忽略了
console.log(a); // function a() {}
a = 2;
console.log(a); // 2

如果是同名函数的声明则会对其进行覆盖,所以js没有函数重载。

练习

1
2
3
4
5
6
var y = 1,
x = y = typeof x;
console.log(x);
求上面代码段输出值;

答案

1
2
3
4
5
6
7
var y; // undefined
var x; // undefined
y = 1;
y = undefined;
x = undefined;
console.log(x); // undefined

热评文章