编程范式|青训营笔记

87 阅读17分钟

一、课程介绍

  • 课程背景

    • 前端的主要编程语言为JavaScript.
    • JavaScript做为一种融合了多种编程范式的语言,灵活性非常高。
    • 前端开发人员需要根据场景在不同编程范式间自如切换。
    • 进一步需要创造领域特定语言抽象业务问题。
  • 课程收益

    • 了解不同编程范式的起源和适用场景。
    • 掌握JavaScript在不同的编程范式特别是函数式编程范式的使用。
    • 掌握创建领域特定语言的相关工具和模式。

二、编程语言

1、机器语言

  • 计算机是不能直接识别我们所编写的C程序或者Java程序的。它只能识别机器语言。

  • 机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指指令系统令的集合。

image.png

  • 计算机语言的发展历程

image.png

2、汇编语言

  • 是一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。 在不同的设备中,汇编语言对应着不同的机器语言指令集。

  • 一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。

  • 汇编语言的主体是汇编指令,汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。

image.png

比如下面将寄存器 BX 的内容发送到 AX 上:

操作:寄存器BX的内容送到AX中

1000100111011000 机器指令

mov ax,bx 汇编指令

3、高级语言

  • 高级语言并不是指一种语言,而是包括很多编程语言,比如Java、C、C++、C#、python等等,是高度封装的编程语言。

高级语言的转换:

image.png

4、C/C++

(1)C语言

  • C 语言是一门通用 计算机编程语言 ,广泛应用于底层开发。 

  • C 语言的设计目标是提供一种能以简易的方式 编译 、处理低级 存储器 、产生少量的 机器码 以及不需要任何运行环境支持便能运行的编程语言。

  • C语言是一门面向过程的计算机编程语言。

image.png

image.png

(2)C++

  • C with Classes

    • C++最初是作为C语言的一种扩展,其基本语法与C语言相同,但增加了类、继承、多态等面向对象的特性,因此C++也被称为C with Classes。
  • 继承

    • 代码中的class Student : public Person语句定义了一个Student类,它继承自Person类,这说明C++支持继承的特性。
  • 权限控制

    • 代码中的publicprotectedprivate关键字用来控制成员变量和成员函数的访问权限,这说明C++支持权限控制的特性。
  • 虚函数

    • 代码中的virtual void sayHello()语句定义了一个虚函数,这说明C++支持虚函数的特性。虚函数可以实现多态,即在运行时根据对象的实际类型来调用相应的函数。
  • 多态

    • 代码中的void sayHello() override语句实现了函数的重写,这说明C++支持多态的特性。在运行时,如果调用的函数是虚函数,那么实际调用的函数将根据对象的实际类型来确定。

image.png

image.png

5、Lisp

  • Lisp是一种计算机编程语言,由约翰·麦卡锡(John McCarthy)于1958年发明。Lisp这个名称是“List Processing”的缩写。

特点:

  • 与机器无关
  • 列表:代码即数据
  • 闭包

image.png

6、JavaScript

基于原型和头等函数的多范式语言

  • 过程式

    • JavaScript最初被设计为一种过程式的脚本语言,它可以在Web浏览器中嵌入HTML页面,实现动态交互效果。
  • 面向对象

    • JavaScript是一种支持面向对象编程的语言,它支持类、对象、继承、封装等面向对象的特性。JavaScript中的对象是动态的,可以随时添加或删除属性和方法。
  • 函数式

    • JavaScript是一种支持函数式编程的语言,它的函数可以作为一等公民,可以赋值给变量,可以作为参数传递给其他函数,可以作为返回值返回给其他函数。
  • 响应式

    • JavaScript可以通过DOM操作实现响应式编程,可以实现页面元素的动态更新,与用户的交互效果等。

image.png

三、编程范式

  • 程序语言特性
    • 是否允许副作用
    • 操作的执行顺序
    • 代码组织
    • 状态管理
    • 语法和词法

1、编程范式

1、 命令式:命令式编程是一种以计算机执行的命令为中心的编程范式,它主要分为面向过程和面向对象两种形式。

(1)面向过程

  • 面向过程是一种以过程为中心的编程方式,它将问题分解为一系列步骤,通过函数的调用来实现程序的功能。面向过程的代码通常是一系列的命令,描述了计算机执行的具体步骤。

(2)面向对象

  • 面向对象是一种以对象为中心的编程方式,它将数据和函数封装在一起,通过对象的交互来实现程序的功能。面向对象的代码通常是一系列的对象,描述了程序中的实体和它们之间的关系。

2、 声明式:声明式编程是一种以描述问题为中心的编程范式,它主要分为函数式和响应式两种形式。

