Skip to main content

函数式编程

函数式编程是一种编程范式,强调函数的使用和函数之间的组合。

函数是一等公民

函数可以作为函数的参数,也可以作为返回值

纯函数

纯函数特性:

  • 相同的输入值,总是返回相同的输出值
  • 没有任何副作用

纯函数示例:

const add = (a, b) => a + b;

副作用是指函数对外部状态的影响,如修改全局变量、修改参数值、修改 DOM 等。

非纯函数示例:

let obj = { a: 1 };
function fn(p) {
p.a = 2;
}
fn(obj);
console.log(obj); // { a: 2 }

不可变性

数据一旦创建就不能被改变。任何修改数据的操作都会返回一个新的数据副本。

const arr = [1, 2, 3];
const newArr = [...arr, 4];

例子中使用扩展运算符创建新数组,不会改变原始数组。

高阶函数

高阶函数是指可以接收函数作为参数,或者返回函数的函数。

const map = (arr, fn) => arr.map(fn);
const double = x => x * 2;

const res = map([1, 2, 3], double); // [2, 4, 6]

函数组合

将多个函数组合成一个函数,前一个函数的输出作为后一个函数的输入。

const compose = (f, g) => x => f(g(x));

const add = x => x + 1;
const double = x => x * 2;
const res = compose(double, add)(1); // (2 + 1) * 2 = 6

函数柯里化

柯里化(Currying)是把一个多参数的函数,转变为一系列单参数函数的过程。

function f(x, y) {
return x + y;
}
f(1, 2);

function g(x) {
return function (y) {
return x + y;
};
}
g(1)(2);

示例 1:累加计数,实现 add(1)(2)(3)add(1, 2)(3)add(1)(2, 3)add(1, 2, 3)的结果都是 6

ES5 实现:

function curry1(fn) {
return function curried() {
var args = Array.prototype.slice.call(arguments);
if (args.length < fn.length) {
return function () {
var args2 = Array.prototype.slice.call(arguments);
return curried.apply(null, args.concat(args2));
};
}
return fn.apply(null, args);
};
}

ES6 实现:

function curry(fn) {
return function curried(...args) {
if (args.length < fn.length) {
return function () {
return curried(...args.concat(Array.from(arguments)));
};
}
return fn(...args);
};
}

使用方式:

function add(a, b, c) {
return a + b + c;
}

const curriedAdd = curry(add);

curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
curriedAdd(1, 2, 3); // 6

示例 2:实现不限层级的累加,如 add(1)(2)(3)的结果是 6

初步实现:

function add(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
add(1)(2)(3);

这种方式只能实现有限的层级,并不能无限嵌套。下面使用闭包,并重写函数的 toString() 方法来实现任意数累加

function add(a) {
function sum(b) {
a = a + b;
return sum;
}
sum.toString = function () {
return a;
};
return sum;
}

const res = add(1)(2)(3).toString();

这里的调用链实际上是在执行多次 sum 函数,每次将参数累加到 a 上。最终的返回值是一个函数,该函数的 toString 方法返回累加后的结果。

函数缓存

函数缓存,就是将函数运行过的结果进行临时缓存,用空间换时间。实现函数缓存主要依靠闭包、高阶函数、柯里化。

实现原理:把参数和对应的结果数据存在一个对象中,调用时判断参数对应的结果是否存在,存在则直接返回结果,不存在则计算结果并保存到对象中,再返回结果。

function memoize(fn, context) {
let cache = {};
context = context || this;
return (...key) => {
if (!cache[key]) {
cache[key] = fn.apply(context, key);
}
return cache[key];
};
}

const add = (a, b) => {
console.log('add被调用了');
return a + b;
};
const calc = memoize(add);
const n1 = calc(1, 2);
const n2 = calc(1, 2);
console.log(n1, n2);

结果显示 add 函数被调用了 1 次,n2 就是缓存的结果。

应用场景:

  1. 执行复杂计算的函数
  2. 具有重复输入值的递归函数

函数式编程的优点

  1. 可预测性
    1. 纯函数总是产生相同的输出,使得代码更可预测、更容易调试。
  2. 可测试性
    1. 纯函数和无副作用使得单元测试更容易编写和维护。
  3. 可组合性
    1. 函数组合和高阶函数使得代码更模块化、更易于复用。
  4. 简洁性
    1. 函数式编程的风格通常更加简洁和清晰,使得代码更易读。