让我们先从十个问题开始
让我们先来思考下面代码的输出值,如果下面的问题你都答对了,
并且知道原因,你就可以直接关闭这篇文章了。
// 问题1
a = 1;
var a;
console.log('问题1输出为:', a);
// 问题2
console.log('问题2输出为:', b);
var b = 2;
// 问题3
function hoisting1() {
console.log('问题3输出为:', c);
var c = 3;
}
hoisting1();
// 问题4
var d = 4;
function hoisting2() {
console.log('问题4输出为:', d);
var d = 44;
}
hoisting2();
// 问题5
hoisting3();
function hoisting3() {
innerHoisting();
function innerHoisting() {
console.log('问题5输出为:', '我被提升了!');
}
}
// 问题6
function hoisting4() {
function innerHoisting() {
console.log('问题6输出为:', '我被提升了!嘤嘤嘤');
}
innerHoisting();
function innerHoisting() {
console.log('问题6输出为:', '我被提升了!');
}
}
hoisting4();
// 问题7
function hoisting5() {
var innerHoisting = 7;
function innerHoisting() {
console.log('问题7输出为:', '我被提升了!');
}
console.log('问题7输出为:', innerHoisting);
}
hoisting5();
// 问题8
hoisting6();
var hoisting6;
function hoisting6() {
console.log('问题8输出为:', '我被提升了!');
}
hoisting6 = function() {
console.log('问题8输出为:', '我被提升了!嘤嘤嘤');
}
hoisting6();
// 问题9
var hoisting7 = 7;
function hoisting7() {
console.log('问题9输出为:', '我被提升了!');
}
console.log('问题9输出为:', hoisting7);
hoisting7 = function() {
console.log('问题9输出为:', '我被提升了!嘤嘤嘤');
}
// 问题10
a();
b();
c();
d();
var a = function () {
console.log('a');
};
function b() {
console.log('b');
};
var c = function d() {
console.log('d')
};
a();
b();
c();
d();
上面👆的代码输出值为:
- 问题1输出为:
1
- 问题2输出为:
undefined
- 问题3输出为:
undefined
- 问题4输出为:
undefined
- 问题5输出为:
我被提升了!
- 问题6输出为:
我被提升了!
- 问题7输出为:
7
- 问题8输出为:
我被提升了!
-- 后续输出 -->我被提升了!嘤嘤嘤
- 问题9输出为:
7
- 问题10的输出,请移步至最后
什么是提升(Hoisting)?
JavaScript引擎,在执行编写的JavaScript代码前会先对这些代码进行编译。 在这个编译的过程中,其中有一个工作就是:
找到所有的声明,并将声明的变量及函数提升到当前作用域的顶端
为了理解JavaScript引擎的工作,下面将详细的讲述一下上述问题的工作过程
什么是变量提升?
下面将通过四个问题来深入理解变量提升
问题1
回顾问题1,戳我👈
a = 1;
var a;
console.log('问题1输出为:', a);
根据上面的规则
问题1中的代码,会被JS引擎理解为👇的形式进行处理:
// 声明的变量a被提升到当前作用域的顶端
var a;
a = 1;
console.log('问题1输出为:', a);
我们最终得到的输出为:1
;
问题2
回顾问题2,戳我👈
console.log('问题2输出为:', b);
var b = 2;
同样的,问题2中的代码,会被JS引擎理解为👇的形式进行处理:
// 声明的变量b被提升到当前作用域的顶端
var b;
console.log('问题2输出为:', b);
b = 2;
我们最终得到的输出为:undefined
问题3
回顾问题3,戳我👈
function hoisting1() {
console.log('问题3输出为:', c);
var c = 3;
}
hoisting1();
由于函数上下文的关系,c
将会被提升到当前函数作用域的顶端
问题3中的代码,会被JS引擎理解为👇的形式进行处理:
function hoisting1() {
var c
console.log('问题3输出为:', c);
c = 3;
}
hoisting1();
我们最终得到的输出为:undefined
问题4
回顾问题4,戳我👈
var d = 4;
function hoisting2() {
console.log('问题4输出为:', d);
var d = 44;
}
hoisting2();
虽然上面的代码中定义了一个全局变量d
,但是在hoisting2
函数的作用域中,同时也定义了一个函数作用域的变量d
。
此时,hoisting2
函数中的变量d
,只会被提升到当前函数作用域的顶端,也就是上面规则中提到的当前作用域的顶端。
问题4中的代码,会被JS引擎理解为👇的形式进行处理:
var d;
d = 4;
function hoisting2() {
var d;
console.log('问题4输出为:', d);
d = 44;
}
hoisting2();
我们最终得到的输出为:undefined
什么是函数提升?
下面将通过五个问题来深入理解函数提升
简单的函数提升
还能记起来,刚刚的变量/函数提升的规则么?再来复习一遍:
找到所有的声明,并将声明的变量及函数提升到当前作用域的顶端
问题5
回顾问题5,戳我👈
hoisting3();
function hoisting3() {
innerHoisting();
function innerHoisting() {
console.log('问题5输出为:', '我被提升了!');
}
}
根据刚刚复习的规则,来思考一下,问题5的代码会被如何处理?
问题5中的代码,会被JS引擎理解为👇的形式进行处理:
// 声明的函数:hoisting3,被整个提升到当前作用域的顶端
function hoisting3() {
// 声明的函数:innerHoisting,被整个提升到当前作用域的顶端
function innerHoisting() {
console.log('问题5输出为:', '我被提升了!');
}
innerHoisting();
}
hoisting3();
我们最终得到的输出为:我被提升了!
假如同时声明了两个相同命名的函数,此时会如何处理?
如果遇到命名了相同名称的方法,此时遵循下面的规则:
后声明的函数,会覆盖先前声明的函数
问题6
回顾问题6,戳我👈
function hoisting4() {
function innerHoisting() {
console.log('问题6输出为:', '我被提升了!嘤嘤嘤');
}
innerHoisting();
function innerHoisting() {
console.log('问题6输出为:', '我被提升了!');
}
}
hoisting4();
结合变量/函数提升
和遇到两个相同命名的函数
的规则
问题6中的代码,会被JS引擎理解为👇的形式进行处理:
function hoisting4() {
function innerHoisting() {
// 由于被干掉了,所以嘤嘤嘤
console.log('问题6输出为:', '我被提升了!嘤嘤嘤');
}
function innerHoisting() {
console.log('问题6输出为:', '我被提升了!');
}
innerHoisting();
}
hoisting4();
后面声明的innerHoisting
函数会覆盖前面声明的innerHoisting
。
我们最终得到的输出为:我被提升了!
假如同时存在相同命名的变量和函数会,此时如何处理?
虽然变量及函数都会被提升,请记住下面一条规则
首先会提升函数,其次才提升变量。
PS:如果同时有命名相同的变量和函数,变量会由于重复声明而被忽略
问题7
回顾问题7,戳我👈
function hoisting5() {
var innerHoisting = 7;
function innerHoisting() {
console.log('问题7输出为:', '我被提升了!');
}
console.log('问题7输出为:', innerHoisting);
}
hoisting5();
根据上面的规则
问题7中的代码,会被JS引擎理解为👇的形式进行处理:
function hoisting5() {
function innerHoisting() {
console.log('问题7输出为:', '我被提升了!');
}
innerHoisting = 7;
console.log('问题7输出为:', innerHoisting);
}
我们可以看到,在函数被提升后,变量声明会覆盖函数
我们最终得到的输出为:7
。
问题8
回顾问题8,戳我👈
hoisting6();
var hoisting6;
function hoisting6() {
console.log('问题8输出为:', '我被提升了!');
}
hoisting6 = function() {
console.log('问题8输出为:', '我被提升了!嘤嘤嘤');
}
hoisting6();
尽管var hoisting6
出现在function hoisting6()
前面,但是由于它重复声明,因此被忽略掉了,因为函数声明会被提升到普通变量之前,但是出现在后面的函数声明可以覆盖前面的声明。
问题8中的代码,会被JS引擎理解为👇的形式进行处理:
function hoisting6() {
console.log('问题8输出为:', '我被提升了!');
}
hoisting6();
hoisting6 = function() {
console.log('问题8输出为:', '我被提升了!嘤嘤嘤');
}
hoisting6();
我们会先得到输出结果:我被提升了!
,再得到输出结果我被提升了!嘤嘤嘤
问题9
回顾问题9,戳我👈
var hoisting7 = 7;
function hoisting7() {
console.log('问题9输出为:', '我被提升了!');
}
console.log('问题9输出为:', hoisting7);
hoisting7 = function() {
console.log('问题9输出为:', '我被提升了!嘤嘤嘤');
}
结合问题7和问题8我们得到的经验
问题9中的代码,会被JS引擎理解为👇的形式进行处理
function hoisting7() {
console.log('问题9输出为:', '我被提升了!');
}
hoisting7 = 7
console.log('问题9输出为::', hoisting7);
hoisting7 = function() {
console.log('问题9输出为:', '我被提升了!嘤嘤嘤');
}
我们最终得到的输出为:7
最后我们为hoisting7
赋了一个新的匿名函数,依旧会生效,如果在结尾加上一行hoisting7()
,我们就能得到输出我被提升了!嘤嘤嘤
。
拓展部分
拓展:函数不同的声明方式会对函数提升造成什么影响?(最后一个问题)
问题10
回顾问题10,戳我👈
a();
b();
c();
d();
var a = function () {
console.log('a');
};
function b() {
console.log('b');
};
var c = function d() {
console.log('d')
};
a();
b();
c();
d();
让我们回忆一下,之前我们解答过的问题,相信应该不难得出答案
// 由于a是一个变量,被提升到顶部的是 var a; 我们会得到报错:Uncaught TypeError: a is not a function
a();
// 由于b是一个函数,被提升到顶部,可以被执行,我们会得到输出:b
b();
// 由于c是一个变量,被提升到顶部的是 var c; 我们会得到报错:Uncaught TypeError: a is not a function
c();
// 由于d未被定义,我们会得到报错:Uncaught ReferenceError: d is not defined
d();
// 匿名函数表达式 (anonymous function expression),此时只有变量a被提升
var a = function () {
console.log('a');
};
// 函数声明 (function declaration),此时整个函数b被提升
function b() {
console.log('b');
};
// 具名函数表达式 (named function expression),此时只有变量c被提升
var c = function d() {
console.log('d')
};
// 输出:a
a();
// 输出:b
b();
// 输出:d
c();
// 由于d未被定义,我们会得到报错:Uncaught ReferenceError: d is not defined
d();
作者的碎碎念
变量提升可以说是一个经久不衰的话题。
虽然已经有了ES6的const
和let
,也有了诸如airbnb在JavaScript书写风格中:在作用域顶部声明变量
的建议。
但是你依旧无法阻止你的队友写出这种代码,或者是维护屎山的过程中遇到此类问题,因此深刻的理解变量提升还是一件很重要的事情。
如果发现本文有错误,或者是你有关于变量/函数提升更好的问题,欢迎在评论区留言。
参考资料
- 【liuhe688】JavaScript: 变量提升和函数提升.发布于2016-10-18
- 【ben cherry】JavaScript Scoping and Hoisting.发布于2010-02-08
- 【Sunil Sandhu】What is Hoisting in JavaScript?.发布于2018-08-27
- 【MDN】Hoisting(变量提升)
- 【徐小夕】《前端实战总结》之变量提升,函数声明提升及变量作用域详解.发布于2019-11-02
- 【程序员小鹿】动画:「变量提升」原理中的变量真的进行提升了吗?.发布于2019-12-12
- 你不知道的JavaScript(上卷).第四章-提升