函数式编程

函数编程是已以函数作为主要载体的编程方式,用函数去拆解、抽象一般的表达式。

与命令式编程相比有哪些好处?

  • 语义清晰
  • 复用性高
  • 可维护性好
  • 作用局局限,副作用少

基本的函数式编程

1、数组中的每个字母的首字母大写

一般写法

  var list = ['apple', 'pen', 'style'];
  for(const i in list){
    const c = list[i][0];
    console.log(c.toLocaleLowerCase())
    list[i] = c.toUpperCase() + list[i].slice(1);
  }
  console.log(list)

函数式写法一

  var list = ['apple', 'pen', 'style'];
  function upperFirst(word){
    return word[0].toUpperCase() + word.slice(1);
  }

  function wordToUpperCase(list){
    return list.map(upperFirst);
  }

  console.log(wordToUpperCase(list));

函数式写法二

console.log(['apple', 'pen', 'style'].map(word => word[0].toUpperCase() + word.slice(1)));
当情况变得复杂的时候,表达式写法会遇到几个问题。
  1. 表意不明显,逐渐难以维护。
  2. 复用性差,产生更多的代码量。
  3. 会产生很多的中间变量。

函数式编程很好的解决以上的问题,如函数式写法一,它利用了函数封装性将功能做拆解,并封装为不同的函数,再利用组合的调用达到目的。这样做使表意更清晰,易于维护、复用以及扩展。其次利用高阶函数,Array.map 代替 for ... of做数组遍历,减少了中间变量和操作。

而函数式写法一和函数式写法二的区别在于,可以考虑后续函数复用的可能,如果没有,则后者更优。

链式优化

从上面的函数式写法二中可以看出,函数式代码在写的过程中,很容易造成横向延展,即产生多层嵌套。

计算数字之和

