快速入门函数式编程
大家好,今天我想跟大家介绍一下JavaScript函数式编程。函数式编程是一种编程范式,它强调的是函数的使用和组合,而不是像传统的面向对象编程那样,强调状态和可变性。在JavaScript中,函数式编程已经得到了广泛的应用,例如Redux
、Lodash
、Ramda
等库都是基于函数式编程思想实现的。如果你想学习JavaScript函数式编程,那么本次分享一定会对你有所帮助。
编程范式
函数式编程是一种编程范式。编程范式指的是一种编写代码的方式或方法,是一种思维方式,不同的编程范式有着不同的特点和优缺点,常见的编程范式有面向过程编程、面向对象编程、函数式编程等等。让我们来具体看几个例子:
面向过程编程
// 定义一个计算两个数和的函数
function sum(a, b) {
return a + b;
}
// 读取用户输入的两个数
let num1 = parseFloat(prompt("请输入第一个数:"));
let num2 = parseFloat(prompt("请输入第二个数:"));
// 调用sum函数计算两个数的和
let result = sum(num1, num2);
// 输出结果
console.log("两个数的和为:" + result);
这是一个面向过程编程的案例,我们定义了一个计算两个数和的函数sum,然后通过读取用户输入的两个数,调用sum函数计算它们的和,最后输出结果。这个案例中没有使用任何面向对象编程的概念,而是通过流程控制的方式完成了计算。
面向对象编程
接下来我们看一个面向对象编程的案例:
class Calculator {
constructor(num1, num2) {
this.num1 = num1;
this.num2 = num2;
}
sum() {
return this.num1 + this.num2;
}
}
let calculator1 = new Calculator(5, 7);
console.log("两个数的和为:" + calculator1.sum());
在这个案例中,我们定义了一个名为Calculator的类,通过构造函数定义两个数的属性,然后定义了一个sum方法,用于计算两个数的和。最后,我们创建了一个Calculator对象calculator1,并调用其sum方法计算两个数的和,将结果输出到控制台中。这个案例中使用了面向对象编程的概念,使代码更加清晰、易于维护和扩展。
函数式编程
最后我们再来看一个函数式编程的案例:遍历一个数组,将每一个元素的数值翻倍
const arr = [1, 2, 3, 4, 5];
const doubledArr = arr.map(num => num * 2);
console.log(doubledArr);
在这个案例中,我们定义了一个名为arr的数组,并使用map方法,对数组中的每一个元素进行操作,在箭头函数中将每个元素乘以2,最终返回一个新的数组doubledArr。最后,我们将新的数组输出到控制台中。这个案例中使用了函数式编程的概念,通过使用高阶函数map,将操作封装成函数,并处理返回值,使代码更加清晰、易于维护和扩展。
注意:函数式编程中的函数不是JavaScript语言中的函数概念,而是我们初中数学里的函数,可以看做是一种映射关系,将输入值映射为输出值。
既然编程范式有那么多种,为什么我们在一些设计中更多会选择函数式编程,它究竟有什么好处呢?这个就要从副作用和纯函数说起了。
副作用和纯函数
函数是对过程的封装,但函数的实现本身可能依赖外部环境,而一旦依赖外部环境,就有可能产生副作用,那我们首先看下什么是副作用?
副作用
所谓函数的副作用,是指函数执行本身可能依赖了外部不可控的环境,具体可以分为3大类进行探讨:
第一类,函数中最常见的副作用,就是全局变量,来看一个案例:
let count = 0;
function increment() {
count++;
console.log(count);
}
increment(); // 输出1
increment(); // 输出2
在这个例子中,increment
函数会对全局变量count
的值造成改变,因此increment
函数具有副作用。每次调用increment
函数都会将count
的值加1,并且将结果输出到控制台上。 这种全局变量对函数的副作用可能会导致代码变得难以理解和调试。因为我们无法确定哪些函数会对全局变量产生影响,从而导致程序的错误或不可预测的结果。
第二类函数中的副作用是 IO 影响,这里的 IO 说的不是函数里的参数和返回值,而是类似前端浏览器中的用户行为,比如鼠标和键盘的输入。
let clicked = 0;
function countClicks() {
document.addEventListener('click', () => { //和外部环境(文档的 DOM 结构)关联
clicked++;
console.log(clicked);
});
}
countClicks();
在这个例子中,countClicks
函数会对用户的鼠标行为造成影响,每次用户点击页面时,clicked
的值都会加1,并且将结果输出到控制台上。这也是一种副作用,因为函数的执行不仅仅是输出结果,还会改变外部环境的状态。
第三类副作用是网络,比如我们要针对用户下单的动作发起一个网络请求,需要先获得用户 ID,再连着用户的 ID 一起发送。假如网络速度慢,如果还没获取到用户 ID,就发起下单请求,就会报错。
那有没有办法减少以上这些副作用呢?答案是在函数式编程中有个核心概念叫做纯函数(pure function)
纯函数
通常把不依赖外部环境和没有副作用的函数叫做纯函数,依赖外部环境或有副作用的函数叫做非纯函数。具体我们来看几个案例:
function add(x, y) {
return x + y;
}
function getEl(id) {
return document.getElementById(id);
}
案例一add 是一个纯函数,它的返回结果只依赖于输入的参数,无论你调用多少次又或者任何时候调用,结果都是一样的。
案例二 getEl 是一个非纯函数,它的返回值除了依赖于参数 id,还和外部环境(文档的 DOM 结构)有关。
了解了什么是纯函数,那它和非纯函数相比有哪些优点呢?
纯函数优点
优点一:易于测试
纯函数不需要依赖外部环境,直接写测试 case 就可以了,例如:
test(t => {
dosth...
done!
});
非纯函数因为依赖外部环境,在测试的时候我们还需要构建外部环境
//开始准备工作
test.before(t => {
//setup environments
});
//结束之后清空配置
test.after('cleanup', t => {
//clean
});
test(t => {
dosth...
done!
});
优点二:纯函数可以并行计算
在浏览器中,我们可以利用 Worker 来并行执行多个纯函数,在 Node.js 中,我们也可以用 Cluster 来实现同样的并行执行。
优点三:纯函数有良好的 Bug 自限性
纯函数不会对外部环境产生任何影响,不会改变任何状态或变量,因为它们只能通过输入参数来计算输出结果,不会受到其他因素的影响。这样就使得纯函数的行为更加可预测和可控,从而减少了出现错误和异常的可能性。
既然纯函数有这么多好处,那我们就可以在自己的项目里尽量多的去使用它。接下来,我举几个在项目中常用到的纯函数案例:柯里化(curry)和函数复合(Compose)
柯里化(curry)
首先什么是柯里化呢?我们可以简单的理解成用少于期望数量的参数去调用一个函数,这个函数返回一个接受剩下参数的函数。这么说可能很抽象,我们来一个具体的案例:
我们先来看看一个简单的add
函数,这个函数接受一个参数并且返回一个函数。
//原始的加法函数add
function add(x, y, z) {
return x + y + z;
}
// 改成柯里化函数
function curriedAdd(x) {
return function(y) {
return function(z) {
return x + y + z;
};
};
}
curriedAdd(1)(2)(3) // 6
在这个例子中,我们定义了一个原始的加法函数add
,它接受三个参数并返回它们的和。然后我们定义了一个柯里化的函数curriedAdd
,它接受一个参数x
,并返回一个新的函数,这个新函数接受参数y
,并返回另一个新函数,这个新函数接受参数z
,并返回x + y + z
的结果。不过这样写太麻烦了,我们可以实现一个通用的 curry
方法来实现上面的效果。
function add(x, y, z) { return x + y + z;} function curry(fn) { // 获得函数参数的数量 const arity = fn.length; return function curried(...args) { // 如果当前收集到的参数数量大于需要的数量,那么执行该函数 if (args.length >= arity) return fn(...args); // 否者,将传入的参数收集起来 // 下面的写法类似于 // return (...args1) => curried(...args, ...args1); return curried.bind(null, ...args); };}let curryAdd = curry( add );console.log(curryAdd(1)(2)(3)) //6 可以每次只传一个参数console.log(curryAdd(1,2)(3)) //6 也可以根据需要传入多个参数,不影响结果
这段代码实现了一个函数柯里化的通用方法curry
,它将任意一个函数转化为柯里化的函数,使得该函数可以接受单个参数,也可以接受多个参数并返回一个新的函数,直到收集到足够的参数后执行原始函数。
curry
函数的实现方法比较简单,它首先获取原始函数的参数数量,然后返回一个接受任意数量参数的函数curried
。在curried
函数中,如果当前收集到的参数数量大于需要的数量,那么执行该函数,否则将传入的参数收集起来,返回一个绑定了这些参数的新函数,继续等待接收下一个参数。 通过使用函数柯里化,我们可以预先定义一些参数,生成一个新的函数,然后在需要的时候再传入剩余的参数,这样可以使代码更加灵活和可复用。使用这种方法,我们不需要在定义函数时考虑所有可能的参数组合,而是可以在运行时动态地定义和调用这些函数。
函数复合(Compose)
函数复合是函数式编程中的一个重要概念,它指的是将多个函数组合在一起,形成一个新的函数。这个新函数将会按照一定的顺序依次执行这些函数,每个函数的输出作为下一个函数的输入。这样的组合方式可以实现更为复杂的逻辑,同时也方便了代码的复用。我们还是来看一个具体的例子:
function addOne(x) {
return x + 1;
}
function double(x) {
return x * 2;
}
function square(x) {
return x * x;
}
// 定义一个函数复合的函数
function compose(...fns) {
return function(x) {
return fns.reduceRight((acc, fn) => fn(acc), x);
}
}
// 将三个函数组合成一个新函数
const addOneThenDoubleThenSquare = compose(square, double, addOne);
// 使用新函数计算结果
const result = addOneThenDoubleThenSquare(3); // ((3 + 1) * 2) ^ 2 = 64
console.log(result); // 输出 64
在这个例子中,我们定义了三个简单的函数:addOne
、double
和square
。然后我们使用compose
函数将它们组合成了一个新的函数addOneThenDoubleThenSquare
。这个新函数的执行顺序是先执行addOne
,然后将结果传递给double
,再将double
的结果传递给square
。最后,我们使用这个新函数计算了输入值为3
时的结果,并将结果输出到控制台上。 这个例子展示了如何使用函数复合来组合多个函数,实现更加复杂的逻辑。通过这种方式,我们可以将复杂的程序逻辑分解成简单的函数,并使用函数复合的方式将它们组合起来,使代码更加简洁、可读、易于维护。
要点总结
函数式编程的内容非常多,我这里只是举例说了一些基础的概念和代码,把大家带进了函数式编程的大门。里面还有很多知识点例如不可变,单子等等很多概念和应用,感兴趣推荐一本书
《Javascript函数式编程思想》
首先,我们了解了编程范式并且举例进行了说明,然后,我们知道函数式编程有一个非常大的优点,就是能够减少非纯函数的数量,这也是我们设计系统时要遵循的原则。因为相比于非纯函数,纯函数具有更好的可测试性、执行效率和可维护性。最后,我们还学会了函数式编程中最重要的两个应用柯里化和函数复合,也是函数式编程的
转载自:https://juejin.cn/post/7212969447424966716