您当前的位置: 首页 > 

@大迁世界

暂无认证

  • 3浏览

    0关注

    739博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

JS中函数式编程基本原理简介

@大迁世界 发布时间:2019-08-07 07:53:08 ,浏览量:3

作者:TK  译者:前端小智  来源:medium

为了保证的可读性,本文采用意译而非直译。

在长时间学习和使用面向对象编程之后,咱们退一步来考虑系统复杂性。

在做了一些研究之后,我发现了函数式编程的概念,比如不变性和纯函数。这些概念使你能够构建无副作用的函数,因此更容易维护具有其他优点的系统。

在这篇文章中,将通大量代码示例来详细介绍函数式编程和一些相关重要概念。

阿里云双12已开启,新老用户均可参与,2核1G云服务器仅需79元,,更多服务器配置及价格请关注:Hi拼团,或点此了解“云上爆款1折特惠活动”。同时,建议在购买阿里云相关产品前 先领取阿里云2000元代金券会更优惠哦。

  什么是函数式编程

函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变。

纯函数

当我们想要理解函数式编程时,需要知道的第一个基本概念是纯函数,但纯函数又是什么鬼?

咱们怎么知道一个函数是否是纯函数?这里有一个非常严格的定义:

如果给定相同的参数,则得到相同的结果

如果给出相同的参数,它返回相同的结果。想象一下,我们想要实现一个计算圆的面积的函数。

不是纯函数会这样做,接收radius 作为参数,然后计算radius * radius * PI

1let PI = 3.14;23const calculateArea = (radius) => radius * radius * PI;45calculateArea(10); // returns 314.0let PI = 3.14;
2
3const calculateArea = (radius) => radius * radius * PI;
4
5calculateArea(10); // returns 314.0

为什么这是一个不纯函数?原因很简单,因为它使用了一个没有作为参数传递给函数的全局对象。

现在,想象一些数学家认为圆周率的值实际上是42并且修改了全局对象的值。

不纯函数得到10 * 10 * 42 = 4200。对于相同的参数(radius = 10),我们得到了不同的结果。

修复它:

1let PI = 3.14;23const calculateArea = (radius, pi) => radius * radius * pi;45calculateArea(10, PI); // returns 314.0let PI = 3.14;
2
3const calculateArea = (radius, pi) => radius * radius * pi;
4
5calculateArea(10, PI); // returns 314.0

现在把 PI 的值作为参数传递给函数,这样就没有外部对象引入。

读取文件

下面函数读取外部文件,它不是纯函数,文件的内容随时可能都不一样。

1const charactersCounter = (text) => `Character count: ${text.length}`;23function analyzeFile(filename) {4  let fileContent = open(filename);5  return charactersCounter(fileContent);6}const charactersCounter = (text) => `Character count: ${text.length}`;
2
3function analyzeFile(filename) {
4  let fileContent = open(filename);
5  return charactersCounter(fileContent);
6}
随机数生成

任何依赖于随机数生成器的函数都不能是纯函数。

1function yearEndEvaluation() {2  if (Math.random() > 0.5) {3    return "You get a raise!";4  } else {5    return "Better luck next year!";6  }7}function yearEndEvaluation() {
2  if (Math.random() > 0.5) {
3    return "You get a raise!";
4  } else {
5    return "Better luck next year!";
6  }
7}
无明显副作用

纯函数不会引起任何可观察到的副作用。可见副作用的例子包括修改全局对象或通过引用传递的参数。

现在,咱们要实现一个函数,该接收一个整数并返对该整数进行加1操作且返回。

1let counter = 1;23function increaseCounter(value) {4  counter = value + 1;5}67increaseCounter(counter);8console.log(counter); // 2let counter = 1;
2
3function increaseCounter(value) {
4  counter = value + 1;
5}
6
7increaseCounter(counter);
8console.log(counter); // 2

该非纯函数接收该值并重新分配counter,使其值增加1

函数式编程不鼓励可变性。我们修改全局对象,但是要怎么做才能让它变得纯函数呢?只需返回增加1的值。

1let counter = 1;23const increaseCounter = (value) => value + 1;45increaseCounter(counter); // 26console.log(counter); // 1let counter = 1;
2
3const increaseCounter = (value) => value + 1;
4
5increaseCounter(counter); // 2
6console.log(counter); // 1

纯函数increaseCounter返回2,但是counter值仍然是相同的。函数返回递增的值,而不改变变量的值。

如果我们遵循这两条简单的规则,就会更容易理解我们的程序。现在每个函数都是孤立的,不能影响系统的其他部分。

纯函数是稳定的、一致的和可预测的。给定相同的参数,纯函数总是返回相同的结果。

咱们不需要考虑相同参数有不同结果的情况,因为它永远不会发生。

纯函数的好处

纯函数代码肯定更容易测试,不需要 mock 任何东西,因此,我们可以使用不同的上下文对纯函数进行单元测试:

一个简单的例子是接收一组数字,并对每个数进行加 1 这种沙雕的操作。

