变量提升与函数声明提升
在上一节中,我们详细介绍了JS中的执行栈和执行上下文,还简单解释了出现变量提升的原因。这一节,我们会对变量声明、函数声明做详细的介绍,深入地了解变量提升和函数声明提升,最后我们还会介绍一下class的声明,以及let、const是如何工作的。
首先,我们来看一个问题:
console.log(a);
function a() {
console.log('fa')
}
console.log(b);
var b = 'b';
上述代码执行后会有怎样的打印结果呢?
答案揭晓: ƒ a() { console.log('fa') }
和 undefined
。
为什么会有这样的结果呢?我们首先需要了解一下JavaScript中的预处理机制。
预处理机制
JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义。
var声明--变量提升
var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。
// 示例
console.log(b);
var b = 'b';
在上述代码中,JavaScript 执行前会先对var b = 'b'
做预处理,在全局环境声明了一个值为undefined
的b
变量,所以在第一行的console.log(b)
时,会打印出undefined
。
立即执行的函数表达式(IIFE
)
因为早年 JavaScript 没有 let 和 const,只能用 var,又因为 var 除了脚本和函数体都会穿透,人民群众发明了“立即执行的函数表达式(IIFE)”这一用法,用来产生作用域。
// 为文档添加了 20 个 div 元素,并且绑定了点击事件,打印它们的序号
for(var i = 0; i < 20; i ++) {
void function(i){
var div = document.createElement("div");
div.innerHTML = i;
div.onclick = function(){
console.log(i);
}
document.body.appendChild(div);
}(i);
}
我们通过 IIFE 在循环内构造了作用域,每次循环都产生一个新的环境记录,这样,每个 div 都能访问到环境中的 i。
如果我们不用 IIFE:
for(var i = 0; i < 20; i ++) {
var div = document.createElement("div");
div.innerHTML = i;
div.onclick = function(){
console.log(i);
}
document.body.appendChild(div);
}
这段代码的结果将会是点每个 div 都打印 20,因为全局只有一个 i,执行完循环后,i 变成了 20。
有了let
关键词之后,可以用let
来声明块级作用域,于是我们就可以不用IIFE了。
function声明--函数声明提升
在全局(脚本、模块和函数体),function 声明表现跟 var 相似,不同之处在于,function 声明不但在作用域中加入变量,还会给它赋值。
// 示例
console.log(a);
function a() {
console.log('fa')
}
在上述代码中,JavaScript 执行前会先对function a (){...}
做预处理,在全局环境声明了一个值为ƒ a() { console.log('fa') }
的a
变量,所以在第一行的console.log(a)
时,会打印出ƒ a() { console.log('fa') }
。
注意:当function 声明出现在 if 等语句中的情况有点复杂,它仍然作用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,但它不再被提前赋值:
// 示例
console.log(foo);
if(true) {
function foo(){
}
}
这段代码得到 undefined
,如果没有函数声明,则会抛出错误。这说明 function 在预处理阶段仍然发生了作用,在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段。出现在 if 等语句中的 function,在 if 创建的作用域中仍然会被提前,产生赋值效果。
console.log(foo);
if(true) {
console.log(foo);
function foo(){
}
}
这段代码得到 undefined
和ƒ foo(){}
。
class声明
class声明在全局的行为与function、var都不一致,在class声明前使用class名,会抛出错误。
// 示例
console.log(C);
class C {
}
上述代码会抛出异常Uncaught ReferenceError: c is not defined
,这个行为很像是class没有预处理,但事实上并非如此。
class 声明也是会被预处理的,它会在作用域中创建变量,并且要求访问它时抛出错误,class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用。
// 示例
const a = 2;
if(true){
console.log(a); //抛错
class a {
}
}
这段代码在全局声明了一个值为2的a变量,但是在函数块中,又进行了一次class声明。class声明被预处理,在if的作用域中不会再访问外部声明的a变量,而是访问class声明的a类,所以抛出错误。
let、const
let 和 const 是都是变量的声明,它们的特性非常相似,与var声明有着很大的差异。
let 和 const 声明虽然看上去是执行到了才会生效,但是实际上,它们还是会被预处理。如果当前作用域内有声明,就无法访问到外部的变量。
备注:let、const 与 class的声明方式类似
//示例
const a = 2;
if(true){
console.log(a); //抛错
const a = 1;
}
在if的作用域中,const声明被预处理,JS引擎就已经知道后面的代码将会声明变量a,从而不允许我们访问外层作用域中的变量a。
私货课堂
先提出一个小问题:如果在同一个作用域中,同时存在函数声明和变量声明,他们的优先级是什么样子的?
console.log(a);
var a = 'varA';
console.log(a);
function a() {
console.log('funA');
}
a();
上述代码会依次输出ƒ a() { console.log('funA'); }
、varA
、抛出异常Uncaught TypeError: a is not a function
。
这是因为函数声明会优先于var变量声明,在第一行中,读取的a
是函数声明(函数声明时会提前赋值),所以会打印ƒ a() { console.log('funA'); }
;继续运行到第二行时,因为已经声明过a
了,所以会将varA
赋值给变量a
,这时a
的值为varA
。所以第三行会打印varA
;继续运行到4~6行时,因为已经声明执行过function a(){}
语句,所以跳过;继续执行到最后一行时,此时的a
是字符串varA
,而非函数,所以会抛出异常:Uncaught TypeError: a is not a function
。
在同一个作用域中,如果同时存在函数声明和变量声明,只需要记住两句话即可解决问题:
- 函数声明会优先于var变量声明
- 同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明
放几道有意思的题目,供大家更好地理解:
//Example1
foo;
var foo = function () {
console.log('foo1');
}
foo();
var foo = function () {
console.log('foo2');
}
foo();
//Example2
foo();
function foo() {
console.log('foo1');
}
foo();
function foo() {
console.log('foo2');
}
foo();
//Example3
foo();
var foo = function() {
console.log('foo1');
}
foo();
function foo() {
console.log('foo2');
}
foo();