【Javascript修炼篇】JS中的函数式编程

2024.9.20 Javascript 15

介绍:

函数式编程(FP)是一种编程范式,这意味着一种基于一些原则来思考软件构建的方法,比如 纯函数、不可变性、一等与高阶函数、函数组合、闭包、声明式编程、递归、引用透明性、柯里化 和 部分应用

当这些原则有效地应用到 JavaScript 中时,可以使得代码更加模块化、可维护、健壮、易于理解、可测试,并且能够优雅地处理复杂的问题。

这篇文章看起来可能有点长,但不会那么理论化。

让我们开始逐一实验吧:

1. 纯函数:

两条规则:

  1. 给定相同的输入,总是返回相同的结果。
  2. 不产生副作用。

用处: 容易重构,使代码更具灵活性和适应性。

例子 1:

JavaScript
// 不纯的函数。
let a = 4;
const multiplyNumbers = (b) => a *= b;

multiplyNumbers(3);
console.log(a); // 第一次调用:12
> 12
multiplyNumbers(3);
console.log(a); // 第二次调用:36
> 36

// 修改了外部变量,所以不是纯函数。
JavaScript
// 纯函数版本。
const multiplyNumbers = (x,y) => x * y;

multiplyNumbers(2, 3);
> 6

例子 2:

JavaScript
// 不纯的函数。
addNumberarr = (arr, num) => {
arr.push(num);
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);

console.log(testArr);
> [1, 2, 3, 4]

// 修改了输入数组,所以不是纯函数。
JavaScript
// 上面的纯函数版本。
addNumberarr = (arr, num) => {
return [...arr, num];
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);
> [1, 2, 3, 4]

JS 内置的纯函数:

JavaScript
arr.reduce()
arr.map()
arr.filter()
arr.concat()
arr.slice()
arr.each()
arr.every()
... - 扩展语法

JS 内置的非纯函数:

JavaScript
arr.splice()
arr.push()
arr.sort()
Math.random()

2. 不可变性:

一旦创建就不能改变状态的对象。

一个简单的例子就是使用 slice 方法来帮助你轻松理解它的含义。

JavaScript
const arr = [1,2,3,4];

const slicedArray = arr.slice(1,2);

slicedArray
> [2]

arr
> [1, 2, 3, 4]

如果你看上面的例子,slice 并没有改变原始数组 arr。而下面的例子则不同:

JavaScript
const arr = [1,2,3,4];

arr.push(5);
> 5

arr
> [1, 2, 3, 4, 5]

原始数组 arr 被修改了。这并不是说我们不应该使用 push,但是在大多数情况下我们可以避免这种情况。一个简单的例子是:

JavaScript
const arr = [1,2,3,4];

const newArr = [...arr, 5];

arr
> [1, 2, 3, 4]

newArr
> [1, 2, 3, 4, 5]

上面的所有都是简单例子,可能不会造成任何问题。但是,如果我们在整个文件中尽可能多地修改同一个对象,就会带来许多问题。因为我们需要跟踪这个对象被修改了多少次以及以何种方式被修改。

为了解决这个问题,我们需要避免修改对象。

3. 一等函数

一等函数是指把函数当作一等公民的概念,意味着它们被视为常规变量或值。这让函数可以像字符串或数字等其他数据类型一样被操作和使用。这允许函数作为参数传递给其他函数,从其他函数返回值,以及被赋值给变量。JavaScript 支持这一点。

它打开了强大的编程技术的大门,比如高阶函数、函数组合,以及抽象的创建。

4. 高阶函数:

一个函数可以接受另一个函数作为参数或者返回一个函数作为结果,这样的函数被称为高阶函数。

  1. 返回一个函数的函数
JavaScript
const higherOrderFunc = function() {
    return function() {
        return 12;
    }
}

// 返回下面的函数,所以它是高阶函数。
higherOrderFunc(); 
> ƒ () {
        return 12;
    }

higherOrderFunc()();
> 12
  1. 接受一个函数作为参数的函数
JavaScript
const testFunc = function(x) {
    return x + 12;
}

// 接受函数作为参数。
const higherOrderFunc = function(testFunc) {
    return testFunc(8);
}

higherOrderFunc(testFunc);
> 20

例子 1:

JavaScript
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. 函数组合:

这是一种方法,其中将一个函数的结果传给下一个函数。

JavaScript
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

看起来很清晰,但如果我们要一个接一个地调用更多函数会怎么样呢?让我们试试更干净的方法。

JavaScript
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 来实现:

JavaScript
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’ 的员工及其工资总和。

命令式风格:

JavaScript
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);

声明式风格:

JavaScript
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:

通常我们写:

JavaScript
function addition(x, y, z) {
    return x + y + z;
}

addition(1, 2, 3);
> 6

柯里化版本:

JavaScript
function addition(x) {
    return function addY(y) {
        return function addZ(z) {
            return x + y + z;
        }
    }
}

addition(1)(2)(3);
> 6

使用箭头函数:

JavaScript
addition = (x) => (y) => (z) => x + y + z;

addition(1)(2)(3);
> 6

例子 2:

JavaScript
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'

我们也可以这样写:

JavaScript
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:

JavaScript
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. 部分应用:

你为一个函数固定一定数量的参数,并生成一个新的带有较少参数的函数。这个新函数可以在稍后的时间用剩下的参数来调用。部分应用有助于创建更加专业和可重用的函数。

例子:

JavaScript
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 中的一个表达式可以用它的值来替代,这种特性叫做引用透明性。

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
JavaScript
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. 闭包:

闭包让你可以从内部函数访问外部函数的作用域。

JavaScript
function outer() {
    const name = 'test';
    function inner() {
        // 'name' 从外部函数可以访问到内部函数中
        console.log(name);
    }
    inner();
}
outer();

> test
JavaScript
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

或者,你也可以像下面这样使用箭头函数:

JavaScript
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

使用闭包的计数器示例:

JavaScript
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. 递归:

递归是一种编程技巧,在其中函数通过自我调用来解决问题。

例子:

JavaScript
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) 时,递归调用序列如下所示:

JavaScript
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中的函数式编程有了更好的理解。并且可以在日常的开发过程中进行灵活应用,以提高开发效率。

相关推荐

    评论

    昵称*

    邮箱*

    网址