一道问题引起的重学预编译
前言:变量提升与函数提升本来是我个人觉得没必要写笔记来复习的知识。因为这部分看的面试题都能做对,就是说确实学的挺扎实的。直到遇到了下面这道题。
前情回顾
参加掘金日新计划时,在群里看到的问题(改造了下)
1 |
|
基础知识回顾
变量提升
实际上,变量的提升其实算是JS的诟病了,所以es6出来了 let
和 const
之后,都是推荐使用 let
和 const
了。
在执行函数前,会先预编译,把用 var
声明的变量的声明提升到前面。
首先,假如我们只有一个语句 console.log(a)
,这样子会直接报错 a is not defined
。
如果只声明变量,但是不赋值,则会得到 undefined
。
1 |
|
那么,如果先打印 a
,之后再定义 a
呢?
1 |
|
首先呢?JS是单线程的,所以JS理论上是从上到下执行代码的,所以按理来说会报错 a is not defined
。
但是,实际上在执行代码前,会先进行一次预编译,把 var变量
的声明提升到前面。
所以上面的代码实际上也相当于
1 |
|
变量提升只会把变量的声明提升到前面,赋值则不会提升到前面。
1 |
|
会先输出 undefined
,然后输出 123
预编译后的代码如下,
1 |
|
函数提升
函数声明整体提升,函数调用不提升
1 |
|
预编译后:
1 |
|
使用使用变量声明函数,则走的是变量提升路线,而不是函数声明路线
1 |
|
函数内部也会有变量提升,这时候会先预处理全局的,再预处理函数的,且函数内的变量、函数提升不能提升到函数外。
1 |
|
预编译后的代码:
1 |
|
如果函数内部的变量没有定义,直接赋值,则会直接变成全局变量(应该算是遗留bug,不要这样用)
1 |
|
那么,是先变量提升,还是先函数提升呢?
有不同意见的欢迎评论。
从结果上看是函数优先,但从过程来看是变量优先
预编译步骤
这是怎么回事呢?
全局预编译
首先先来看一下全局预编译的3个步骤:
- 创建
GO对象(Global Object)
- 找变量声明,将变量作为
GO属性
(在浏览器中的话,实际上就是挂载到window
对象上),值为undefined
- 找函数声明,作为
GO属性
值为函数体
案例分析:
1 |
|
创建
GO对象
,找变量声明1
2
3GO: {
a: undefined
}找函数声明(会覆盖掉重名的)
1
2
3
4
5
6
7
8GO: {
a: function a() {
console.log(222)
},
b: function b() {
console.log(555)
}
}全局预编译过程结束,开始真正的编译过程(把提升的给去掉先)
1
2
3
4
5
6
7
8
9
10console.log(111)
console.log(a)
a = 333
console.log(a)
console.log(b)
console.log(b)结合
GO对象
的属性1
2
3
4
5
6
7
8
9
10console.log(111)
console.log(a) // f a() { console.log(222) }
a = 333
console.log(a) // 333
console.log(b) // f b() { console.log(555) }
console.log(b) // f b() { console.log(555) }
局部(函数)预编译
GO对象是全局预编译,所以它优先于AO对象所创建和执行。
首先先来看一下局部预编译的4个步骤:
- 创建
AO对象(Activation Object)
- 找形参和变量声明,将变量和形参作为
AO属性
,值为undefined
- 实参和形参统一(将实参的值赋值给形参)
- 找函数声明,值赋予函数体
案例分析:
1 |
|
创建
AO对象
找形参和变量声明
1
2
3
4
5AO: {
a: undefined,
b: undefined,
c: undefined
}实参与形参统一
1
2
3
4
5AO: {
a: 123,
b: 456,
c: undefined
}找函数声明
1
2
3
4
5
6
7AO: {
a: function a() {
console.log(333)
},
b: 456,
c: undefined
}局部预编译过程结束,开始真正的编译过程(把提升的给去掉先)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function mytest(a, b) {
console.log(a)
console.log(b)
console.log(c)
a = 111
console.log(a)
console.log(a)
console.log(a)
b = 444
console.log(b)
c = 555
console.log(c)
}
mytest(123, 456)结合
AO对象
的属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function mytest(a, b) {
console.log(a) // f a() { console.log(333) }
console.log(b) // 456
console.log(c) // undefined
a = 111
console.log(a) // 111
console.log(a) /// 111
console.log(a) // 111
b = 444
console.log(b) // 444
c = 555
console.log(c) // 456
}
mytest(123, 456)
从结果上看是函数优先,但从过程来看是变量优先,因为变量提升后被之后的函数提升给覆盖掉了。
回归正题
准备好基础知识后,自然就是不忘初心,开始解决最开始的问题
参考:Function declaration in block moving temporary value outside of block?
1 |
|
分析:
会有两个变量声明
a
,一个在块内,一个在块外函数声明被提升,并被绑定到内部的块变量上
1
2
3
4
5
6
7
8
9var a¹;
if (true) {
function a²() {}
console.log(a²)
a² = 111
a² = 222
console.log(a²)
}
console.log(a¹);这么一看,这不是和局部变量提升差不多。但是,当到达原来的函数声明处,会把块变量赋值给外部变量
1
2
3
4
5
6
7
8
9
10var a¹;
if (true) {
function a²() {}
console.log(a²)
a² = 111
a¹ = a² // 当到达原来的函数声明处,会把块变量赋值给外部变量
a² = 222
console.log(a²)
}
console.log(a¹);之后,块变量和外部变量不再有联系,即块变量变化不会导致外部变量的变化。
依次输出
f a() {}
、222
、111
为什么当到达原来的函数声明处,会把块变量赋值给外部变量?
the spec says so. I have no idea why. – Jonas Wilms
不要用块级声明式函数
不要用块级声明式函数
不要用块级声明式函数
1 |
|
根据上面的分析:
1 |
|
我们把if语句
的条件变为false
后:
if语句
的内容不再执行,合理- 函数没有被提升到外面
- 但是考虑到
if条件
为false
的话,可能不会预编译内容 - 但是外边的
b
却不是报错b is not defined
,而是输出undefined
- 但是考虑到
为什么?不知道,想不到原因,有人知道的话,评论告诉一下。(不会这样用,纯好奇为什么)
实际上,想要根据条件切换函数,可以用以下形式
1 |
|