一道问题引起的重学预编译
前言:变量提升与函数提升本来是我个人觉得没必要写笔记来复习的知识。因为这部分看的面试题都能做对,就是说确实学的挺扎实的。直到遇到了下面这道题。
前情回顾
参加掘金日新计划时,在群里看到的问题(改造了下)
代码语言:javascript复制var a;
if (true) {
console.log(a)
a = 111
function a() { }
a = 222
console.log(a)
}
console.log(a)
基础知识回顾
变量提升
实际上,变量的提升其实算是JS的诟病了,所以es6出来了 let
和 const
之后,都是推荐使用 let
和 const
了。
在执行函数前,会先预编译,把用 var
声明的变量的声明提升到前面。
首先,假如我们只有一个语句 console.log(a)
,这样子会直接报错 a is not defined
。
如果只声明变量,但是不赋值,则会得到 undefined
。
var a;
console.log(a)
那么,如果先打印 a
,之后再定义 a
呢?
console.log(a)
var a
首先呢?JS是单线程的,所以JS理论上是从上到下执行代码的,所以按理来说会报错 a is not defined
。
但是,实际上在执行代码前,会先进行一次预编译,把 var变量
的声明提升到前面。
所以上面的代码实际上也相当于
代码语言:javascript复制var a
console.log(a)
变量提升只会把变量的声明提升到前面,赋值则不会提升到前面。
代码语言:javascript复制console.log(a)
var a = 123
console.log(a)
会先输出 undefined
,然后输出 123
预编译后的代码如下,
代码语言:javascript复制var a
console.log(a)
a = 123
函数提升
函数声明整体提升,函数调用不提升
代码语言:javascript复制console.log(mytest)
console.log(111)
function mytest() {
console.log(222)
}
console.log(mytest)
console.log(333)
mytest()
console.log(444)
预编译后:
代码语言:javascript复制function mytest() {
console.log(222)
}
console.log(test)
console.log(111)
console.log(mytest)
console.log(333)
mytest()
console.log(444)
https://img.yuanmabao.com/zijie/pic/2023/03/11/fcovr1k4kpj
使用使用变量声明函数,则走的是变量提升路线,而不是函数声明路线
代码语言:javascript复制console.log(mytest) // undefined
mytest() // mytest is not a function
var mytest = function () {
console.log(123)
}
函数内部也会有变量提升,这时候会先预处理全局的,再预处理函数的,且函数内的变量、函数提升不能提升到函数外。
代码语言:javascript复制function mytest1() {
console.log(a) // undefined
b() // 456
var a = 123
function b() {
console.log(456)
}
}
mytest1()
console.log(a) // a is not defined
预编译后的代码:
代码语言:javascript复制function mytest1() {
function b() {
console.log(456)
}
var a
console.log(a)
b()
a = 123
}
mytest1()
console.log(a)
如果函数内部的变量没有定义,直接赋值,则会直接变成全局变量(应该算是遗留bug,不要这样用)
代码语言:javascript复制function mytest1() {
b() // 456
a = 123
function b() {
console.log(456)
}
}
mytest1()
console.log(a) // 123
那么,是先变量提升,还是先函数提升呢?
有不同意见的欢迎评论。
从结果上看是函数优先,但从过程来看是变量优先
预编译步骤
这是怎么回事呢?
全局预编译
首先先来看一下全局预编译的3个步骤:
- 创建
GO对象(Global Object)
- 找变量声明,将变量作为
GO属性
(在浏览器中的话,实际上就是挂载到window
对象上),值为undefined
- 找函数声明,作为
GO属性
值为函数体
案例分析:
代码语言:javascript复制console.log(111)
console.log(a)
function a() {
console.log(222)
}
var a = 333
console.log(a)
function b() {
console.log(444)
}
console.log(b)
function b() {
console.log(555)
}
console.log(b)
创建 GO对象
,找变量声明
GO: {
a: undefined
}
找函数声明(会覆盖掉重名的)
代码语言:javascript复制GO: {
a: function a() {
console.log(222)
},
b: function b() {
console.log(555)
}
}
全局预编译过程结束,开始真正的编译过程(把提升的给去掉先)
代码语言:javascript复制console.log(111)
console.log(a)
a = 333
console.log(a)
console.log(b)
console.log(b)
结合 GO对象
的属性
console.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
- 实参和形参统一(将实参的值赋值给形参)
- 找函数声明,值赋予函数体
案例分析:
代码语言:javascript复制function mytest(a, b) {
console.log(a)
console.log(b)
console.log(c)
var a = 111
console.log(a)
function a() {
console.log(222)
}
console.log(a)
function a() {
console.log(333)
}
console.log(a)
var b = 444
console.log(b)
var c = 555
console.log(c)
}
mytest(123, 456)
创建 AO对象
找形参和变量声明
代码语言:javascript复制AO: {
a: undefined,
b: undefined,
c: undefined
}
实参与形参统一
代码语言:javascript复制AO: {
a: 123,
b: 456,
c: undefined
}
找函数声明
代码语言:javascript复制AO: {
a: function a() {
console.log(333)
},
b: 456,
c: undefined
}
局部预编译过程结束,开始真正的编译过程(把提升的给去掉先)
代码语言:javascript复制function 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对象
的属性
function 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?
代码语言:javascript复制var a;
if (true) {
console.log(a)
a = 111
function a() { }
a = 222
console.log(a)
}
console.log(a)
分析:
会有两个变量声明 a
,一个在块内,一个在块外
函数声明被提升,并被绑定到内部的块变量上
代码语言:javascript复制 var a¹;
if (true) {
function a²() {}
console.log(a²)
a² = 111
a² = 222
console.log(a²)
}
console.log(a¹);
这么一看,这不是和局部变量提升差不多。但是,当到达原来的函数声明处,会把块变量赋值给外部变量
代码语言:javascript复制var 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
不要用块级声明式函数
不要用块级声明式函数
不要用块级声明式函数
代码语言:javascript复制if (true) {
function b() {
console.log(111)
}
console.log(b) // f b() { console.log(111) }
}
console.log(b) // f b() { console.log(111) }
根据上面的分析:
代码语言:javascript复制if (true) {
function b²() {
console.log(111)
}
b¹ = b² // 没有定义,直接赋值,变为全局变量
console.log(b²) // f b() { console.log(111) }
}
console.log(b¹) // f b() { console.log(111) }
我们把if语句
的条件变为false
后:
if语句
的内容不再执行,合理- 函数没有被提升到外面
- 但是考虑到
if条件
为false
的话,可能不会预编译内容 - 但是外边的
b
却不是报错b is not defined
,而是输出undefined
- 但是考虑到
为什么?不知道,想不到原因,有人知道的话,评论告诉一下。(不会这样用,纯好奇为什么)
实际上,想要根据条件切换函数,可以用以下形式
代码语言:javascript复制let fn
if (true) {
fn = function () {
console.log(111)
}
} else {
fn = function () {
console.log(222)
}
}
fn()