将一个无效参数传递给了将无效参数视为严重错误的函数_JavaScrip函数式编程原理简介...
发布日期:2021-09-14 01:32:05 浏览次数:2 分类:技术文章

本文共 6172 字,大约阅读时间需要 20 分钟。

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

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

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

9c50d33dfa7e1df941cce55a82bb70ff.png

什么是函数式编程

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

纯函数

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

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

  • 如果给定相同的参数,则返回相同的结果(也称为确定性)。
  • 它不会引起任何副作用。

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

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

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

let PI = 3.14;const calculateArea = (radius) => radius * radius * PI;calculateArea(10); // returns 314.0

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

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

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

修复它:

let PI = 3.14;const calculateArea = (radius, pi) => radius * radius * pi;calculateArea(10, PI); // returns 314.0

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

  • 对于参数radius = 10和PI = 3.14,始终都会得到相同的结果:314.0。
  • 对于 radius = 10 和 PI = 42,总是得到相同的结果:4200

读取文件

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

const charactersCounter = (text) => `Character count: ${text.length}`;function analyzeFile(filename) { let fileContent = open(filename); return charactersCounter(fileContent);}

随机数生成

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

function yearEndEvaluation() { if (Math.random() > 0.5) { return "You get a raise!"; } else { return "Better luck next year!"; }}

无明显副作用

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

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

let counter = 1;function increaseCounter(value) { counter = value + 1;}increaseCounter(counter);console.log(counter); // 2

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

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

let counter = 1;const increaseCounter = (value) => value + 1;increaseCounter(counter); // 2console.log(counter); // 1

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

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

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

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

纯函数的好处

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

  • 给定一个参数 A,期望函数返回值 B
  • 给定一个参数C,期望函数返回值D

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

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

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

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

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

不可变性

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

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

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

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

var values = [1, 2, 3, 4, 5];var sumOfValues = 0;for (var i = 0; i < values.length; i++) { sumOfValues += values[i];}sumOfValues // 15

对于每次遍历,都在更改i和sumOfValue状态,但是我们如何在遍历中处理可变性呢? 答案就是使用递归

let list = [1, 2, 3, 4, 5];let accumulator = 0;function sum(list, accumulator) { if (list.length == 0) { return accumulator; } return sum(list.slice(1), accumulator + list[0]);}sum(list, accumulator); // 15list; // [1, 2, 3, 4, 5]accumulator; // 0

上面代码有个 sum 函数,它接收一个数值向量。函数调用自身,直到 list为空退出递归。对于每次“遍历”,我们将把值添加到总accumulator中。

使用递归,咱们保持变量不变。不会更改list和accumulator变量。它保持相同的值。

观察:我们可以使用reduce来实现这个功能。这个在接下的高阶函数内容中讨论。

构建对象的最终状态也很常见。假设我们有一个字符串,想把这个字符串转换成url slug。

在Ruby的面向对象编程中,咱们可以创建一个类 UrlSlugify,这个类有一个slugify方法来将字符串输入转换为url slug。

class UrlSlugify attr_reader :text def initialize(text) @text = text end def slugify! text.downcase! text.strip! text.gsub!(' ', '-') endendUrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"

上面使用的有命令式编程方式,首先用小写字母表示我们想在每个slugify进程中做什么,然后删除无用的空格,最后用连字符替换剩余的空格。

这种方式在整个过程中改变了输入状态,显然不符合纯函数的概念。

这边可以通过函数组合或函数链来来优化。换句话说,函数的结果将用作下一个函数的输入,而不修改原始输入字符串。

const string = " I will be a url slug ";const slugify = string => string .toLowerCase() .trim() .split(" ") .join("-");slugify(string); // i-will-be-a-url-slug

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

  • toLowerCase:将字符串转换为所有小写字母。
  • trim:删除字符串两端的空白。
  • split和join:用给定字符串中的替换替换所有匹配实例

引用透明性

接着实现一个square 函数:

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

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

square(2); // 4square(2); // 4square(2); // 4// ...

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

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

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

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

用这些参数来调用它

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

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

sum(3, 13);

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

函数是 JS 中的一级公民

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

  • 从常量和变量中引用它。
  • 将其作为参数传递给其他函数。
  • 作为其他函数的结果返回它。

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

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

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

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

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

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

const sum = (a, b) => a + b;const subtraction = (a, b) => a - b;const doubleOperator = (f, a, b) => f(a, b) * 2;doubleOperator(sum, 3, 1); // 8doubleOperator(subtraction, 3, 1); // 4

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

高阶函数

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

  • 将一个或多个函数作为参数
  • 返回一个函数作为结果

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

我们经常用的filter、map和reduce都是高阶函数,Look see see。

Filter

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

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

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

命令式

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

  • 创建一个空数组evenNumbers
  • 遍历数组 numbers
  • 将偶数 push 到evenNumbers数组中
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];var evenNumbers = [];for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { evenNumbers.push(numbers[i]); }}console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

我们还可以使用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的那些值。

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

var filterArray = function(x, coll) { var resultArray = []; for (var i = 0; i < coll.length; i++) { if (coll[i] < x) { resultArray.push(coll[i]); } } return resultArray;}console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]

声明式方式

对于上面的总是,我们更想要一种更声明性的方法来解决这个问题,如下所示:

function smaller(number) { return number < this;}function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x);}let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];filterArray(3, numbers); // [2, 1, 0]

在smaller的函数中使用 this,一开始看起来有点奇怪,但是很容易理解。

filter函数中的第二个参数表示上面 this, 也就是 x 值。

我们也可以用map方法做到这一点。想象一下,有一组信息

let people = [ { name: "TK

转载地址:https://blog.csdn.net/weixin_40004659/article/details/111016043 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:gis可达性分析步骤_【极简】城乡规划新技术GIS应用厦大李渊(1)
下一篇:今天吃什么随机网页_灵魂拷问:今天在西昌你想吃什么?

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年04月22日 08时48分35秒