Skip to content
On this page

函数式

声明式编程

函数式编程属于声明式编程范式,这种范式会描述一系列操作,但是不会暴露他们是如何实现的或是数据流如何穿过他们。相对于声明式编程,更加主流的是命令式编程,命令式编程视计算机程序为从上而下的断言,并通过修改系统的各个状态来计算最终的结果。

这是一个命令式编程的示例:

js
let arr = [1, 2, 3];
for (let i = 0;i < arr.length;i++) {
    arr[i] = Math.pow(arr[i], 2)
}
arr // 1, 4, 9

命令式编程具体地告诉计算机如何计算某个任务,这是编写代码的常见方式。

而声明式编程是将程序的描述和求值分离开的,它关注如何使用各种表达式描述程序,而不一定要指明其控制流或状态的变化。

我们用一个声明式编程解决一个相同的问题:

js
[1, 2, 3].map(num => Math.pow(num, 2)) // 1, 4, 9

函数式的代码让开发者免于考虑如何管理循环计数器和数组索引访问的问题,标准的循环是很难重用的东西,除非把他们抽象为函数。ES6 提供了 map 这样的函数来把循环抽象为函数,结合 lambda 表达式能漂亮的减少代码的书写。循环意味着响应新的迭代,代码会不断变化,而函数式编程则尽可能的提高代码的无状态性和不变性。

纯函数

函数式编程基于一个前提,即使用纯函数构建具有不变性的程序。纯函数具有以下性质:

  1. 仅取决于提供的输入,而不依赖于任何在函数求值期间或调用间隔时可能变化的隐藏状态和外部状态
  2. 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数

因此,哪怕是如此简单的函数也是不纯的:

js
let count = 0;
const increment = () => count++

事实上,纯函数就是很难使用的,但是函数式编程实践上并不限制一切状态的改变,它只是提供了一个框架来帮助管理和减少可变状态,将纯函数从不纯的部分中分离出来。

比如这个例子:

js
const showName = (sid) => {
    let student = db.get(sid)
    document.getElementByID("student").innnerHTML=`${student.name}, ${student.sid}`
}

showName("111")

确实也还行,但是不够灵活,也不便调试,我们目前能做到两点:

  1. 将长函数分离成多个具有单一原则的短函数
  2. 将功能所需的依赖都定义为函数参数来减少副作用的数量
js
const getStudent = (sid) = db.get(sid);
const template = (student) => `${student.name}, ${student.sid}`;
const writeInfo = (elementID, info) => {
    document.querySelector(elementID).innerHTML = info
};
const showStudent = (sid, template, elementID) => {
    writeInfo(elementID, template(getStudent(sid)))
}

showStudent("111", template, "#student")

虽然代码量似乎增加了,但是它已经比先前灵活了许多,可重用性也增强了。

引用透明

引用透明是定义一个纯函数较为正确的方式,如果一个函数对于一个相同的输入总能产生相同的结果,那么就说它是引用透明的。引用透明的函数不仅让代码更易于测试,还让我们能更容易推理整个程序。

总结

函数式带来的好处:

  1. 任务分解成简单的函数
  2. 使用流式的调用链来处理数据
  3. 通过响应式范式降低事件驱动代码的复杂性

OOP 和 FP 的重要性质比较

函数式面向对象
组合单元函数对象
编程风格命令式命令式
数据和行为独立且松耦合的纯函数与方法紧密耦合的类
状态管理将对象视为不可变的值主张通过实例方法改变对象
程序流控制函数与递归循环与条件
线程安全可并发编程难以实现
封装性因为一切都是不可变的,所以没有必要需要保护数据的完整性

虽然两种范式存在差异,但是有效构建应用程序的方法是混合两种范式,一方面使用组成类型之间存在自然关系的富领域模型,另一方面应用于类型之上的纯函数。

对象的深冻结

JavaScript 并没有官方提供一个能满足函数式编程的冻结方法,直接用 Object.freeze(),对象的子对象依然可以更改,面向对象的封装则完全不够格。

所以我们用递归函数实现

js
// 因为 null 也是 object,所以前面还要判断 val 是不是 falsy
const isObject = (val) => val && typeof val === 'object'

function deepFreeze(obj) {
    // 跳过不是对象和已经冻结的对象
    if (isObject(obj) && !Object.isFrozen(obj)) {
        Object.keys(obj).forEach( name => deepFreeze(obj[name]) )
    }
    return obj
}

函数式基础

命令式代码缺点是限定于高效解决某个特定问题,因此比起函数式代码,其抽象水平要低很多。抽象层次越低,代码重用概率就会越低,出现错误的复杂性和可能性就越大。

Released under the MIT License.