简单的说,“函数式编程”就是一种编程范式,也就是如何编写程序的方法论。

虽然JavaScript并不是一门纯函数式编程的语言,但是它使用了许多函数式编程的特性。因此了解这些特性可以让我们更加了解我们自己的代码

意义

函数式编程到底有什么意义呢?在学习JavaScript基础知识之前,我甚至都没有关注过这个词,当我看到阮一峰大神的博客以后,有了答案。函数式编程的意义大致如下:

1. 代码简洁,开发快速

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。(阮一峰大神通过c语言和Lisp语言的编程周期进行对比)
在JavaScript的开发中具体体现在模块化和函数封装方面,就是将我们日常开发中频繁重复的业务逻辑用函数的方式进行一个封装,这样可以去掉大量冗余代码,也给加快了开发效率。

2. 接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码
用一个简单的例子来说明下:

  1. // 将表达式(1 + 2) * 3 - 4;写出一个编程式函数
  2. subtract(multiple(add(1, 2), 3, 4));
  3. // 进行简化得到,使之更易懂
  4. add(1, 2).multiple(3).subtract(4)

3. 更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每个函数都可以被看作独立单元,对单元测试和出错很有利,以及模块化组合。

4. 易于“并发编程”

函数式编程不必考虑“死锁”,因为它不修改变量,所以根本不存在“锁”线程的问题。不必担心一个线程的数据,被另一个线程修改。所以可以很放心地把工作分摊到多个线程,部署“并发编程”。这个在JS开发意义不大,所以不多加阐述

5. 代码的热升级

函数式编程没有副作用,只要保证接口不变,也就是参数不变。内部实现是与外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,这部分在JavaScript中的体现就是纯函数输出,函数内部实现与外部环境解耦。

特性

函数式编程具有一些特性,如下:

1. 函数式第一等公民

所谓“第一等公民”,指的是函数和其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。这些场景我们应该见过很多:

  1. var a = function foo() {} // 赋值
  2. function fn(function() {}, arg) {} // 函数作为参数
  3. // 函数作为返回值
  4. function func() {
  5. return function() {
  6. ……
  7. }
  8. }

以上这些情况都是很常见的,但是我想还有一些类似常见的情况,大家可能都会忽视,比如说:

我们先定义一个这样的函数:

  1. function delay() {
  2. console.log('5000ms之后执行该方法');
  3. }

现在要做的是,如果要求你结合setTimeout方法,让delay方法延时5000ms执行,应该怎么做呢?

这也太简单了吧,把delay方法放入setTimeout函数的回调不就得了

  1. setTimeout(function() {
  2. delay();
  3. }, 5000);

这不就搞定了吗,那么问题来了,上面说了函数是一等公民,可以作为参数传入另一个函数,为啥不直接将函数名作为参数传入setTimeout方法呢。

  1. setTimeout(delay, 5000);

这样不是更省事嘛,不要急,如果你理解了上面这个,那很不错,再来看下下面这个情况该怎么优化呢?

  1. function getUser(path, callback) {
  2. $.get(path, function(info) {
  3. callback(info);
  4. })
  5. }
  6. // 使用getUSer
  7. getUser('/api/user', function(resp) {
  8. // resp为成功请求之后返回的数据
  9. console.log(resp);
  10. })

大家仔细琢磨琢磨,这里该咋优化?优化的原理和上面的setTimeout()一样哦~

2. 只用表达式,不用语句

“表达式”是一个单纯的运算过程,总是有返回值;“语句”是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,必须有返回值。

假如我们的项目中有多处需要修改某个元素的字体颜色,我们可以这样封装一下:

  1. var ele = document.querySelector('div');
  2. function changeColor(color) {
  3. ele.style.color = color;
  4. }
  5. // 多处使用
  6. changeColor('red');
  7. changeColor('yellow');

这样封装有它的好处,但是changeColor函数中仅仅有一条语句,这并不是理想的效果,函数式编程期望有输入,有输出。因此良好的编程习惯应该是如下:

  1. function changeColor(ele, color) {
  2. ele.style.color = color;
  3. return color;
  4. }
  5. // 多处使用
  6. var ele = document.querySelector('div');
  7. changeColor(ele, 'red');
  8. changeColor(ele, 'blue');

了解这一点,可以让我们自己在封装函数的时候养成良好的习惯。

3. 纯函数

相同的输入总会得到相同的输出,并且不会产生副作用的函数就是纯函数。

所谓“副作用”,指的是函数内部与外部耦合(最典型的情况就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有“副作用”,意味着函数要保持独立,所有功能就是返回一个新的值。没有其他行为,尤其不能修改外部变量的值

即所谓的只要是同样的参数传入,返回的结果一定是相同的

例如我们期望封装一个函数,能够得到传入数组的最后一项。那么可以通过下面两种方式来实现。

  1. function getLast(arr) {
  2. return arr[arr.length];
  3. }
  4. function _getLast(arr) {
  5. return arr.pop();
  6. }
  7. var source = [1, 2, 3, 4, 5];
  8. var last = getLast(source); // 返回结果[1, 2, 3, 4],原数组保持不变
  9. var _last = _getLast(source); // 返回结果[1, 2, 3, 4],原数组改变

getLast()与_getLast()虽然都能返回我们想要的值,但是_getLast()修改了原数组的值,当我们下次调用该方法时,同样的参数,得到的结果就不一样了。这样不可预测的封装方式在我们看来,是很糟糕的体验。它会把我们的数据搞得很混乱。在JavaScript原声支持的数据方法中,也有许多不纯的方法。我们在使用时需要非常警惕,我们要清晰地知道原始数据的改变是否会留下隐患。

  1. var source = [1, 2, 3, 4, 5];
  2. source.slice(1, 3); // 纯函数,返回[2, 3] 原数组不变
  3. source.splice(1, 3); // 不纯的,返回[2, 3, 4] 原数组不变
  4. source.pop(); // 不纯的, 改变了原数组
  5. source.push(6); // 不纯的, 改变了原数组
  6. source.shift(); // 不纯的, 改变了原数组
  7. source.unshift(1); // 不纯的, 改变了原数组
  8. source.reverse(); // 不纯的, 改变了原数组
  9. // 此时的source已经变得很混乱了,所以重新给source赋值
  10. source = [1, 2, 3, 4, 5];
  11. source.concat([6, 7]); // 纯函数, 返回一个副本,不改变原数组
  12. Source.join('-'); // 纯函数,返回一段拼接字符串

总结

我们应该在项目开发中有意识的使用函数式编程范式,提高代码的复用性和健壮性。也可以加快我们的开发效率。
文章中有不对的地方欢迎指正,有兴趣的话可以交流交流~~~
本文章内容引用内容:这波能反杀的博客:函数和函数式编程阮一峰大神的函数式编程初探