一道问题引起的重学预编译
前言:变量提升与函数提升本来是我个人觉得没必要写笔记来复习的知识。因为这部分看的面试题都能做对,就是说确实学的挺扎实的。直到遇到了下面这道题。
前情回顾
参加掘金日新计划时,在群里看到的问题(改造了下)
代码语言: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()


