前端60s 一分钟了解编程范式

2,052 阅读7分钟

0、60s In Short

太长不看篇——60s简单总结

  • 编程范式是一种编程风格,可分为命令式、函数式(属于声明式)、面向对象等多种范式。
  • JS 是一种动态语言,它支持多种范式,具体选择哪种范式,根据业务需求和个人风格做选择。
  • 编程范式没有绝对的好坏之分,只有合适和不合适。
  • 简单来说下这几种范式的特点。
    • 命令式更符合自然逻辑,容易理解。
    • 函数式减少了临时变量,容易维护。
    • 面向对象更方便扩展,代码复用程度高。

1、编程范式是什么

编程范式是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。

如:命令式编程、函数式编程、面向对象编程等不同的编程范式。

前端领域的函数式编程的体现

最近函数式编程逐渐又火了起来,我们看看前端领域有什么函数式编程的影子吧 ​

  • ES6的箭头函数、map 、reduce 、filter
  • React 16.8 的Hook
  • Vue3.0的Composition API

2、命令式编程

命令式编程就是关注计算执行步骤,如何执行,注重过程。

大部分命令式编程语言都支持四种基本的语句

  1. 运算语句;
  2. 循环语句(for、while);
  3. 条件分支语句(if else、switch);
  4. 无条件分支语句(return、break、continue)。

举个例子

常见表格带表头分组的需求,需要单独给每个列表配置宽度,而表格数据是动态获取的,所以要根据接口返回的数据生成一定数量重复的固定宽度数组,用来配置表格宽度。

  • 使用命令式的写法
const WIDTHS = ['10px','20px','30px'], LENGTH = 3;
let arr = [];
for (let i = 0; i < LENGTH; i++) {
  arr = arr.concat(WIDTHS);
}
// ["10px", "20px", "30px", "10px", "20px", "30px", "10px", "20px", "30px"]
  • 使用函数式的写法
const WIDTHS = ['10px','20px','30px'], LENGTH = 3;
const arr = Array(LENGTH).fill('').reduce(acc => acc.concat(WIDTHS), []);
// ["10px", "20px", "30px", "10px", "20px", "30px", "10px", "20px", "30px"]

命令式编程的优缺点

  • 优点
    • 性能高,因为有引擎作优化
    • 容易理解,因为符合自然编程思路
  • 缺点
    • 产生大量临时变量
    • 代码可读性低,需要通读代码才知道具体做了什么

3、函数式编程(FP)

函数式编程,主要强调如何通过函数的组合变化来解决问题,关注结果。

函数式编程的特性

  • 函数是"第一等公民"
    • 函数可以像其他数据类型一样操作,如赋值给其他变量、作为函数的入参、作为函数的返回值。
  • 惰性计算
    • 只在需要的时候执行,不产生无意义的中间变量。
  • 没有"副作用"
    • 副作用指函数计算结果的过程中,系统状态的变化,或者函数内部和外部进行交互,产生其他影响。
    • 常见的副作用:更改全局变量、发送http请求、dom查询。
  • 引用透明性
    • 即如果提供同样的输入,那么函数总是返回同样的结果。
    • 完全不依赖外部状态的变化(无状态),如全局变量,this 指针,IO 操作等。
    • PS:没有副作用 + 无状态 又可以称为纯函数。
  • 柯里化(Currying)
    • 理解为“加工站”,接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。
    • 将一个多元函数转为一个单元函数,可以依次调用f(x,y,z) → f(x)(y)(z)
    • 举个例子:求两个数的平方和
// 原始版本
const squares = function(x, y) {
  return x * x + y * y;
}
// 柯里化版本
const currySquares = function(x) {
    return function(y) {
    	return x * x + y * y;
    }
}
console.log(squares(1,2));
console.log(currySquares(1)(2));
  • 函数组合(Compose)
    • 理解为“流水线”,将多个函数组合成一个新的函数,初始数据通过多个函数依次处理,最后整体输出。
    • 把复杂的逻辑拆分成一个个简单任务,最后组合起来完成任务,使得整个过程的数据流更明确、可控。
    • 为了保证每个加工站的输出刚好流入下个工作站的输入,必须是单元函数。如果是加工站是多元函数,就需要用到柯里化转为单元函数,再放到流水线上组合使用。
    • 两个函数组合示例: 注意compose (从右往左的组合顺序执行 )
