通常在直觉上我们会认为代码执行时是从上到下按顺序执行的,但在 JavaScript 中(就同步代码而言),这并不完全正确。
小二,上栗子!
实例 1:
a = 2;
var a;
console.log(a); // ?
如果实例 1 的代码是自上而下执行的话,那么 var a
在a = 2
之后,应该是变量被重新赋值为undefined
了。但输出的结果其实是 2。
实例 2:
console.log(a); // ?
var a = 2;
根据上一个例子的代码的表现,如果代码并不是完全自上而下执行的,那实例 2 代码里会输出 2,还是会报错呢。实际上输出的是 undefined
。
哦豁,到底发生了什么?
一、预解析
JavaScript 引擎在执行任何代码片段(例如函数调用)之前,会先对其中所有变量(包括函数)声明进行处理,这是一个预解析的过程。
1. 变量提升
JavaScript 会将var a = 2;
看成是两个操作:var a;
和a = 2;
。第一个声明是预解析阶段进行的,第二个赋值操作留在原地等待执行阶段。
上面实例 1 的代码可以认为被处理成如下形式:
// 预解析阶段
var a;
//----------------
// 执行阶段
a = 2;
console.log(a); // 2
实例 2 的代码可以认为被处理成如下形式:
// 预解析阶段
var a;
//----------------
// 执行阶段
console.log(a); // undefined
a = 2;
这样来看,代码的执行顺序就变得正常了。预解析的过程好像是变量的声明被“移动”到顶部,这个过程就叫作提升。
2. 函数提升
函数声明同样也会被提升,但是跟变量的提升行为有所不同。
实例 3:
fn(); // 输出 undefined
function fn() {
console.log(a);
var a = 1;
}
在实例 3 中,在函数声明之前就可以正常调用函数,因为整个函数体都被提升了。值得注意的是,函数内也声明了一个变量 a ,那这个变量会被提升到哪里呢,这就跟作用域有关系了。
每个作用域都会发生提升行为,且只提升到当前作用域的顶部。在函数被调用时,才会对函数内部代码进行预解析。因此实例 3 的代码可以等同于如下形式:
function fn() {
var a;
console.log(a);
a = 1;
}
fn(); // 输出 undefined
另外,函数表达式的提升行为,是跟普通变量一样的。
实例 4:
fn(); // 报错,只提升了 var fn; 不能对 undefined 进行函数调用
var fn = function() {
console.log("fn");
}
3. 函数优先
我们知道变量声明和函数声明都会被提升,那么在重复声明的情况下,JavaScript 引擎会怎么处理呢?
预解析过程中,每遇到一个 var
关键字的变量声明,首先会查询当前作用域之前是否已经有了该名称的变量,如果是,则会忽略该声明;如果没有则把该变量声明提升。
所以需要注意的是,预解析时函数首先被提升,然后才到变量。
实例 5:
fn(); // 1
var fn;
function fn() {
console.log(1);
}
fn = function() {
console.log(2);
}
fn(); // 2
实例 5 中,虽然 var fn;
出现在 function fn (){...}
之前,但因为首先提升函数,而同名的 var 声明就被忽略了。
尽管同名的 var 声明会被忽略掉,但是后出现的函数声明是能够覆盖前面的。
实例 6:
fn(); // 3
function fn() {
console.log(1);
}
var fn = function() {
console.log(2);
}
function fn() {
console.log(3);
}
虽然这些看起来似乎都是并没有什么用的理论,一般也没谁这么写,但是至少说明了在同一个作用域内重复声明是非常糟糕的,会导致各种莫名其妙的问题,可见良好的编程习惯是多么重要!
二、暂时性死区
前面提到的种种提升行为,并没有出现 let
和 const
关键字。因为,let
和 const
并不会表现出变量提升的现象。
ES6 明确规定,代码块(
{}
)中如果出现 let 和 const 声明的变量,这些变量的作用域会被限制在代码块内,也就是块级作用域。
实例 7:
var a = 1;
if(true){
a = 2;
console.log(a); // ?
let a;
}
实例 7 代码中,在a = 2
这里就已经报错:Cannot access 'a' before initialization
,意思是无法在初始化之前访问 a,这是为什么呢?
在预解析的时候,JavaScript 引擎当然也会注意到 let 和 const 声明的变量,因为实例 7 的块级作用域中存在变量 a ,便不会继续去外部作用域查找变量。
严格来说, let 和 const 也会有类似“提升“的行为,但跟 var
不同的是,提升的时候变量值并不会默认赋值为 undefined
,并且会禁止在声明之前使用这些变量,这就是所谓的暂时性死区。
实例 8:
var a = 1;
if(true){
// 死区开始--------------------------
// 访问 a 都会报错,不能在声明之前使用
a = 2;
console.log(a);
// 死区结束--------------------------
let a;
console.log(a); // undefined
a = 3;
console.log(a); // 3
}
暂时性死区的设计,也是为了提倡大家先声明,后使用,养成良好的编程习惯。同时推荐大家不管是用哪种声明方式,最好都先声明然后再使用,才能避免变量提升现象给代码带来的负面影响。
暂时性死区是 let 和 const 共同的特性,为了方便起见,上面的例子都只用了 let 关键字。
三、总结
var
声明的变量,只提升声明,赋值操作留在原地。- 函数声明提升整个函数体,函数表达式的提升行为和变量一致。
- 同作用域中,如果函数声明和
var
声明同名,只提升函数声明,忽略var
声明。 let
和const
有暂时性死区,必须先声明后使用。
感谢你花费宝贵的时间阅读本文,文章在撰写过程中难免有疏漏和错误,欢迎你在下方留言指出文章的不足之处;如果觉得这篇文章对你有用,也欢迎你点赞和留下你的评论哦。