(1) 函数式

  • 函数式编程是一种以函数为中心的编程方式,它将计算视为函数的应用,通过函数的组合来实现程序的功能。函数式的代码通常是一系列的函数调用,描述了计算的过程。

(2) 响应式

  • 响应式编程是一种以数据流为中心的编程方式,它将数据和函数封装在一起,通过数据的变化来触发函数的执行,实现程序的功能。响应式的代码通常是一系列的数据流,描述了数据的变化和处理。

2、过程式编程

  • 自定向下

image.png

  • 结构化编程

image.png

3、JS面向过程

​
//数据
export let car = {
  meter:100,
  speed:10
};
​
//算法:函数可以看作面向过程中的算法
export function advanceCar(meter){
  while(car < meter){
    car.meter += car.speed;
  }
}
import { car , advanceCar } from ".car"//导入上方模块内容
​
function main(){
  console.log('before',car);
  
  advanceCar(1000)
  
  console.log('after',car)
}

面向过程式编程有什么缺点?为什么后面会出现面向对象?

  • 数据与算法关联弱

  • 不利于修改和扩充

    • 可维护性差:面向过程式编程缺乏封装性和抽象性,代码的耦合度高,修改代码时容易影响其他部分的代码,导致维护性差
  • 不利于代码重用

    • 可扩展性差:面向过程式编程很难对程序进行扩展,因为程序的逻辑分散在各个函数或过程中,很难进行整体性的扩展

面向对象的出现解决了这几个问题?

  • 可读性好:面向对象编程将数据和函数封装在一起,代码的可读性好,易于理解整个程序的逻辑。
  • 可维护性好:面向对象编程具有封装性和抽象性,代码的耦合度低,修改代码时只需要修改对象的内部实现,不会影响其他部分的代码,导致维护性好。
  • 可扩展性好:面向对象编程将数据和函数封装在一起,对象之间通过接口进行交互,易于对程序进行扩展。

4、面向对象

  • 封装
  • 继承
  • 多态
  • 依赖注入

5、封装

  • 封装是把彼此相关数据和操作包围起来,抽象成为一个对象,变量和函数就有了归属,想要访问对象的数据只能通过已定义的接口。

封装的使用:通过一个属性感受封装

    int age;
}
 
public static void main(String[] args){
 
    A a = new A();
    a.age = 500;
}

在这里设置一个A类,有一个年龄属性,但是在主函数里面给年龄设置值的时候,可以胡乱的设置一个值,几百几千的违背常理的设置都不会报错,这个时候就可以使用封装了。

    private int age;
 
    
    //提供一个返回年龄的方法
    public int getAge(){
        return age;
    }
 
    //提供一个处理年龄误差的方法
    public void setAge(int age){
        if(age<80){
            age=18;
        }else{
            this.age = age;
        }
    }
}
 
 
public static void main(String[] args){
 
    A a = new A;
    a.getAge(81);
    system.uot.println(a.setAge);
    

上面的代码,对于age属性来说,我加了private。这样外界对他的访问就受到了限制,现在我还想加上其他的限制条件,但是在属性本身上没有办法再加了,所以我们通过定义方法来进行条件的添加。

6、继承

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段。

继承定义格式:

image.png

  • 继承方式

    • 单继承
class A
 
{};
 
class B:public A
 
{};//B继承A,:可以理解为通过原本的类产生新得类,那新的类是新出来的嘛,所以就在前面,所以就是
派生类:基类
  • 多继承
class D
 
{};
 
class E
 
{};
 
class F :public D, public E
 
{};//多个继承之间用逗号连接,:同理,一般的代码继承的类不会太多,因为能少写一点就少写一点嘛,没必要嘛
  • 继承方式总结语法
class 派生类名:继承方式1 基类名1,继承方式2 基类名2.......
 
{
 
类成员声明;
 
};

7、多态

  • 多态是同一个行为具有多个不同表现形式或形态的能力。
  • 多态就是同一个接口,使用不同的实例而执行不同操作。
  • 多态要点:
    • 多态就是方法的多态,不是属性的多态与属性无关
    • 多态存在必须要有3个条件:继承,方法重写 父类引用指向子类对象
    • 父类引用指向子类对象后,父类引用调用子类重写的方法,这时多态就出现了

类型转换测试:

class Animal{
    public void shout(){
        System.out.println("叫");
    }
}
//创建一个子类Dog
class Dog extends Animal{
    public void shout(){
        System.out.println("旺");
    }
}
//再创建一个子类Cat
class Cat extends Animal{
    puyblic void shout(){
        System.out.println("喵");
    }
}
class TestPolym{
    public static void main(String[] args){
	//向上可以自动转,因为父类引用指向子类对象
        Animal a1 =new Dog(); 
        //传的什么类就是什么类
        animalCry(a1);
         Animal a2 =new Cat(); 
        animalCry(a2);
        
        //编写程序时,如果向调用运行时类型的方法,只能进行类型强转,否则通不过编译器的检查
        Dog dog = (Dog)a2;
        dog.seeDoor;
    }
    	//有了多态后,我们只需要让增加的这个类继承Animal类就可以了
    static void animalCry(Animal a){
        a.shout();
    }
    /*
    如果没有多态,那么我们这里会写很多重载的方法,没增加一种就会多重载一种方法,比较麻烦
    */
}

8、依赖注入

依赖注入(Dependency Injection),简称DI,类之间的依赖关系由容器来负责。简单来讲a依赖b,但a不创建(或销毁)b,仅使用b,b的创建(或销毁)交给容器。

例子: 小明要杀怪,那小明拿什么武器杀怪呢?可以用刀、也可以用拳头、斧子等。

首先,我们创建一个演员类,名字叫“小明”,具有杀怪功能。

namespace NoInjection.ConsoleApp
{
    public class Actor
    {
        private string name = "小明";
        public void Kill()
        {
            var knife = new Knife();
            knife.Kill(name);
        }
    }
}

然后,我们再创建一个武器-刀类,具有杀怪功能。

using System;
 
namespace NoInjection.ConsoleApp
{
    public class Knife
    {
        public void Kill(string name)
        {
            Console.WriteLine($"{name}用刀杀怪");
        }
    }
}

最后,我们客户端调用演员类,执行杀怪功能。

using System;
 
namespace NoInjection.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var actor = new Actor();
            actor.Kill();
 
            Console.ReadKey();
        }
    }
}

