函数式JavaScript(2):如何打造“函数式”编程语言?

JavaScript是函数式编程语言吗?

没有神奇的公式能够判定一种语言是不是“函数式”语言。有些语言很明显就是函数式的,就像另外一些语言很明显不是函数式的,但是有大量语言的是模棱两可的中间派。

于是这里给出一些常用的、重要的函数式语言的“配料”(JavaScript能实现用粗体标志)

  • 函数是“第一等公民”
  • 函数能够返回函数
  • 词法上支持闭包
  • 函数要“纯粹”
  • 可靠递归
  • 没有变异状态

这决不是一个排它的列表,但是我们至少要逐个讨论Javascript中最重要的三个特性,它们支撑我们可以用函数式的方式来编写程序。

让我们逐个详细的了解下:

函数是“第一等公民”

这条可能是在所有的配料中最明显的,并且可能是在很多现代编程语言中最常见到的。

在JavaScript局部变量是通过var关键字来定义的。

JavaScript中把函数以局部变量的方式定义是非常容易做到的。

这些都是事实,变量:变量add和变量even通过被赋值的方式,与函数定义建立引用关系,这种引用关系是在任何时候如果需要是可以被改变的。

当然,这没有什么特别的。但是成为“第一等公民”这个重要的特性使得我们能够把函数以参数的方式传递给另一个函数。举个例子:

这是一个函数,他接受了一个二元函数f,和两个参数a,b,然后调用这个二元函数f,该二元函数f以a、b为输入参数。

这样做看起来有点笨拙,但是当把接下来的函数式编程“配料”合并考虑的时候,牛叉之处就显而易见了…

函数能返回函数(换个说法“高阶函数”)

事情开始变的酷起来。尽管开始比较简单。函数最终以新的函数作为返回值。举个例子:

这个函数(applyFirst)接受一个二元函数作为其中一个参数,可以把第一个参数(即二元函数)看作是这个applyFirst函数的“部分操作”,然后返回一个一元(一个参数)函数,该一元函数被调用的时候返回外部函数的第一个参数(f)的二元函数f(a, b)。返回两个参数的二元函数。

让我们再谈谈一些函数,例如mult(乘法)函数:

依循mult(乘法)函数的逻辑,我们可以写一个新的函数double(乘方):

这就是偏函数,在FP中经常会用到。(译注:FP全名为 Functional Programming 函数式程序设计 )

我们当然可以像applyFirst那样定义函数:

现在,我想要一个double(乘方)函数,我们换种方式做:

这种方式被称作“函数柯里化”。有点类似partial application(偏函数应用),但是更强大一点。

在这个系列文章的后半部分会详细讨论柯里化。

准确的说,函数式编程之所以强大,大部分因于此。简单和易理解的函数成为我们构筑软件的基础构件。当拥有高水平的组织能力、很少重用的逻辑的时候,函数能够被组合和混合在一起用来表达出更复杂的行为。

高阶函数可以得到的乐趣更多。让我们看两个例子:

翻转二元函数参数顺序

创建一个组合了其他函数的函数

这个例子创建了一个实用的函数,我们可以使用它来记录下每次函数调用。

词法闭包+作用域

我深信理解如何有效利用闭包和作用域是成为一个伟大JavaScript开发者的关键。
那么…什么是闭包?

可能需要个例子。

一旦createCounter函数被调用,变量count就被分配一个新的内存区域。然后,返回一个函数,这个函数持有对变量count的引用,并且每次调用的时候执行count加1操作。

注意从createCounter函数的作用域之外,我们是没有办法直接操作count的值。Counter1和Counter2函数可以操作各自的count变量的副本,但是只有在这种非

常具体的方式操作count(自增1)才是被支持的。

在JavaScript,作用域的边界检查只在函数被声明的时候。逐个函数,并且仅仅逐个函数,拥有它们各自的作用域表。(注:在ECMAScript 6中不再是这样,因为let的引入)

一些进一步的例子来证明这论点:

关于作用域还有一些重要的事情需要考虑。例如,我们需要创建一个函数,接受一个数字(0-9),返回该数字相应的英文名称。

简单点,有人会这样写:

但是缺点是,names定义在了全局作用域,可能会意外的被修改,这样可能致使digit_name1函数所返回的结果不正确。
那么,这样写:

这次把names数组定义成函数digit_name2局部变量.这个函数远离了意外风险,但是带来了性能损失,由于每次digit_name2被调用的时候,都将重新为names数组定义和分配空间。换个例子如果names是个非常大的数组,或者可能digit_name2函数在一个循环中被调用多次,这时候性能影响将非常明显。

这时候我们面临第三个选择。这里我们实现立即调用的函数表达式,仅仅实例化names变量一次,然后返回digit_name3函数,在 IIFE (Immediately-Invoked-Function-Expression 立即执行表达式)的闭包函数持有names变量的引用。
这个方案兼具前两个的优点,回避了缺点。搞定!这是一个常用的模式用来创建一个不可被外部环境修改“private”(私有)状态。

2014-4-25更新:JavaScript不具备的配料该怎么办呢?

我们已经细致的讨论了JavaScript具备的“配料”,但那三条我曾提到的JS不具备的咋办?

  • 函数要“纯粹”
  • 可靠递归
  • 没有变异状态

纯粹函数 和 无变异状态

纯粹函数具备“没有副作用”的特性,是没有变异状态的另一种说法。这使得你的代码更容易被分析、测试、并行化,等等…
尽管你可以编写代码让变量不被改变,但这不如拥有不会发生的保证更有用。

可靠递归

尽管以可以以递归的方式调用JavaScript代码,解释程序不以尾递归优化方式编译。这意味着如果不小心点儿,任何递归方式定义的函数将很快导致堆栈溢出错误。

这使得在某些情况下使用for/while循环是必要的。(这显然就不那么优雅了)

能更好点吗?接下来干点儿什么?

JavaScript是极其灵活的语言。然而,这篇文章中的大多数例子在实践中会很少用到。

举个例子,curry2这个函数非常有用但它仅仅适用于二元函数。大多数的函数式编程语言都在语法上内置了柯里化,但是JavaScript并没有。

当然了还是有办法的:JavaScript提供了一些非常有用的函数,像Function.prototype.call, Function.prototype.bind, 和与众不同的 arguments 对象。能够利用这些函数实现一些强大的函数式编程所约定的。(像柯里化)

我将在这个系列接下来的文章中更详细的讨论这些东西:

接下来 -> 第三部分:.apply()、.call()以及arguments对象

更多内容预告:

【译注1】:所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为其他的函数的返回值。
【译注2】:什么是纯粹?先看另外一个词,”副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。保持计算过程的纯粹性。
【译注3】:Currying:因为是美国数理逻辑学家哈斯凯尔•加里(Haskell Curry)发明了这种函数使用技巧,所以这样用法就以他的名字命名为Currying,中文翻译为“柯里化”。我感觉很多人都对函数加里化(Currying)和偏函数应用(Partial Application)之间的区别搞不清楚,尤其是在相似的上下文环境中它们同时出现的时候。偏函数解决这样的问题:如果我们有函数是多个参数的,我们希望能固定其中某几个参数的值。
【译注4】:各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有父函数内部的子函数才能读取父函数的局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。》这就是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

:-)

收藏 评论

关于作者:如是如是

新浪微博:@ETHING 个人主页 · 我的文章

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部