介绍:
函数式编程(FP)是一种编程范式,这意味着一种基于一些原则来思考软件构建的方法,比如 纯函数、不可变性、一等与高阶函数、函数组合、闭包、声明式编程、递归、引用透明性、柯里化 和 部分应用。
当这些原则有效地应用到 JavaScript 中时,可以使得代码更加模块化、可维护、健壮、易于理解、可测试,并且能够优雅地处理复杂的问题。
这篇文章看起来可能有点长,但不会那么理论化。
让我们开始逐一实验吧:
1. 纯函数:
两条规则:
- 给定相同的输入,总是返回相同的结果。
- 不产生副作用。
用处: 容易重构,使代码更具灵活性和适应性。
例子 1:
// 不纯的函数。
let a = 4;
const multiplyNumbers = (b) => a *= b;
multiplyNumbers(3);
console.log(a); // 第一次调用:12
> 12
multiplyNumbers(3);
console.log(a); // 第二次调用:36
> 36
// 修改了外部变量,所以不是纯函数。
// 纯函数版本。
const multiplyNumbers = (x,y) => x * y;
multiplyNumbers(2, 3);
> 6
例子 2:
// 不纯的函数。
addNumberarr = (arr, num) => {
arr.push(num);
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);
console.log(testArr);
> [1, 2, 3, 4]
// 修改了输入数组,所以不是纯函数。
// 上面的纯函数版本。
addNumberarr = (arr, num) => {
return [...arr, num];
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);
> [1, 2, 3, 4]
JS 内置的纯函数:
arr.reduce()
arr.map()
arr.filter()
arr.concat()
arr.slice()
arr.each()
arr.every()
... - 扩展语法
JS 内置的非纯函数:
arr.splice()
arr.push()
arr.sort()
Math.random()
2. 不可变性:
一旦创建就不能改变状态的对象。
一个简单的例子就是使用 slice 方法来帮助你轻松理解它的含义。
const arr = [1,2,3,4];
const slicedArray = arr.slice(1,2);
slicedArray
> [2]
arr
> [1, 2, 3, 4]
如果你看上面的例子,slice 并没有改变原始数组 arr。而下面的例子则不同:
const arr = [1,2,3,4];
arr.push(5);
> 5
arr
> [1, 2, 3, 4, 5]
原始数组 arr 被修改了。这并不是说我们不应该使用 push,但是在大多数情况下我们可以避免这种情况。一个简单的例子是:
const arr = [1,2,3,4];
const newArr = [...arr, 5];
arr
> [1, 2, 3, 4]
newArr
> [1, 2, 3, 4, 5]
上面的所有都是简单例子,可能不会造成任何问题。但是,如果我们在整个文件中尽可能多地修改同一个对象,就会带来许多问题。因为我们需要跟踪这个对象被修改了多少次以及以何种方式被修改。
为了解决这个问题,我们需要避免修改对象。
3. 一等函数
一等函数是指把函数当作一等公民的概念,意味着它们被视为常规变量或值。这让函数可以像字符串或数字等其他数据类型一样被操作和使用。这允许函数作为参数传递给其他函数,从其他函数返回值,以及被赋值给变量。JavaScript 支持这一点。
它打开了强大的编程技术的大门,比如高阶函数、函数组合,以及抽象的创建。
4. 高阶函数:
一个函数可以接受另一个函数作为参数或者返回一个函数作为结果,这样的函数被称为高阶函数。
- 返回一个函数的函数
const higherOrderFunc = function() {
return function() {
return 12;
}
}
// 返回下面的函数,所以它是高阶函数。
higherOrderFunc();
> ƒ () {
return 12;
}
higherOrderFunc()();
> 12
- 接受一个函数作为参数的函数
const testFunc = function(x) {
return x + 12;
}
// 接受函数作为参数。
const higherOrderFunc = function(testFunc) {
return testFunc(8);
}
higherOrderFunc(testFunc);
> 20
例子 1:
function calculate(operation, numbers) {
return operation(numbers);
}
function addition(numbers) {
let sum = 0;
for (const number of numbers) {
sum+=number;
}
return sum;
}
function multiply(numbers) {
let sum = 1;
for (const number of numbers) {
sum*=number;
}
return sum;
}
const numbers = [1,2,3,4,5];
console.log(calculate(addition, numbers));
> 15
console.log(calculate(multiply, numbers));
> 120
// calculate(multiply, numbers) - 传递函数作为参数时不加括号。
高阶函数的好处:
减少代码重复
单一职责
在 JavaScript 中,函数可以接受原始类型或对象作为参数并返回相同类型,称为一阶函数。
JS 内置的高阶函数有:
arr.reduce(), arr.forEach(), arr.filter(), arr.map()
5. 函数组合:
这是一种方法,其中将一个函数的结果传给下一个函数。
const add = (x, y) => x+y;
const subtract = (x) => x-4;
const multiply = (x) => x * 8;
// add 的结果传给 subtract,其结果再传给 multiply。
const result = multiply(subtract(add(2, 3)));
result;
> 8
看起来很清晰,但如果我们要一个接一个地调用更多函数会怎么样呢?让我们试试更干净的方法。
const compose = (...functions) => x => functions.reduceRight((total, f) => f(total), x);
const add = x => x+2;
const subtract = x => x-1;
const multiply = x => x * 8;
compose(multiply, subtract, add)(2);
> 24
我们也可以使用 reduce 来实现:
const pipe = (...functions) => x => functions.reduce((total, f) => f(total), x);
const add = x => x+2;
const subtract = x => x-1;
const multiply = x => x * 8;
pipe(add, subtract, multiply)(2);
> 24
pipe – 从左到右执行。
compose – 从右到左执行。
6. 声明式编程:
声明式: 告诉 做什么
命令式: 告诉 怎么做
例子: 找出部门为 ‘justCode’ 的员工及其工资总和。
命令式风格:
const employees = [
{id: 1, name: 'james', dept: 'admin', salary: 10000},
{id: 1, name: 'Tom', dept: 'finance', salary: 10000},
{id: 1, name: 'peter', dept: 'justCode', salary: 12500},
{id: 1, name: 'tunner', dept: 'justCode', salary: 14500},
];
const justCodeDept = [];
// 根据部门名称筛选员工。
for (let i=0; i<employees.length; i++) {
if (employees[i].dept === 'justCode') {
justCodeDept.push(employees[i]);
}
}
// 计算 justCodeDept 员工的工资总和。
let summation = 0;
for (j = 0; j<justCodeDept.length; j++) {
summation = summation + justCodeDept[j].salary;
}
console.log(summation);
声明式风格:
const employees = [
{id: 1, name: 'james', dept: 'admin', salary: 10000},
{id: 1, name: 'Tom', dept: 'finance', salary: 10000},
{id: 1, name: 'peter', dept: 'justCode', salary: 12500},
{id: 1, name: 'tunner', dept: 'justCode', salary: 14500},
];
console.log(employees.filter(item => item.dept === 'justCode').reduce(((previousValue, currentValue) => previousValue += currentValue.salary), 0));
7. 柯里化:
将接收多个参数的函数拆分成一系列函数,每个函数只接收单个参数。
例子 1:
通常我们写:
function addition(x, y, z) {
return x + y + z;
}
addition(1, 2, 3);
> 6
柯里化版本:
function addition(x) {
return function addY(y) {
return function addZ(z) {
return x + y + z;
}
}
}
addition(1)(2)(3);
> 6
使用箭头函数:
addition = (x) => (y) => (z) => x + y + z;
addition(1)(2)(3);
> 6
例子 2:
function formWelcomNote(name) {
name = `Hello ${name}, `;
return function(location) {
location = `Welcome to ${location},`;
return function(section) {
return `${name}${location} Please visit ${section} section`
}
}
}
formWelcomNote('Yester')('VK Just Code Articles')('JS Articles');
> 'Hello Yester, Welcome to VK Just Code Articles, Please visit JS Articles section'
我们也可以这样写:
formWelcomNote = (name) => {
name = `Hello ${name}, `;
return (location) => {
location = `Welcome to ${location},`;
return (section) => {
return `${name}${location} Please visit ${section} section`
}
}
}
formWelcomNote('Yester')('VK Just Code Articles')('JS Articles');
> 'Hello Yester, Welcome to VK Just Code Articles, Please visit JS Articles section'
例子 3:
function calculation(fn) {
switch (fn) {
case 'add': return (a, b) => a + b;
case 'sub': return (a, b) => a - b;
case 'mul': return (a, b) => a * b;
case 'div': return (a, b) => a / b;
}
}
console.log(calculation('mul')(4, 2));
8. 部分应用:
你为一个函数固定一定数量的参数,并生成一个新的带有较少参数的函数。这个新函数可以在稍后的时间用剩下的参数来调用。部分应用有助于创建更加专业和可重用的函数。
例子:
function add(a, b) {
return a + b;
}
// 部分应用第一个参数
const add2 = add.bind(null, 2);
console.log(add2(5)); // 输出:7 (2 + 5)
console.log(add2(8)); // 输出:10 (2 + 8)
9. 引用透明性:
JavaScript 中的一个表达式可以用它的值来替代,这种特性叫做引用透明性。
const add = (x, y) => x + y;
const multiply = (x) => x * 4;
// add (3, 4) 可以被替换为 7 —— 引用透明性。
multiply(add(3, 4));
> 28
multiply(add(3, 4));
> 28
const arr = [];
const add = (x, y) => {
const addition = x + y;
arr.push(addition);
return addition;
}
const multiply = (x) => x * 4;
// 在这里,我们不能用 7 替换 add(3, 4),因为它会影响程序逻辑。
multiply(add(3, 4));
> 28
multiply(add(3, 4));
> 28
10. 闭包:
闭包让你可以从内部函数访问外部函数的作用域。
function outer() {
const name = 'test';
function inner() {
// 'name' 从外部函数可以访问到内部函数中
console.log(name);
}
inner();
}
outer();
> test
function outerAdd(x) {
return function(y) {
return x + y;
};
}
const outer12 = outerAdd(12); // x 为 12.
const outer14 = outerAdd(14); // x 为 14.
const outer12Result = outer12(12); // y 为 12.
console.log(outer12Result);
> 24
const outer14Result = outer14(14); // y 为 14.
console.log(outer14Result);
> 28
或者,你也可以像下面这样使用箭头函数:
outerAdd = x => y => x + y;
const outer12 = outerAdd(12);
const outer14 = outerAdd(14);
const outer12Result = outer12(12);
console.log(outer12Result);
> 24
const outer14Result = outer14(14);
console.log(outer14Result);
> 28
使用闭包的计数器示例:
function outer() {
let counter = 0;
return function inner() {
counter += 1;
return counter;
}
}
const out = outer();
console.log(out());
console.log(out());
console.log(out());
> 1
> 2
> 3
11. 递归:
递归是一种编程技巧,在其中函数通过自我调用来解决问题。
例子:
function factorial(n) {
if (n === 0 || n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
console.log(factorial(5)); // 输出:120 (5 * 4 * 3 * 2 * 1)
console.log(factorial(0)); // 输出:1 (按定义)
在这个例子中,factorial 函数计算给定数 n 的阶乘。它使用了 n === 0 和 n === 1 的基本情况,阶乘定义为 1。对于其它任何值的 n,函数递归地调用自身,并将结果乘以 n。
当你调用 factorial(5) 时,递归调用序列如下所示:
factorial(5)
-> 5 * factorial(4)
-> 4 * factorial(3)
-> 3 * factorial(2)
-> 2 * factorial(1)
-> 1
<- 2 * 1 = 2
<- 3 * 2 = 6
<- 4 * 6 = 24
<- 5 * 24 = 120
如果有任何概念上的例子需要补充,请随时评论。
希望通过今天这篇文章,让你对JS中的函数式编程有了更好的理解。并且可以在日常的开发过程中进行灵活应用,以提高开发效率。