//一般写法
console.log(1 + 2 + 3 - 4);
//函数式写法
function sum(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

console.log(sub(sum(sum(1, 2), 3), 4);

随着函数的嵌套层数不断增多,导致代码的可读性下降,还容易产生错误。

在这种情况下,我们可以考虑链式优化。

const utils = {
  chain(a) {
    this._temp = a;
    return this;
  },
  sum(b) {
    this._temp += b;
    return this;
  },
  sub(b) {
    this._temp -= b;
    return this;
  },
  value() {
    return this._temp;
  }
};

console.log(utils.chain(1).sum(2).sum(3).sub(4).value());

这样改写之后,结构整体变得比较清晰。函数的嵌套和链式对比还有一个很好的例子,就是回调函数和Promise模式。

顺序请求两个接口

//回调函数
import $ from 'jquery';
$.post('a/url/to/target', (rs) => {
  if(rs){
    $.post('a/url/to/another/target', (rs2) => {
      if(rs2){
        $.post('a/url/to/third/target');
      }
    });
  }
});
//Promise
import request from 'catta';  // catta 是一个轻量级请求工具,支持 fetch,jsonp,ajax,无依赖
request('a/url/to/target')
  .then(rs => rs ? $.post('a/url/to/another/target') : Promise.reject())
  .then(rs2 => rs2 ? $.post('a/url/to/third/target') : Promise.reject());

随着回调函数嵌套层级和单层复杂数增加,它会变得臃肿且难以维护,而 Promise 的链式结构,在高度复杂时,仍能纵向扩展,而且层次清晰。

常见的函数式编程模型

闭包

闭包是由函数以及创建该函数的词法环境组合而成。
可以保留局部变量不被释放的代码块,称为闭包。

//创建一个闭包
function makeCounter() {
  let k = 0;

  return function() {
    return ++k;
  };
}

const counter = makeCounter();

console.log(counter());  // 1
console.log(counter());  // 2

makeCounter 这个函数的代码块,在返回的函数中,对局部变量 k ,进行了引用,导致局部变量无法在函数执行结束后,被系统回收掉,从而产生了闭包。而这个闭包的作用就是,“保留住“ 了局部变量,使内层函数调用时,可以重复使用该变量;而不同于全局变量,该变量只能在函数内部被引用。

换句话说,闭包其实就是创造出了一些函数私有的 ”持久化变量“。

所以从这个例子,我们可以总结出,闭包的创造条件是:

1、存在内、外两层函数
2、内层函数对外层函数的局部变量进行了引用

闭包的用途

闭包的主要用途就是可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者中间量等等。

简单的缓存工具

//匿名函数创建一个闭包
const cache = (function() {
  const store = {};
  
  return {
    get(key) {
      return store[key];
    },
    set(key, val) {
      store[key] = val;
    }
  }
}());

cache.set('a', 1);
cache.get('a');  // 1

上面的例子是一个简单的缓存工具的实现,匿名函数创建了一个闭包,使得 store 对象,一直可以被引用,不会被回收。

闭包的弊端

持久化的变量不会被正常释放,持续占有内存空间,很容易造成内存浪费,所以一般需要一些额外手动的清除机制。

高阶函数

接受或者返回一个函数的函数称为高阶函数。

JavaScript 预言师原生支持高阶函数的,因为 JavaScript 的函数是一等公民,它既可以作为参数又可以作为另一个函数的返回值使用。

我们经常在JavaScript中见到雨多原生的高阶函数,例如 Array.map, Array.reduce , Array.filter。

下面以 map 为例,看看它是如何使用的。

map(映射)

映射是对集合而言的,即把集合的每一项都做相同的变换,产生一个新的集合。

map 作为一个高阶函数,它接受一个函数的参数作为映射的逻辑。

数组中的每一项加一,组成一个新数组。
//一般写法
const arr = [4, 5, 6, 7];
const rs = [];
for(const n of arr){
  rs.push(n + 1);
}
console.log(rs)

// map改写
const arr = [1, 2, 3, 4];
const rs = arr.map(n => ++n);
console.log(rs)

上面的一般写法,利用 for ...of 循环的方式遍历数组会产生额外的操作,而且有改变原数组的风险,而 map 函数封装了必要的操作,使我们仅需要关心映射的逻辑的函数实现即可,减少了代码量,也降低了副作用产生的风险。

柯里化

给定一个函数的部分参数,生成一个接受其他参数的新函数。

可能不常听到这个名词,但是用过 undescore 或 lodash 的人都见过他。

有一个神奇的 _.partial 函数,它就是柯里化的实现

// 获取目标文件对基础路径的相对路径


// 一般写法
const BASE = '/path/to/base';
const relativePath = path.relative(BASE, '/some/path');


// _.parical 改写
const BASE = '/path/to/base';
const relativeFromBase = _.partial(path.relative, BASE);

const relativePath = relativeFromBase('/some/path');

通过 _.partial ,我们得到了新的函数 relativeFromBase ,这个函数在调用时就相当于调用 path.relative ,并默认将第一个参数传入 BASE ,后续传入的参数顺序后置。

本例中,我们真正想完成的操作是每次获得相对于 BASE 的路径,而非相对于任何路径。柯里化可以使我们只关心函数的部分参数,使函数的用途更加清晰,调用更加简单。

组合(Composing)

将多个函数的能力合并,创造一个新的函数。
同样你第一次见到他可能还是在 lodash 中,compose 方法(现在叫 flow)

// 数组中每个单词大写,做 Base64


// 一般写法 (其中一种)
const arr = ['pen', 'apple', 'applypen'];
const rs = [];
for(const w of arr){
  rs.push(btoa(w.toUpperCase()));
}
console.log(rs);


// _.flow 改写
const arr = ['pen', 'apple', 'applypen'];
const upperAndBase64 = _.partialRight(_.map, _.flow(_.upperCase, btoa));
console.log(upperAndBase64(arr));

_.flow 将转大写和转 Base64 的函数的能力合并,生成一个新的函数。方便作为参数函数或后续复用。

函数式编程的特点

1. 函数式一等公民

指的是函数和其他的数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为其他函数的返回值。

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

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

原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。

当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式 

编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3. 没有"副作用"

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

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

4. 不修改状态

上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。

function reverse(string) {
 if(string.length == 0) {
   return string;
 } else {
  return reverse(string.substring(1, string.length)) + string.substring(0, 1);
 }
}

5. 引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

原文链接
我眼中的 JavaScript 函数式编程

你可能感兴趣的:(函数式编程)