重学预编译

简介: 重学预编译
前言:如果这篇文章 对你有帮助,请不要吝啬你的赞。😃

前情回顾

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

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)

image-20220407234205587

使用使用变量声明函数,则走的是变量提升路线,而不是函数声明路线

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个步骤:

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

    GO: {
        a: undefined
    }
  2. 找函数声明(会覆盖掉重名的)

    GO: {
        a: function a() {
            console.log(222)
        },
        b: function b() {
            console.log(555)
        } 
    }
  3. 全局预编译过程结束,开始真正的编译过程(把提升的给去掉先)

    console.log(111)
    console.log(a)
    
    a = 333
    
    console.log(a)
    
    console.log(b)
    
    console.log(b)
  4. 结合 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个步骤:

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

案例分析:

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)
  1. 创建 AO对象
  2. 找形参和变量声明

    AO: {
        a: undefined,
        b: undefined,
        c: undefined
    }
  3. 实参与形参统一

    AO: {
        a: 123,
        b: 456,
        c: undefined
    }
  4. 找函数声明

    AO: {
        a: function a() {
            console.log(333)
        },
        b: 456,
        c: undefined
    }
  5. 局部预编译过程结束,开始真正的编译过程(把提升的给去掉先)

    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)
  6. 结合 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)

分析:

  1. 会有两个变量声明 a,一个在块内,一个在块外
  2. 函数声明被提升,并被绑定到内部的块变量上

     var a¹;
     if (true) {
       function a²() {} 
       console.log(a²)
       a² = 111
       a² = 222
       console.log(a²)
    }
    console.log(a¹);
  3. 这么一看,这不是和局部变量提升差不多。但是,当到达原来的函数声明处,会把块变量赋值给外部变量

     var a¹;
     if (true) {
       function a²() {} 
       console.log(a²)
       a² = 111
       a¹ = a²        // 当到达原来的函数声明处,会把块变量赋值给外部变量
       a² = 222
       console.log(a²)
    }
    console.log(a¹);
  4. 之后,块变量和外部变量不再有联系,即块变量变化不会导致外部变量的变化。
  5. 依次输出 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()
目录
相关文章
|
6天前
|
存储 缓存 Java
写代码原来如此简单:两种常用代码范式
一次项目包含非常多的流程,有需求拆解,业务建模,项目管理,风险识别,代码模块设计等等,如果我们在每次项目中,都将精力大量放在这些过程的思考上面,那我们剩余的,放在业务上思考的精力和时间就会大大减少;这也是为什么我们要 总结经验/方法论/范式 的原因;这篇文章旨在建立代码模块设计上的思路,给出了两种非常常用的设计范式,减少未来在这一块的精力开销。
|
3月前
|
存储 C语言
C语言程序设计核心详解 第七章 函数和预编译命令
本章介绍C语言中的函数定义与使用,以及预编译命令。主要内容包括函数的定义格式、调用方式和示例分析。C程序结构分为`main()`单框架或多子函数框架。函数不能嵌套定义但可互相调用。变量具有类型、作用范围和存储类别三种属性,其中作用范围分为局部和全局。预编译命令包括文件包含和宏定义,宏定义分为无参和带参两种形式。此外,还介绍了变量的存储类别及其特点。通过实例详细解析了函数调用过程及宏定义的应用。
|
5月前
|
存储 分布式计算 索引
Python函数式编程入门窥探
Python本身不是一门函数式编程语言,但是它参考了一些函数式编程语言很好的地方,除了可以写出更可读的代码外。还能用它来实现一些特定功能,本身也提供了强大的注解系统和函数和对象之间的灵活调用。
|
5月前
|
Python
告别代码冗余!Python闭包与装饰器如何让你秒变代码优化大师?
【7月更文挑战第6天】Python的闭包和装饰器是解决代码冗余的利器。闭包,如匿名函数,记忆外部作用域变量,实现代码封装。例如,`make_multiplier_of`生成特定乘法函数,避免重复。装饰器如`@my_decorator`,不修改原函数,添加额外功能,如在函数调用前后打印信息。两者结合,提升代码灵活性和复用性,是优化和整洁代码的关键。
36 0
|
7月前
|
测试技术 程序员 Python
Python 妙用运算符重载——玩出“点”花样来(上)
Python 妙用运算符重载——玩出“点”花样来(上)
90 0
|
7月前
|
索引 Python
Python 妙用运算符重载——玩出“点”花样来(下)
Python 妙用运算符重载——玩出“点”花样来(下)
86 0
|
自然语言处理 编译器
编译原理之词法分析器随笔和简单实现
编译原理之词法分析器随笔和简单实现
编译原理之词法分析器随笔和简单实现
|
自然语言处理 Java 编译器
JAVA编程语言的编译原理以及执行过程
编译原理过程的介绍,以及JAVA编程语言的编译和执行过程
161 0
|
自然语言处理 前端开发 JavaScript
重学前端 28 # 通过四则运算的解释器快速理解编译原理
重学前端 28 # 通过四则运算的解释器快速理解编译原理
94 0
|
Java
java学习第四天笔记-流程控制语句-分支结构73-switch的扩展知识点
java学习第四天笔记-流程控制语句-分支结构73-switch的扩展知识点
83 0
java学习第四天笔记-流程控制语句-分支结构73-switch的扩展知识点

热门文章

最新文章