函数式编程是什么?

863 阅读4分钟

这是一篇学习笔记,原文是 Master the JavaScript Interview: What is Functional Programming?

定义

函数式编程(Functional Programming)是一种编程范式(programming paradigm),也就是基于一些原则来编程。面向对象编程(Object Oriented Programming)和过程编程(Procedure Programming)也都是编程范式。

函数式编程的主要原则如下:

  • 使用纯函数(pure functions)
  • 函数组合(function composition)
  • 避免共享状态(shared states)
  • 避免可变数据(mutable data)
  • 避免副作用(side-effects)
  • 使用声明式(declarative)而不是命令式(imperative)

原则

1. 纯函数

纯函数需要满足:

  • 参数相同时,返回值一定相同

    引用透明(reference transparency):函数的运行不依赖外部变量或者状态,只依赖输入的参数。只要参数相同,函数返回值总是相同的。

  • 没有副作用

    副作用:函数内部与外部互动,产生运算以外的其他结果。比如修改全局变量的值时。

比如:

var arr = [1,2,3,4,5];
// Array.slice 是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
arr.slice(0,3);

// Array.splice 就不是纯函数, 返回值不固定。
arr.splice(0,3); // => [1,2,3]
arr.splice(0,3); // => [4, 5]

2. 函数组合

函数组合就是把两个或多个函数组合起来,从而生成一个新的函数或者执行计算。

比如将 fg 两个函数组合成 f(g(x))

var compose = (f, g) => (x => f(g(x)))
var add1 = x => x + 1
var mul5 = x => x * 5
compose(mul5, add1)(2) // 15

3. 避免共享状态

共享状态(shared state),是指任何在共享作用域(比如全局作用域,闭包作用域)中,或是在作用域之间作为对象属性传递的变量、对象或存储空间。

当避免了共享状态,函数的执行的时机和顺序就不会影响结果。

const x = {
  val: 2
};
// 使用 Object.assign 把 x 的属性拷贝到一个空对象中,避免直接对 x 的修改
const x1 = x => Object.assign({}, x, { val: x.val + 1});
const x2 = x => Object.assign({}, x, { val: x.val * 2});
console.log(x1(x2(x)).val); // 5

const y = {
  val: 2
};

// 函数可以以任意顺序和时机调用,不会改变其他函数的返回结果
x2(y);
x1(y);

console.log(x1(x2(y)).val); // 5

4. 避免可变数据

一个不可变(immutable)对象在被创建之后就不会被修改。不可变性是函数式编程的一个中心概念。因为没有不可变性的特点,数据流是不完整的,可能导致 bug。

注意 const 声明的对象虽然不能重新赋值,但是可以修改对象的属性,所以是可变的。

5. 避免副作用

副作用(side effects)是任何除了函数返回值以外,在函数外部可见的状态改变。比如:

  • 修改外部变量,或对象的属性(全局变量,或者作用域链上父函数的变量)
  • 输出到控制台的日志
  • 写到屏幕上
  • 写到文件中
  • 上传到网络
  • 触发外部进程
  • 调用其他带副作用的函数

避免副作用使得代码更容易理解,并且更容易测试。函数式编程中,需要尽量把副作用行为跟其他的逻辑分开,这样使得程序更容易扩展,重构,调试,测试和维护。这也是为什么现在很多前端框架鼓励用户在独立的,低耦合的模块中管理状态和渲染组件。

6. 声明式 vs 命令式

函数式编程是一个声明式的范式。

命令式(imperative)会描述实现某些目标而进行的操作步骤,也就是描述了 flow control,即怎么做。

声明式(declarative)描述的是数据流(data flow),即做什么。

比如为了实现将数组中所有数值翻倍,命令式写法描述详细步骤:

const doubleMap = numbers => {
  const doubled = [];
  for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
  }
  return doubled;
};

声明式写法,使用一个表达式更清晰简介地描述了数据流:

const doubleMap = numbers => numbers.map(n => n * 2);

命令式经常使用语句(statement),比如 forifswitch 等,都是表示要执行什么操作。

而声明式经常使用表达式(expression),通常组合了函数调用,数值,操作符等,为了求出一个值。比如 Math.max(4, 3, 2)

延伸阅读

Curry and Function Composition

JavaScript函数式编程(一)