const compose = (f, g) => x => f(g(x))

const f = x => x + 1;
const g = x => x * 2;
const fg = compose(f, g);
fg(1) // 3
  • 多个函数组合示例: 如果需要类似管道pipe(从左往右)的数据流,将reduce换成reduceRight即可。
const compose = (...fns) => {
  return fns.reduce((acc,cur) => {
    return (...args) => {
      return acc(cur(...args))
    }
  })
}
// 最后返回的函数先执行

const f = x => {
  console.log('f: ', x);
  return x + 1;
}

const g = x => {
  console.log('g: ', x);
  return x + 1;
}

const t = x => {
  console.log('t: ', x);
  return x + 1;
}

compose(f, g, t)(1);

// t:  1
// g:  2
// f:  3

函数式编程的优缺点

  • 优点
    • 代码简洁,开发快速
      • 函数复用率很高,减少重复,开发速度快
    • 维护方便
      • 减少状态变量的声明和维护
    • 更少的出错概率
      • 强调纯函数,没有副作用
  • 缺点
    • 性能不好,容易过度抽象包装
      • 比如 使用命令式就只需要 变量+命令式(一层循环),使用函数式,不使用外部变量(双重循环)。
      • 在 JS 这种非函数式语言中,函数式的方式比直接写语句指令慢(引擎会针对很多指令做特别优化),如纯循环就比原生map性能快几倍。
    • 内存容易占用过高
      • 为了实现对象状态的不可变,创建更多新对象。消耗更多内存空间,JS引擎进行垃圾回收有压力。

4、面向对象编程(OOP)

面向对象的三个特征

封装

  • 封装即隐藏对象的属性和实现细节,仅对外公开API接口。
  • 将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体。
class Person {
  constructor(name) {
      this.name = name;
  }
  work() {
      console.log(`${this.name} is working hard`);
  }
  static touch() {
      console.log("Touch the fish");   
  }
}
let boy = new Person("Joe");
boy.touch();   // boy.touch is not a function
boy.work();  // Joe is working hard
Person.touch();  // Touch the fish

继承

  • 继承就是子类可以继承父类,使得子类对象(实例)具有父类公有的属性和方法,不需要写重复的代码。
  • 子类继承父类后,子类就会拥有父类的属性和方法,同时子类还可以声明自己的属性和方法。
class Person {
  constructor(name) {
      this.name = name;
  }
  work() {
      console.log(`${this.name} is working hard`);
  }
  static touch() {
      console.log("Touch the fish");   
  }
}
class FatMan extends Person {
    constructor(name){
        super(name);
    }
    drink() {
      console.log("喂!三点几了,饮茶先!");
    }
}
let uncle = new FatMan("饮茶哥");
uncle.work(); // 饮茶哥 is working hard
uncle.drink(); // 喂!三点几了,饮茶先!
uncle.touch();    // uncle.touch is not a function

多态

  • 多态按字面的意思就是“多种状态”,允许将子类类型的指针赋值给父类类型的指针。即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
  • 多态的表现方式有重写,重载和接口,原生 JS 能够实现的多态只有重写。
  • 重写:重写是子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想直接地继承父类的方法,自己想做调整和改动,就要重写父类的方法。我们也可以称之为方法覆盖。
class Person {
  constructor(name) {
      this.name = name;
  }
  getName() {
    console.log(this.name);
  }
  work() {
      console.log(`${this.name} is working hard`);
  }
  static touch() {
      console.log("Touch the fish");   
  }
}
class FatMan extends Person {
    constructor(name){
        super(name);
    }
    work() {
    	console.log("做碌*啊做!");
    }
    drink() {
      console.log("喂!三点几了,饮茶先!");
    }
}
const boy = new Person('Joe');
const uncle = new FatMan('饮茶哥');
boy.getName(); // Joe
boy.work(); // Joe is working hard
uncle.getName(); // 饮茶哥
uncle.work(); // 做碌*啊做!

5、如何选择

  • 学习编程范式的意义是丰富你的编程思路,在面对特定场景,选择最合适的编程武器,灵活组合使用。
  • 命令式、函数式、面向对象本质上并没有优劣之分。我们可以在实际开发中,根据需求选择合适的编程范式。

参考文章

推荐