在进入主题之前我们需要理清楚一个概念,就是像函数式编程和面向对象编程他们只是一个工具或者说是一种规范,他的目的是为了培养我们的编程思维。我们知道编程的本质其实就是用代码来指挥计算机去做一些事,但是当我们面临的问题比较复杂的时候其实我们大部分的时间就不是编写代码,而是协调,我们要去理解和协调不同的模块之间关系。
一、函数式编程
1、什么是函数式编程
函数式编程是一种编程范式,主要是利用函数把运算过程包装起来,我们想实现一个复杂的功能时,可以通过组合各种函数来计算结果,下面我们看一个例子,我们想对一个数组进行如下转换:
['book-red','pen-black','leaf-green']
//转换后的结果
[
{
name:Book,
color:Red
},
{
name:Pen,
color:Black
},
{
name:Leaf,
color:Green
}
]
复制代码
看到这个题要是按照之前做题的习惯看到这个问题就直接上手,在一个函数中将整个转换过程统统搞定,下面来试试:
const fun = (arr) => {
const res = [];
for (const val of arr) {
let [name, color] = val.split("-");
name = name[0].toUpperCase() + name.slice(1);
color = color[0].toUpperCase() + color.slice(1);
res.push({ name, color });
}
return res;
};
const res = fun(["book-red", "pen-black", "leaf-green"])
复制代码
这种编程方式也称为命令式编程
,但是这种写法也是有他的缺点,比如可读性不强、过多的声名中间变量等,尤其是当出现问题时很难去排查问题,这个笔者在刷题的过程中深有体会。
那如果我们该用函数式编程会是怎样的呢?函数式编程要求我们先把问题想明白再去写,下面我们一步步来:
- 首先我们需要一个函数去帮我们实现将字符串转换为对象。
const trasnformStringToObj = curry((keys,info) => {
const obj = {};
keys.forEach((key,index) => {
obj[key] = info[index]
});
return obj
})
复制代码
- info 里面是已经转换好的信息,例如 [Book, Red],所以我们需要一个将字符串开头转为大写的函数。
const upperCase = s => s[0].toUpperCase() + s.slice(1);
复制代码
- 在转换前我们需要对字符串进行拆分再组合起来,这些功能我们可以 hack 一下 map、split 的实现。
有了这些函数之后最终我们可以将它们组合起来,这里我们使用 compose
函数进行组合:
const upperCaseInfo = compose(trasnformStringToObj(['name','color']),map(upperCase), split('-'))
const getRes = map(upperCaseInfo);
const res = getRes(['book-red',...])
复制代码
从这里我们就能明显的看出函数式编程的实现过程,他的注重点是函数而不是过程,当我们的功能比较多的时候可以将功能进行拆分,这也有利于代码的复用。
2、 函数式编程的特点
- 函数是一等公民
在 JavaScript 中函数被认为是一等公民,也就是说函数和其他数据类型一样处于平等的地位,可以作为变量赋值给其他变量、可以作为参数进行传递、可以作为其他函数的返回值等,正是由于函数的特殊地位我们才能去实现函数式编程
。
- 声明式编程
正如我上面说的,用函数式编程去解决问题时,我们首先要把问题想明白再去写,这里想明白也就是需要我们提前去进行声明接下来我们需要做什么,而不是怎么去做,这种编程方式也被成为声明式编程
,在这种范式下,我们不需要关心函数的具体实现,这样做的好处就是代码的可读性强,我们只要关系自己的业务逻辑,不需要去关心函数的具体逻辑以及优化。
- 纯函数
纯函数他有三个特点,一是没有副作用
,二是引用透明
,三是数据不可变
,下面一一介绍。
没有副作用:
没有副作用就是指函数本身不依赖于且不会修改外部的数据,最常见的外部变量就是this,副作用不仅会降低程序整体的可读性,有时候还会带来意料之外、难以排查的错误,如下所示:
var count = 1;
const fun = () => {
count += Math.random();
return count + this.total;
}
复制代码
上面的代码有两个问题,一个是使用了外部 this 上的属性 total,这个属性时候在那里会被改变,什么时候会被改变我们都不能预知,其次是改变了外部的属性 count,这就可能会影响其他使用了 count 的地方。
引用透明
引用透明就是指输入相同的参数永远会得到相同的输出,简单的说函数的返回值只受输入的影响,比如下面这个例子:
const add = (a,b) => a + b;
复制代码
数据不可变
数据不可变主要是针对类型引用数据类型的入参,如果以一定要修改,最好的方式就是重新去生成一份数据,这里推荐使用 immer.js
这个工具。
3、函数式编程在 React 中的应用
- 纯函数
纯函数是怎么在 React 中体现的呢?我们先看一个例子:
const APP = ({ count }) => {
return <p>{`Count: ${count}`}</p>;
};
复制代码
只是一个简单的函数组件,他接受一个参数并返回一个 P 标签,这是不是很符合我们的引用透明
这项原则,给定相同输入就会有相同的输出,在 React 里面它遵循的是单向数据流,也就是说当我的数据从父组件流向子组件时,数据就会变成不可变的了,即子组件是不能够去改变传入的数据,这就很好的体现了纯函数的特性。
- 声明式编程
在 React 中我们只需要去声明我们的 UI,而对应的数据更新,DOM 树的比对构建以及渲染都是由 React 帮我们去完成的。
- composition
在函数式编程中,组合是通过组合或链接多个较小的函数来构建复杂函数的行为,在 React 中我们也可以通过使用他的 children
属性来构建复杂的组件,这使我们可以灵活地决定组件内部的内容并自定义内容以获得所需的输出,想深入了解这个属性可以看我这篇文章。
4、总结
下面来总结下函数式编程的优缺点:
- 代码简洁,易于理解
首先函数式编程使用的是声明式代码,因此特别易于理解,同时函数式编程将功能细分化能够有效的提高函数的复用率。
- 并发速度快
使用 immutable 数据管理方式可以避免掉很多情况的 lock,并发处理速度会很快。
- 出错率少
我们将每个功能拆分到具体的函数中,同时每个函数都保证是纯函数,这样就减少了出错的概率,同时排查问题的效率也提升了。
函数式编程其实也不是十全十美的,正因为他有如此多的优点所以他天生就有以下的缺陷:
- 性能消耗大
函数式编程相对于命令式编程的最大的缺点就是性能,那我们上面那个例子来说,我们将字符串的操作都放在一个函数中进行,这样我也就只需要一个执行上下文,而对于函数式编程,会因为我们过度的拆分功能,从而导致不停的在不同的上下文中进行切换,在性能上是一笔不小的开销。
- 资源占用大
在函数式编程中我们要求数据是不可变的,那也就是说我们每更新一次数据就需要创建一个新的数据去覆盖之前的,尤其是当我们要实时间旅行
时情况更严重。
二、面向对象编程
1、什么是面向对象编程
面向对象也是一种开发模型,它主要围绕着数据或者对象而不是功能和逻辑来组织代码,他让开发者的关注点由逻辑转为想要操纵的数据,他将数据和方法都包装在一个类中,这样有利于代码的重用和可扩展性。
2、面向对象编程的特点
- 封装
封装意味着所有的数据和方法都被封装在对象内,由开发者自己选择性的去公开哪些属性和方法,对于创建的实例来说他能访问的只能是这些公开的属性和方法,而对于其他对象来说是无权访问此类或者进行更改,封装这一特性为程序提供了更高的安全性。
- 继承
继承意味着代码的可重用性,子类和父类这两个概念就是很好的体现,子类拥有父类所有的属性和方法避免数据和方法的重复定义,同时也能够保持独特的层析结构,
- 多态
多态意味着设计对象以共享行为,使用继承子类可以用新的方法去覆盖父类共享的行为,多态性允许同一个方法执行两种不同的行为:覆盖和重载。
覆盖我们刚刚说了子类可以使用自己的方法去覆盖父类的,比如下面这个例子:
class Person {
play(){
console.log('Person')
}
}
class Student extends Person {
play(){
console.log('Student')
}
}
const student = new Student()
student.play() //student
复制代码
重载指的是方法可以拥有相同的名称,但是传递给方法的参数类型或者数量不同,我们在调用的时候会根据参数去执行对应的函数。
3、总结
优点
- 高效率开发
在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
- 易维护,结构清晰
采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
- 易扩展
由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
- 可重用
代码可以通过继承得以重用,这意味着在进行多人开发时我们不需要编写相同的代码
缺点
- 过度的对象化
我们知道子类去继承父类后可以拥有父类中定义的数据和方法,此时父类它必须是一个类,但是有时我仅仅只想复用一段代码,因为继承的关系我不得不去定义一个父类将它对象化,比如我们想要做一个 tabel 列表,因为不同的场景 tabel 组件所需要的的方法和数据有可能是不一样的,但是他们也有许多相同的数据和方法,比如 PageNo、PageSize、selectedRowKeys 等,这些完全是可以服复用的,我们不得不讲这些数据塞到一个类中。
- 状态的共享
在面向对象中状态的共享是经常发生的,因为在面向对象中我们定义的数据是可变的
,也就是说我定义了一个实例,这个实例的状态能够在不同的地方按照他们认为最合适的方式对其进行修改,反过来随着代码越来越复杂对程序的推理也就变得越来越复杂。
- 并发问题
面向对象中可变状态的混杂共享使得这种代码几乎不可能并行化。为了解决这个问题,人们发明了复杂的机制。线程锁定、互斥和许多其他机制已经被发明出来。当然,这种复杂的方法也有其自身的缺点——死锁、缺乏可组合性、调试多线程代码既困难又耗时。更别提使用这种并发机制会增加复杂性。
- 耗内存、性能低
这个问题也是面向对象先天性的问题,因为我们定义的的方法和数据都是封装在类中,首先这个类体积大,其次就是每个地方要使用这个类上的方法和数据都需要重新新建一个新的实例。