让我们来看看输出结果:

小明用刀杀怪

通过这个例子我们可以看到,Actor类依赖Knife类,在Actor中创建Knife,执行Knife.Kill方法。

9、面向对象编程_五大原则

  • 单一职责原则SRP(Single Responsibility Principle)

    • 一个类只负责一个功能领域中的相应职责,或者可以定义为一个类只有一个引起它变化的原因。这个原则的目的是将职责分离,提高类的内聚性,降低类的耦合性,使得代码更加灵活、可维护和可扩展
  • 开放封闭原则OCP(Open-Close Principle)

    • 一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这个原则的目的是使得代码更加灵活、可扩展和可维护,同时也能降低代码的风险和复杂度。通过使用抽象化和多态等技术,使得代码能够适应不同的需求和变化
  • 里式替换原则LSP(the Liskov Substitution Principle LSP)

    • 所有引用基类(父类)的地方必须能透明地使用其子类的对象。这个原则的目的是保证代码的正确性和可靠性,避免在子类中破坏父类的行为和逻辑。通过遵循这个原则,可以使得代码更加灵活、可扩展和可维护
  • 依赖倒置原则DIP(the Dependency Inversion Principle DIP)

    • 高层模块不应该依赖于底层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。这个原则的目的是降低代码的耦合性,提高代码的灵活性和可扩展性。通过使用接口和抽象类等技术,使得代码能够适应不同的需求和变化
  • 接口分离原则ISP(the Interface Segregation Principle ISP)

    • 一个类不应该依赖于它不需要的接口,一个类应该只依赖于它需要的接口。这个原则的目的是降低代码的耦合性,提高代码的灵活性和可扩展性。通过将接口进行分离,使得代码更加灵活、可维护和可扩展

面向对象编程有什么缺点?为什么我们推荐函数式编程?

image.png

10、函数式编程

  • 函数的特点

    • 函数是"一等公民"
    • 纯函数/无副作用
    • 高阶函数跟闭包

image.png

  • 优势
    • 可缓存
    • 可移植
    • 可测试
    • 可推理
    • 可并行

image.png

11、Currying

柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

柯里化不会调用函数。它只是对函数进行转换。

让我们先来看一个例子,以更好地理解currying。

我们将创建一个辅助函数 curry(f),该函数将对两个参数的函数 f 执行柯里化。换句话说,对于两个参数的函数 f(a, b) 执行 curry(f) 会将其转换为以 f(a)(b) 形式运行的函数