1let list = [1, 2, 3, 4, 5];23const incrementNumbers = (list) => list.map(number => number + 1);let list = [1, 2, 3, 4, 5];
2
3const incrementNumbers = (list) => list.map(number => number + 1);

接收numbers数组,使用map递增每个数字,并返回一个新的递增数字列表。

1incrementNumbers(list); // [2, 3, 4, 5, 6]list); // [2, 3, 4, 5, 6]

对于输入[1,2,3,4,5],预期输出是[2,3,4,5,6]

不可变性

尽管时间变或者不变,纯函数大佬都是不变的。

当数据是不可变的时,它的状态在创建后不能更改。

咱们不能更改不可变对象,如果非要来硬的,刚需要深拷贝一个副本,然后操作这个副本。

在JS中,我们通常使用for循环,for的每次遍历 i是个可变变量。

1var values = [1, 2, 3, 4, 5];2var sumOfValues = 0;34for (var i = 0; i 
 4  string
 5    .toLowerCase()
 6    .trim()
 7    .split(" ")
 8    .join("-");
 9
10slugify(string); // i-will-be-a-url-slug

上述代码主要做了这几件事:

引用透明性

接着实现一个square 函数:

1const square = (n) => n * n;(n) => n * n;

给定相同的输入,这个纯函数总是有相同的输出。

1square(2); // 42square(2); // 43square(2); // 44// ...2); // 4
2square(2); // 4
3square(2); // 4
4// ...

2作为square函数的参数传递始终会返回4。这样咱们可以把square(2)换成4,我们的函数就是引用透明的。

基本上,如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。

有了这个概念,咱们可以做的一件很酷的事情就是记住这个函数。假设有这样的函数

1const sum = (a, b) => a + b;(a, b) => a + b;

用这些参数来调用它

1sum(3, sum(5, 8));

sum(5, 8) 总等于13,所以可以做些骚操作:

1sum(3, 13);

这个表达式总是得到16,咱们可以用一个数值常数替换整个表达式,并把它记下来。

函数是 JS 中的一级公民

函数作为 JS 中的一级公民,很风骚,函数也可以被看作成值并用作数据使用。

其思想是将函数视为值,并将函数作为数据传递。通过这种方式,我们可以组合不同的函数来创建具有新行为的新函数。

假如我们有一个函数,它对两个值求和,然后将值加倍,如下所示:

1const doubleSum = (a, b) => (a + b) * 2;(a, b) => (a + b) * 2;

对应两个值求差,然后将值加倍:

1const doubleSubtraction = (a, b) => (a - b) * 2;(a, b) => (a - b) * 2;

这些函数具有相似的逻辑,但区别在于运算符的功能。如果我们可以将函数视为值并将它们作为参数传递,我们可以构建一个接收运算符函数并在函数内部使用它的函数。

1const sum = (a, b) => a + b;2const subtraction = (a, b) => a - b;34const doubleOperator = (f, a, b) => f(a, b) * 2;56doubleOperator(sum, 3, 1); // 87doubleOperator(subtraction, 3, 1); // 4const sum = (a, b) => a + b;
2const subtraction = (a, b) => a - b;
3
4const doubleOperator = (f, a, b) => f(a, b) * 2;
5
6doubleOperator(sum, 3, 1); // 8
7doubleOperator(subtraction, 3, 1); // 4

f参数并用它来处理ab, 这里传递了sum函数和subtraction并使用doubleOperator函数进行组合并创建新行为。

高阶函数

当我们讨论高阶函数时,通常包括以下几点:

上面实现的doubleOperator函数是一个高阶函数,因为它将一个运算符函数作为参数并使用它。

我们经常用的filtermapreduce都是高阶函数,Look see see。

Filter

对于给定的集合,我们希望根据属性进行筛选。filter函数期望一个truefalse值来决定元素是否应该包含在结果集合中。

如果回调表达式为真,过滤器函数将在结果集合中包含元素,否则,它不会。

一个简单的例子是,当我们有一个整数集合,我们只想要偶数。

命令式

使用命令式方式来获取数组中所有的偶数,通常会这样做:

我们还可以使用filter高阶函数来接收偶函数并返回一个偶数列表:

const even = n => n % 2 == 0; const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]

我在 Hacker Rank FP 上解决的一个有趣问题是Filter Array问题。问题是过滤给定的整数数组,并仅输出小于指定值X的那些值。

