一道问题引起的重学预编译

2023-03-11 16:33:09 浏览数 (1)

一道问题引起的重学预编译

前言:变量提升与函数提升本来是我个人觉得没必要写笔记来复习的知识。因为这部分看的面试题都能做对,就是说确实学的挺扎实的。直到遇到了下面这道题。

前情回顾

参加掘金日新计划时,在群里看到的问题(改造了下)

代码语言: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

代码语言:javascript复制
var a;
console.log(a)

那么,如果先打印 a,之后再定义 a呢?

代码语言:javascript复制
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个步骤:

  1. 创建 GO对象(Global Object)
  2. 找变量声明,将变量作为GO属性(在浏览器中的话,实际上就是挂载到 window对象上),值为 undefined
  3. 找函数声明,作为 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对象,找变量声明

代码语言:javascript复制
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对象的属性

代码语言:javascript复制
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个步骤:

  1. 创建 AO对象(Activation Object)
  2. 找形参和变量声明,将变量和形参作为 AO属性,值为 undefined
  3. 实参和形参统一(将实参的值赋值给形参)
  4. 找函数声明,值赋予函数体

案例分析:

代码语言: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对象的属性

代码语言:javascript复制
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()

0 人点赞