function curry(f) { // curry(f) 执行柯里化转换
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// 用法
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

正如你所看到的,实现非常简单:只有两个包装器(wrapper)。

curry(func) 的结果就是一个包装器 function(a)。 当它被像 curriedSum(1) 这样调用时,它的参数会被保存在词法环境中,然后返回一个新的包装器 function(b)。 然后这个包装器被以 2 为参数调用,并且,它将该调用传递给原始的 sum 函数。

总结:柯里化 是一种转换,将 f(a,b,c) 转换为可以被以 f(a)(b)(c) 的形式进行调用。JavaScript 实现通常都保持该函数可以被正常调用,并且如果参数数量不足,则返回偏函数。柯里化让我们能够更容易地获取偏函数。

12、组合函数

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:

image.png

13、容器式编程

  • 可以当做容器的类型,类型支持对容器内元素进行操作
  • 常见的:functor:Array(Iterable).map,Promise.then

image.png

14、响应式编程

  • 响应式编程是一种新的编程技术,其目的是构建响应式系统。
    • 通俗来说,响应式编程就是一种处理数据流的编程方式。我们可以把数据流看成一条河流,数据就像是水流一样从上游流向下游。在响应式编程中,我们可以方便地定义这条河流,并在河流中处理数据的变化,就像是在河流中处理水流一样。这样,我们就可以很方便地处理数据的变化,而不需要手动追踪和处理每一个数据变化的位置。

没有纯粹的响应式编程语言,我们需要借助工具库的帮忙,例如RxJS。

  • 异步/离散的函数式编程

    • 数据流

    • 操作符

      • 过滤
      • 合并
      • 转化
      • 高阶

15、观察者模式

观察者模式(Observer Pattern)是一种设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有观察者都会收到通知并自动更新。

在观察者模式中,有两个核心角色:主题对象和观察者对象。主题对象维护一个观察者列表,并提供添加、删除和通知观察者的方法;观察者对象则定义了接收通知并进行更新的方法。

观察者模式的优点包括:

  1. 松耦合:观察者模式将主题对象和观察者对象之间解耦,使得它们可以独立地变化和扩展。
  2. 可复用性:由于观察者对象可以动态地添加和删除,因此可以在不修改主题对象的情况下增加新的观察者对象,提高了代码的可复用性。
  3. 扩展性:在观察者模式中,可以灵活地添加和删除观察者对象,因此可以方便地扩展和修改系统的功能。

image.png

16、迭代器模式

迭代器模式(Iterator Pattern)是一种设计模式,它提供了一种顺序访问聚合对象中的元素,而不需要暴露聚合对象的内部表示。迭代器模式可以将遍历聚合对象的过程从聚合对象中分离出来,从而可以简化聚合对象的实现和遍历算法的实现。

在迭代器模式中,有两个核心角色:聚合对象和迭代器对象。聚合对象是一组对象的集合,它提供了一个方法来获取迭代器对象;迭代器对象则定义了访问和遍历聚合对象中元素的方法。

迭代器模式的优点包括:

  1. 简化聚合对象的实现:由于迭代器模式将遍历聚合对象的过程从聚合对象中分离出来,因此可以简化聚合对象的实现,使其只需要关注自己的核心业务逻辑。
  2. 提高聚合对象的访问效率:在迭代器模式中,迭代器对象可以提供不同的遍历算法,从而可以针对不同的应用场景进行优化,提高聚合对象的访问效率。
  3. 提高代码的可复用性:由于迭代器模式将遍历算法从聚合对象中分离出来,因此可以方便地重用遍历算法,提高代码的可复用性。

image.png

17、响应式编程的"compose"

  • 合并
  • 过滤
  • 转化
  • 异常处理
  • 多播

image.png

image.png

去除嵌套的Observable:

image.png

四、领域特定语言

  • 领域特定语言是在特定领域下用于特定上下文的语言

从使用方式的角度,语言可以划分出以下两类:

(1)DSL:使用 DSL 形式编写或表示的语言。

(2)宿主语言 host language :用于执行或处理 DSL 的语言。

语言运行:

image.png

1、SQL Token分类

  • 注释
  • 关键字
  • 操作符
  • 空格
  • 字符串
  • 变量

2、Lexer

image.png

3、上下文无关语法规则

image.png

  • 推导式:表示非终结符到(非终结符或终结符)的关系。

  • 终结符:构成句子的实际内容。可以简单理解为词法分析中的token。

  • 非终结符:符号或变量的有限集合。它们表示在句子中不同类型的短语或子句。

4、Parser_LL

LL:从左到右检查,从左到右构建语法树

image.png

5、Parser_LR

LR:从左到右检查,从右到左构建语法树

LL(K) > LR(1) > LL(1),括号里的内容构建语法树需要向下看的数量.

image.png

6、tools

image.png

7、解析与编译过程

image.png

image.png

五、课程总结

image.png