前言:如果这篇文章 对你有帮助,请不要吝啬你的赞。😃
前情回顾
参加掘金日新计划时,在群里看到的问题(改造了下)
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变量
的声明提升到前面。
所以上面的代码实际上也相当于
var a
console.log(a)
变量提升只会把变量的声明提升到前面,赋值则不会提升到前面。
console.log(a)
var a = 123
console.log(a)
会先输出 undefined
,然后输出 123
预编译后的代码如下,
var a
console.log(a)
a = 123
函数提升
函数声明整体提升,函数调用不提升
console.log(mytest)
console.log(111)
function mytest() {
console.log(222)
}
console.log(mytest)
console.log(333)
mytest()
console.log(444)
预编译后:
function mytest() {
console.log(222)
}
console.log(test)
console.log(111)
console.log(mytest)
console.log(333)
mytest()
console.log(444)
使用使用变量声明函数,则走的是变量提升路线,而不是函数声明路线
console.log(mytest) // undefined
mytest() // mytest is not a function
var mytest = function () {
console.log(123)
}
函数内部也会有变量提升,这时候会先预处理全局的,再预处理函数的,且函数内的变量、函数提升不能提升到函数外。
function mytest1() {
console.log(a) // undefined
b() // 456
var a = 123
function b() {
console.log(456)
}
}
mytest1()
console.log(a) // a is not defined
预编译后的代码:
function mytest1() {
function b() {
console.log(456)
}
var a
console.log(a)
b()
a = 123
}
mytest1()
console.log(a)
如果函数内部的变量没有定义,直接赋值,则会直接变成全局变量(应该算是遗留bug,不要这样用)
function mytest1() {
b() // 456
a = 123
function b() {
console.log(456)
}
}
mytest1()
console.log(a) // 123
那么,是先变量提升,还是先函数提升呢?
有不同意见的欢迎评论。
从结果上看是函数优先,但从过程来看是变量优先
预编译步骤
这是怎么回事呢?
全局预编译
首先先来看一下全局预编译的3个步骤:
- 创建
GO对象(Global Object)
- 找变量声明,将变量作为
GO属性
(在浏览器中的话,实际上就是挂载到window
对象上),值为undefined
- 找函数声明,作为
GO属性
值为函数体
案例分析:
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 }
找函数声明(会覆盖掉重名的)
GO: { a: function a() { console.log(222) }, b: function b() { console.log(555) } }
全局预编译过程结束,开始真正的编译过程(把提升的给去掉先)
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
- 实参和形参统一(将实参的值赋值给形参)
- 找函数声明,值赋予函数体
案例分析:
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对象
找形参和变量声明
AO: { a: undefined, b: undefined, c: undefined }
实参与形参统一
AO: { a: 123, b: 456, c: undefined }
找函数声明
AO: { a: function a() { console.log(333) }, b: 456, c: undefined }
局部预编译过程结束,开始真正的编译过程(把提升的给去掉先)
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?
var a;
if (true) {
console.log(a)
a = 111
function a() { }
a = 222
console.log(a)
}
console.log(a)
分析:
- 会有两个变量声明
a
,一个在块内,一个在块外 函数声明被提升,并被绑定到内部的块变量上
var a¹; if (true) { function a²() {} console.log(a²) a² = 111 a² = 222 console.log(a²) } console.log(a¹);
这么一看,这不是和局部变量提升差不多。但是,当到达原来的函数声明处,会把块变量赋值给外部变量
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
不要用块级声明式函数
不要用块级声明式函数
不要用块级声明式函数
if (true) {
function b() {
console.log(111)
}
console.log(b) // f b() { console.log(111) }
}
console.log(b) // f b() { console.log(111) }
根据上面的分析:
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
- 但是考虑到
为什么?不知道,想不到原因,有人知道的话,评论告诉一下。(不会这样用,纯好奇为什么)
实际上,想要根据条件切换函数,可以用以下形式
let fn
if (true) {
fn = function () {
console.log(111)
}
} else {
fn = function () {
console.log(222)
}
}
fn()