你不知道的 JS-上
作用域和闭包
提升
直觉上会认为 JS 代码在执行时是由上倒下一行一行执行的。但实际上这并不完全正确。
我们回顾一下第 1 章中关于编译器的内容。引擎在解释 JS 代码之前会先对其进行编译。编译阶段中的一部分工作就是找到所有声明,并用合适的作用域将他们关联起来。第 2 章中展示了这个机制,也正是词法作用域的核心内容。
因此,正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2;
cosole.log(a);
会被处理为
var a;
a = 2;
console.log(a);
这个过程就好像变量和函数声明从他们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升。
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
会被处理为
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
可以看到,函数声明会被提升,但函数表达式却不会被提升。
foo(); //不是ReferenceError,而是TypeError!
var foo = function bar() {
// ...
};
同时也要记住,即使是具名的函数表达式,名称标识符在赋值前也无法在所在作用域中使用:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
这个代码会被提升后,会被理解为一下形式:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function {
var bar = ...self...
// ...
};
函数优先
函数会优先被提升,然后才是变量。考虑以下代码:
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
该段代码会被输出1,而不是2!会被引擎理解为如下形式:
function foo() {
console.log(1);
}
foo();
foo = function () {
console.log(2);
};
注意,var foo尽管出现在function foo()...的声明之前,但它是重复声明(因此被忽略了),因此函数声明会被提升到普通变量之前。
尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是会覆盖前面的。
foo(); // 3
function foo() {
console.log(1);
}
var foo = function() {
console.log(2);
};
function foo() {
console.log(3);
}
开发中在同一作用域进行重复定义是非常糟糕的,会导致各种奇怪的问题。
一个普通块内部的函数声明通常会被提升到所在的作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:
foo(); // TypeError: foo is not a function
var a = true;
if(a) {
function foo() {console.log("a");}
}else {
function foo() {console.log("b");}
}