我们都知道,变量在使用之前必须声明。但我们在 javascript 中可能见到这样的代码。

1
2
3
console.log(a);
var a = 'a';
// 输出结果: undefined

这着实令我们费解。当然事出必有因,这也引出了我们今天要讨论的内容:变量提升 (Hoisting)

变量提升

ES6 之前,javascript 的作用域一共两种:全局作用域方法作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 这里是全局作用域
var a = '全局的 a';
console.log(a); // '全局的 a'

(function f1() {
// 这里是 f1 的方法作用域
var a = 'f1 的 a';
console.log(a); // 'f1 的 a'
})();

(function f2() {
// 这里是 f2 的方法作用域
var a = 'f1 的 a';
console.log(a); // 'f2 的 a'
})();

(function f3() {
// 这里是 f3 的方法作用域
// 如果方法作用域中没有声明该变量,就使用全局作用中的对应变量。
// 修改会影响全局作用域的变量。
console.log(a); // '全局的 a'
})();

(function f4() {
// 这里是 f4 的方法作用域
console.log(a); // undefined
var a = 'f4 的 a';
})();

console.log(a); // '全局的 a'

不难看出,各个作用域之间声明的变量相互独立,互不影响。上面的代码中我们对 f4() 感到疑惑。a 不是应该使用全局作用域的 a, 输出为:'全局的 a' 吗?
这就是之前提到的 变量提升,javascript 中,每一个变量的声明都被提升到所在作用域的顶部,不过初始化顺序不变。
那么,对于 f4() 方法而言:var a = 'f4 的 a'; 等同于 var a; a = 'f4 的 a';。所以整个 f4() 方法就等同于以下代码:

1
2
3
4
5
6
(function f4() {
// 这里是 f4 的方法作用域
var a; // 由于变量提升,a 的声明被放在了作用域的顶部
console.log(a); // undefined
a = 'f4 的 a';
})();

这样就豁然开朗了,我们开始时的疑问解决了。不得不说,这样的设计在调试程序时确实很糟糕。
随着 ES6 的到了,引入了两个新的关键字 letconstconst 和其他语言并无差,用于声明常量,一旦声明,不能修改。
letvar 类似(ES6 已不再推荐使用 var),但抛弃了 var 的“不合理”行为。

  • 同一作用域内不能多次声明同一变量。
  • 不再出现变量提升。
  • ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 同一作用域内不能多次声明同一变量。
let a; // 或者 var a;
let a; // error

// 不再出现变量提升。
console.log(b); // error, b is not defined
let b;

// 对于第三条而言,其可以防止变量提升的出现,对于下面情况也会出错。
let c;
(function() {
c = 'c'; // error, c is not defined
let c;
})();

函数提升

你可能也猜到了,既然由 变量提升,那肯定也会有 函数提升 吧。
是的,我们也来看看它的表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn0('Hello ~'); // Hello ~
function fn0(word) {
console.log(word);
}

// ------------------------

var fn1 = function() {
console.log('111');
};
function fn1() {
console.log('222');
}
fn1(); // 111

不难发现,fn1() 被提到了顶部。然后,被第一个方法“覆盖”。

关于函数

javascript 中函数的声明我们一般采用下面两种方式:

1
2
3
4
5
6
7
8
9
// 第一种
var fn = function() {
console.log(this);
};

// 第二种
function fn() {
console.log(this);
}

这两种方式基本等同。不过第二种会出现 函数提升,而第一种则不会。

对于 javascript 而言,函数其实是 Function 对象的实例。

1
2
3
4
5
var fn = function(word) {
console.log(word);
};
// 等同于
var fn = new Function("word", "console.log("word");"); // 参数必须都是字符串