博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JavaScript函数式编程简介
阅读量:2526 次
发布时间:2019-05-11

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

当Brendan Eich在1995年创建JavaScript时,他打算执行 。 Scheme是Lisp的方言,是一种功能编程语言。 当Eich被告知新语言应该是Java的脚本语言伴侣时,情况发生了变化。 Eich最终选择了一种具有C样式语法(如Java)但具有一流功能的语言。 从技术上讲,Java在版本8之前没有一流的功能,但是您可以使用匿名类模拟一流的功能。 这些一流的功能使使用JavaScript进行功能编程成为可能。

JavaScript是一种多范式语言,可让您自由地混合和匹配面向对象,过程和功能的范式。 最近,函数式编程的趋势正在增长。 在和框架中,您实际上将通过使用不可变的数据结构来提高性能。 不变性是函数式编程的核心宗旨。 它与纯函数一起使推理和调试程序更加容易。 用函数替换过程循环可以提高程序的可读性,并使其更美观。 总体而言,函数式编程有很多优点。

什么不是函数式编程

在讨论什么是函数编程之前,让我们先谈谈它不是什么。 实际上,让我们讨论一下您应该抛弃的所有语言结构(再见,老朋友):

  • 循环
    • 做...而
    • 对于
    • 对于...的
    • 为...在
  • 使用varlet的变量声明
  • 虚函数
  • 对象突变(例如: ox = 5;
  • 数组变异器方法
    • copyWithin
    • 流行音乐
    • 逆转
    • 转移
    • 分类
    • 拼接
    • 不动
  • 映射变异器方法
    • 明确
    • 删除
  • 设置变异器方法
    • 明确
    • 删除

没有这些功能,您怎么可能编程? 这正是我们接下来几节将要探讨的内容。

纯功能

仅仅因为您的程序包含函数,并不一定意味着您正在执行函数编程。 函数式编程区分纯函数和不纯函数。 它鼓励您编写纯函数。 纯函数必须满足以下两个属性:

  • 引用透明性:函数始终为相同的参数提供相同的返回值。 这意味着该功能不能依赖任何可变状态。
  • 无副作用:该功能不会引起任何副作用。 副作用可能包括I / O(例如,写入控制台或日志文件),修改可变对象,重新分配变量等。

让我们用一些例子来说明。 首先, 乘法函数是纯函数的一个示例。 对于相同的输入,它总是返回相同的输出,并且不会引起任何副作用。

以下是不纯函数的示例。 canRide函数取决于捕获的heightRequirement变量。 捕获的变量不一定会使函数不纯,但可变的(或可重新分配的)确实可以。 在这种情况下,它是使用let声明的,这意味着可以重新分配它。 乘法功能是不纯的,因为它会通过登录到控制台而产生副作用。

以下列表包含JavaScript中一些不纯的内置函数。 您能否说明两个属性中的每个属性都不满足?

  • console.log
  • element.addEventListener
  • 数学随机
  • 现在日期
  • $ .ajax (其中$ ==您选择的Ajax库)

生活在一个完美的世界中,我们的所有功能都是纯净的,这会很好,但是如您从上面的清单中可以看出,任何有意义的程序都将包含不纯函数。 大多数时候,我们将需要进行Ajax调用,检查当前日期或获取随机数。 一个好的经验法则是遵循80/20规则:80%的函数应该是纯函数,而其余20%的函数必不可少。

纯函数有几个好处:

  • 由于它们不依赖于可变状态,因此它们更易于推理和调试。
  • 返回值可以缓存或“存储”,以免将来再次计算它。
  • 它们更容易测试,因为不需要模拟任何依赖项(例如日志记录,Ajax,数据库等)。

如果您正在编写或使用的函数是无效的(即,它没有返回值),则表明它是不纯的。 如果该函数没有返回值,则说明它是无操作的,或者会引起一些副作用。 同样,如果您调用一个函数但不使用其返回值,同样,您可能依赖于它产生一些副作用,并且它是一个不纯函数。

不变性

让我们回到捕获变量的概念。 上面,我们看了canRide函数。 我们认为这是一个不纯函数,因为可以重新分配heightRequirement 。 这是一个人为设计的示例,说明如何将其重新分配以产生不可预测的结果:

让我再次强调,捕获的变量不一定会使函数不纯。 我们可以重写canRide函数,使其简单,只需更改声明heightRequirement变量的方式即可。

const声明变量意味着没有机会重新分配它。 如果尝试重新分配它,则运行时引擎将引发错误; 但是,如果我们有一个存储所有“常数”的对象而不是一个简单的数字怎么办?

我们使用了const,因此无法重新分配变量,但是仍然存在问题。 该对象可以被突变。 如下代码所示,要获得真正的不变性,您需要防止重新分配变量,并且还需要不变的数据结构。 JavaScript语言为我们提供了Object.freeze方法,以防止对象发生突变。

不变性适用于所有数据结构,包括数组,映射和集合。 这意味着我们无法调用诸如array.prototype.push之类的增变方法,因为这会修改现有的数组。 除了将一项推入现有阵列之外,我们还可以创建一个具有与原始阵列相同的所有项的新阵列,再加上一个附加项。 实际上,每个mutator方法都可以替换为一个函数,该函数返回具有所需更改的新数组。

使用或时也会发生同样的事情。 我们可以通过返回具有所需更改的新MapSet来避免使用mutator方法。

我想补充一点,如果您正在使用TypeScript(我是TypeScript的忠实拥护者),那么可以使用Readonly <T>ReadonlyArray <T>ReadonlyMap <K,V>ReadonlySet <T>接口来如果您尝试对任何这些对象进行突变,则会出现编译时错误。 如果您在对象文字或数组上调用Object.freeze ,则编译器将自动推断出它是只读的。 由于Maps和Sets是内部表示的,因此在这些数据结构上调用Object.freeze不能正常工作。 但是告诉编译器您希望它们是只读的很容易。

TypeScript Readonly Interfaces

TypeScript只读接口

好的,所以我们可以创建新对象而不是对现有对象进行突变,但这不会对性能产生不利影响吗? 是的,它可以。 确保在您自己的应用中进行性能测试。 如果需要提高性能,请考虑使用 。 Immutable.js使用实现 , , , 和其他 。 这与功能编程语言(例如Clojure和Scala)内部使用的技术相同。

功能组成

还记得回到高中时,您学到的东西看起来像(f∘g)(x)吗? 记得想过:“我什么时候会使用它?” 好吧,现在你是。 准备? f∘g读为“ 由g组成的f ”。 有两种等效的思维方式,如此恒等式所示: (f∘g)(x)= f(g(x)) 。 您可以将f∘g视为单个函数,也可以将其视为调用函数g的结果,然后获取其输出并将其传递给f 。 请注意,函数是从右到左应用的,也就是说,我们执行g ,后跟f

关于函数组成的两个重要点:

  1. 我们可以组成任意数量的函数(不限于两个)。
  2. 组成函数的一种方法就是简单地从一个函数获取输出并将其传递给下一个函数(即f(g(x)) )。

有资料库,如和提供合成功能的更优雅的方式。 代替简单地将返回值从一个函数传递到下一个函数,我们可以从更数学的角度处理函数组成。 我们可以创建一个由其他函数组成的复合函数(即(f∘g)(x) )。

好的,所以我们可以使用JavaScript进行函数组合。 有什么大不了的? 好吧,如果您真正从事函数式编程,那么理想情况下,您的整个程序就是函数组合。 您的代码中不会有循环( forfor ... offor ... inwhiledo )。 无(句号)。 但这是不可能的,你说! 不是这样 这就引出了下两个主题:递归和高阶函数。

递归

假设您要实现一个计算数字阶乘的函数。 让我们回想一下数学中阶乘的定义:

n! = n *(n-1)*(n-2)* ... * 1

也就是说, n! 是从n1的所有整数的乘积。 我们可以编写一个循环,为我们轻松地进行计算。

请注意, 乘积i在循环中反复被重新分配。 这是解决问题的标准程序方法。 我们如何使用功能性方法解决它? 我们需要消除循环,并确保没有任何变量被重新分配。 递归是功能性程序员工具栏中最强大的工具之一。 递归要求我们将整个问题分解为类似于整个问题的子问题。

计算阶乘是一个很好的例子。 计算n! ,我们只需要取n并将其乘以所有较小的整数即可。 这就是说同一句话:

n! = n *(n-1)!

哈! 我们找到了一个要解决的子问题(n-1)! 它类似于整体问题n! 。 还有另一件事要注意:基本情况。 基本情况告诉我们何时停止递归。 如果没有基本情况,那么递归将永远持续下去。 实际上,如果递归调用过多,则会出现堆栈溢出错误。

阶乘函数的基本情况是什么? 起初,您可能会认为这是n == 1的时候,但是由于一些 ,这是n == 0的时候。 0! 定义为1 。 考虑到这些信息,让我们编写一个递归阶乘函数。

好的,让我们计算recursiveFactorial(20000) ,因为……好吧,为什么不呢! 当我们这样做时,我们得到:

Stack overflow error

堆栈溢出错误

那么这是怎么回事? 我们有一个堆栈溢出错误! 这不是因为无限递归。 我们知道我们处理了基本情况(即n === 0 )。 这是因为浏览器具有有限的堆栈,而我们已经超出了它。 每次对recursiveFactorial的调用都会导致将新帧放入堆栈中。 我们可以将堆栈可视化为一组彼此堆叠的盒子。 每次调用recursiveFactorial时,都会在顶部添加一个新框。 下图显示了计算recursiveFactorial(3)时堆栈的样式化版本。 请注意,在实际堆栈中,最上面的框架将存储执行后应返回的内存地址,但是我选择使用变量r来描述返回值。 我这样做是因为JavaScript开发人员通常不需要考虑内存地址。

The stack for recursively calculating 3! (three factorial)

用于递归计算的堆栈3! (三阶)

您可以想象n = 20000的堆栈要高得多。 我们有什么可以做的吗? 事实证明,是的,我们可以做一些事情。 作为ES2015 (aka ES6 )规范的一部分,添加了优化以解决此问题。 这称为适当的尾部调用优化 (PTC)。 如果递归函数做的最后一件事是调用自身并返回结果,则它允许浏览器忽略或忽略堆栈帧。 实际上,优化也适用于相互递归的函数,但是为了简单起见,我们仅关注单个递归函数。

您会在上面的堆栈中注意到,在递归函数调用之后,仍然需要进行其他计算(即n * r )。 这意味着浏览器无法使用PTC对其进行优化; 但是,我们可以用这种方式重写函数,以便最后一步是递归调用。 这样做的一个技巧是将中间结果(在本例中为product )作为参数传递给函数。

现在,我们在计算阶乘(3)时可视化优化堆栈。 如下图所示,在这种情况下,堆栈永远不会超过两个帧。 原因是我们正在将所有必要的信息(即product )传递给递归函数。 因此,在更新产品之后,浏览器可以丢弃该堆栈框架。 您会在此图中注意到,每次顶部框架掉落并变为底部框架时,前一个底部框架都会被抛出。 不再需要。

The optimized stack for recursively calculating 3! (three factorial) using PTC

用于递归计算3的优化堆栈! (三阶)使用PTC

现在,在您选择的浏览器中运行该代码,并假设您在Safari中运行了该代码,那么您将得到答案,即Infinity (该数字比JavaScript中的最大可表示数字高)。 但是我们没有收到堆栈溢出错误,所以很好! 现在其他所有浏览器呢? 事实证明,Safari是唯一实现PTC的浏览器,并且可能是唯一实现PTC的浏览器。 请参阅以下兼容性表:

PTC compatibility

PTC兼容性

其他浏览器提出了一种竞争性标准,称为 (STC)。 “语法”意味着您将必须通过新语法指定您希望函数参与尾调用优化。 尽管还没有广泛的浏览器支持,但是编写递归函数仍然是一个好主意,这样它们就可以在(无论何时)到来时进行尾部调用优化。

高阶函数

我们已经知道JavaScript具有一流的功能,可以像传递其他任何值一样传递这些功能。 因此,我们可以将一个函数传递给另一个函数也就不足为奇了。 我们也可以从一个函数返回一个函数。 瞧! 我们具有高阶函数。 您可能已经熟悉Array.prototype上存在的几个高阶函数。 例如, filtermapreduce等等。 考虑高阶函数的一种方法是:该函数接受(通常称为)回调函数。 让我们来看一个使用内置高阶函数的示例:

注意,我们在数组对象上调用方法,这是面向对象编程的特征。 如果我们想使它更能代表函数式编程,可以使用Ramda或lodash / fp提供的函数。 我们还可以使用在上一节中探讨的功能组合。 请注意,如果我们使用R.compose ,我们将需要颠倒函数的顺序,因为它将函数从右到左(即从下到上)应用; 但是,如上例所示,如果要从左到右(即从上到下)应用它们,则可以使用R.pipe 。 这两个示例都在下面使用Ramda给出。 请注意,Ramda具有一个均值函数,可以代替reduce使用

函数式编程方法的优势在于,它可以将数据(即, 车辆 )与逻辑(即,函数filtermapreduce )清楚地分开。 与将对象形式的数据和函数与方法混合在一起的面向对象的代码形成对比。

咖喱

非正式地, 柯里化是一个过程,该过程接受一个接受n个参数的函数,然后将其变成每个接受一个参数的n个函数。 函数的多样性是它接受的参数数量。 接受单个参数的函数为一元 ,两个参数为二进制 ,三个参数为三进制n个参数为n-ary 。 因此,我们可以将currying定义为采用n元函数并将其转换为n元函数的过程。 让我们从一个简单的示例开始,该函数采用两个向量的点积。 回想一下线性代数,两个向量[a,b,c][x,y,z]的点积等于ax + by + cz

函数是二进制的,因为它接受两个参数。 但是,我们可以将其手动转换为两个一元函数,如以下代码示例所示。 注意curriedDot是如何接受向量并返回另一个然后接受第二向量的一元函数的一元函数。

对我们来说幸运的是,我们不必手动将每个函数转换为咖喱形式。 库包括和具有的功能,将做到这一点对我们来说。 实际上,它们是混合的curring类型,您可以一次调用一个参数,也可以像原始方法一样继续一次传递所有参数。

Ramda和lodash都允许您“跳过”参数并在以后指定它。 他们使用占位符执行此操作。 因为点积是可交换的,所以我们将向量传递到函数的顺序不会有任何区别。 让我们使用一个不同的示例来说明使用占位符。 Ramda使用双下划线作为占位符。

在完成currying主题之前的最后一点是部分应用。 尽管部分应用和欺骗确实是独立的概念,但它们经常并存。 即使没有提供任何参数,curried函数仍然是curried函数。 另一方面,部分应用是指给函数提供了一些但不是全部参数。 咖喱通常用于部分应用,但这不是唯一的方法。

JavaScript语言具有内置的机制,可以进行部分应用而不会引起麻烦。 这是使用方法完成的。 此方法的一个特质是它要求您传入this的值作为第一个参数。 如果您不进行面向对象的编程,则可以通过传入null来有效地忽略

结语

希望您喜欢与我一起探索JavaScript函数编程! 对于某些人来说,这可能是一个全新的编程范例,但是我希望您能给它一个机会。 我认为您会发现您的程序更易于阅读和调试。 不变性还将使您能够利用Angular和React中的性能优化。

本文基于Matt的OpenWest演讲“ 入门”。 将于2017年7月12日至15日在犹他州盐湖城举行。

翻译自:

转载地址:http://kwpzd.baihongyu.com/

你可能感兴趣的文章
presumably用法
查看>>
stick用法
查看>>
TeamWork#3,Week5,The First Meeting of Our Team
查看>>
获取或者设置非行间样式方法二
查看>>
隔日随笔样式测试
查看>>
ICMP(网际控制报文协议)
查看>>
Sonar安装和常见问题解决
查看>>
[蓝桥杯]PREV-12.历届试题_危险系数
查看>>
redis常用命令
查看>>
第一周例行报告及作业汇总
查看>>
SQL2043N 与 linux的randomize_va_space特性
查看>>
树莓派使用无线网卡上网相关命令
查看>>
优秀架构师是怎么炼成的?
查看>>
Hibernate的CRUD
查看>>
StringBuilder和StringBuffer的区别
查看>>
基于Extjs+SpringMVC+MyBatis+Oracle的B/S信息系统简化开发思路
查看>>
【python】字典的嵌套
查看>>
微信运营:必须收藏的101条万能微信标题公式
查看>>
【XLL 框架库函数】 TempMissing/TempMissing12
查看>>
利用VS自带发布功能实现web项目快速部署
查看>>