命令式做法通常是这样的:

 1var filterArray = function(x, coll) { 2  var resultArray = []; 3 4  for (var i = 0; i  person.age > 21;
2const overAge = people => people.filter(olderThan21);
3overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
map

map函数的主要思路是转换集合。

map方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。

假如我们不想过滤年龄大于 21 的人,我们想做的是显示类似这样的:TK is 26 years old.

使用命令式,我们通常会这样做:

 1var people = [ 2  { name: "TK", age: 26 }, 3  { name: "Kaio", age: 10 }, 4  { name: "Kazumi", age: 30 } 5]; 6 7var peopleSentences = []; 8 9for (var i = 0; i  people.map(makeSentence);45peopleSentences(people);6// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']const makeSentence = (person) => `${person.name} is ${person.age} years old`;
2
3const peopleSentences = (people) => people.map(makeSentence);
4
5peopleSentences(people);
6// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']

整个思想是将一个给定的数组转换成一个新的数组。

另一个有趣的HackerRank问题是更新列表问题。我们想要用一个数组的绝对值来更新它的值。

例如,输入[1,2,3,- 4,5]需要输出为[1,2,3,4,5]-4的绝对值是4

一个简单的解决方案是每个集合中值的就地更新,很危险的作法

1var values = [1, 2, 3, -4, 5];23for (var i = 0; i  values.map(Math.abs);
4
5updateListMap(values); // [1, 2, 3, 4, 5]
Reduce

reduce函数的思想是接收一个函数和一个集合,并返回通过组合这些项创建的值。

常见的的一个例子是获取订单的总金额。

假设你在一个购物网站,已经将产品1、产品2、产品3和产品4添加到购物车(订单)中。现在,我们要计算购物车的总数量:

以命令式的方式,就是便利订单列表并将每个产品金额与总金额相加。

 1var orders = [ 2  { productTitle: "Product 1", amount: 10 }, 3  { productTitle: "Product 2", amount: 30 }, 4  { productTitle: "Product 3", amount: 20 }, 5  { productTitle: "Product 4", amount: 60 } 6]; 7 8var totalAmount = 0; 910for (var i = 0; i  shoppingCart.reduce(sumAmount, 0);1112getTotalAmount(shoppingCart); // 120let shoppingCart = [
 2  { productTitle: "Product 1", amount: 10 },
 3  { productTitle: "Product 2", amount: 30 },
 4  { productTitle: "Product 3", amount: 20 },
 5  { productTitle: "Product 4", amount: 60 }
 6];
 7
 8const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;
 9
10const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0);
11
12getTotalAmount(shoppingCart); // 120

这里有shoppingCart,接收当前currentTotalAmount的函数sumAmount,以及对它们求和的order对象。

咱们也可以使用mapshoppingCart转换为一个amount集合,然后使用reduce函数和sumAmount函数。

const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount;

1function getTotalAmount(shoppingCart) {2  return shoppingCart3    .map(getAmount)4    .reduce(sumAmount, 0);5}67getTotalAmount(shoppingCart); // 120function getTotalAmount(shoppingCart) {
2  return shoppingCart
3    .map(getAmount)
4    .reduce(sumAmount, 0);
5}
6
7getTotalAmount(shoppingCart); // 120

getAmount接收product对象并只返回amount值,即[10,30,20,60],然后,reduce通过相加将所有项组合起来。

三个函数的示例

看了每个高阶函数的工作原理。这里为你展示一个示例,说明如何在一个简单的示例中组合这三个函数。

说到购物车,假设我们的订单中有这个产品列表

1let shoppingCart = [2  { productTitle: "Functional Programming", type: "books", amount: 10 },3  { productTitle: "Kindle", type: "eletronics", amount: 30 },4  { productTitle: "Shoes", type: "fashion", amount: 20 },5  { productTitle: "Clean Code", type: "books", amount: 60 }6]let shoppingCart = [
2  { productTitle: "Functional Programming", type: "books", amount: 10 },
3  { productTitle: "Kindle", type: "eletronics", amount: 30 },
4  { productTitle: "Shoes", type: "fashion", amount: 20 },
5  { productTitle: "Clean Code", type: "books", amount: 60 }
6]

假如相要想要购物车里类型为 books的总数,通常会这样做:

 1let shoppingCart = [ 2  { productTitle: "Functional Programming", type: "books", amount: 10 }, 3  { productTitle: "Kindle", type: "eletronics", amount: 30 }, 4  { productTitle: "Shoes", type: "fashion", amount: 20 }, 5  { productTitle: "Clean Code", type: "books", amount: 60 } 6 ] 7 8 const byBooks = (order) => order.type == "books"; 9 const getAmount = (order) => order.amount;10 const sumAmount = (acc, amount) => acc + amount;1112function getTotalAmount(shoppingCart) {13  return shoppingCart14    .filter(byBooks)15    .map(getAmount)16    .reduce(sumAmount, 0);17  }18getTotalAmount(shoppingCart); // 70let shoppingCart = [
 2  { productTitle: "Functional Programming", type: "books", amount: 10 },
 3  { productTitle: "Kindle", type: "eletronics", amount: 30 },
 4  { productTitle: "Shoes", type: "fashion", amount: 20 },
 5  { productTitle: "Clean Code", type: "books", amount: 60 }
 6 ]
 7
 8 const byBooks = (order) => order.type == "books";
 9 const getAmount = (order) => order.amount;
10 const sumAmount = (acc, amount) => acc + amount;
11
12function getTotalAmount(shoppingCart) {
13  return shoppingCart
14    .filter(byBooks)
15    .map(getAmount)
16    .reduce(sumAmount, 0);
17  }
18getTotalAmount(shoppingCart); // 70

原文:https://medium.com/better-programming/introduction-to-the-basic-principles-of-functional-programming-in-javascript-6849ae196326

码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

关注
打赏
1664287990
查看更多评论
立即登录/注册

微信扫码登录

0.0506s