【译】JavaScript的函数式编程和面向对象编程

原创 老姚 随笔 我也来说说系列 1495阅读 2017-05-04 23:43:57 举报

声明:本文是Dan Mantyla的《Functional Programming in JavaScript》一书的最后一章。翻译水平有限,请多多包涵。“FP”是“函数式编程”的简写,“OOP”是“面向对象编程”的简写。二者都是编程范式,为了方便,文中使用的都是简写形式。纵观全章主要讲的还是OOP,其中涉及到了继承、策略模式、mixin的实现。翻译的过程中,见到有错的地方,都已改过。尤其是mixin那块儿,估计作者都没有运行自己的代码。

你会经常听说JavaScript是一门空白的语言,其中“空白的”是指,它可以是面向对象的、可以是函数式的,或者说是多用途的。本书一直重点集中在,JavaScript是一门函数式语言,也花了大量篇幅来证明确实如此。但真相是,JavaScript是一门多用途的语言,意味这它完全有能力支持多种编程风格。像Python和F#一样,JavaScript是支持多范式的。但和那些语言有一点不同,JavaScript的面向对象编程这一块儿是基于原型的,而其他多用途的语言是基于类的。

在这最后一章里,我们会把JavaScript的函数式编程(FP)和面向对象编程(OOP)联系起来,进而看看这两种编程范式是如何相得益彰以及和平共处的。本章讨论的主题包括如下:

  • JavaScript如何既是FP又是OOP的?
  • JavaScript的OOP-使用原型
  • JavaScript中如何混合使用FP和OOP?
  • 函数式继承
  • 函数式混入
    写出更好的代码是目标,而FP和OOP只是为了达到这一目标的手段而已。
1.JavaScript-多范式的语言

如果说OOP是把所有的变量都当做对象,而FP是把所有的函数当作变量,那么就不能把函数当作对象吗?在JavaScript中,是可以的。
但是,“FP是把所有函数当作变量”,这一说法某种意义上是不准确的。一个更好的说法是,FP是把一切都当作“值”,尤其是函数。
一个更好的方式依旧来描述FP可能是,称其为声明式的。去表达解决问题的必要计算逻辑时,声明式编程不依赖编程的命令分支结构。FP告诉计算机问题是什么,而不是如何解决问题的处理程序。
同时,OOP源自于命令式编程风格的:告诉计算机按部就班地如何解决问题的指令。OOP要求这些计算方法和被处理的数据组成一个单元:对象。只有通过使用对象的方法才能访问数据。

那么,这两种风格如何才能整合到一起呢?
-对象方法的代码通常写成命令式风格的。但假如写成函数式风格又会怎样呢?毕竟OOP是不会把不变数据和高阶函数拒之门外的。
-也许混合使用这二者的一个更纯粹的方式,就是把对象同时既当作函数,又当作传统上的基于类的对象。(译:强调对象中的方法?)
-也许我们可以在面向对象应用程序中引入FP中的想法,比如promise和递归。
-OOP涵盖的主题,譬如封装、多态和抽象。而FP也是如此,只是表现形式不一样罢了。因此,我们或许又可以在函数式应用程序中引进OOP的想法。
重点在于:OOP和FP可以混合使用,而且还有多种方式实现。它们并不互相排斥。

2.JavaScript面向对象的实现-使用原型

JavaScript是一门class-less的语言。并不是说它与其他语言那样相比,不时髦或者低人一等。“class-less”是说它没有面向对象语言的那种类结构,它使用原型来继承。
尽管这点通常会使拥有C++或Java背景的程序设计师困惑,但基于原型的继承比传统继承更富有表现力。下面是C++和JavaScript不同点的简单对比:

1

2.1继承

在详细探究之前,先确保我们充分理解OOP中的继承概念。基于类的继承可以用下面的伪代码来演示:
javascript 代码

其中Polygon类是继承于它的其他类父类。它只定义了一个成员变量,多边形的边数,numSides,并在init函数中被设置。Rectangle子类继承于Polygon类,并新增了两个成员变量,length和width,和一个方法,getArea。Rectangle类不需要定义变量numSides,因为此变量已经在其继承的父类中定义过了,同时它覆盖了init函数。Square类的存在继续拓展了继承链,它继承了Rectangle类的getArea方法。简单通过覆盖init方法来使length和width相等,getArea方法就不需修改了,代码也省了。

在一个传统的OOP语言中,继承就是上面那些东西。如果我们需要给所有类添加个color属性,只需把它加入到Polygon类中,而不需要修改其他子类。

2.2JavaScript的原型链

JavaScript中的继承实际上是原型。每一个对象都有一个内部属性被称为是它的原型,此内部属性是另一个对象的引用。而这另一个对象也有它自己的原型。这种模式重复,直到某个对象的原型未定义为止。这就是广为熟知的原型链,也是JavaScript继承如何工作的原理。下图表明了JavaScript的继承:

2
当查找一个对象的某个函数定义时,JavaScript会沿着其原型链进行查找,直到找到有同样的名字的第一个定义为止。因此只需在子类的原型上提供一个新的定义,就可以轻松实现覆盖。

2.3JavaScript中的继承和Object.create方法

就像有多种方式在JavaScript中创建对象那样,也有多种方式模拟基于类的经典继承。一个首选方式就是使用Object.create方法。
javascript 代码

这种写法对很多人来说,看起来不太寻常,只需少量练习,就会变得很亲近。prototype这个单词是用来获取对象的内部属性[[Prototype]],每个对象都有这个属性的。Object.create方法用来得到一个以指定对象为其原型的新对象,以便来继承。通过这种方式,JavaScript就实现了经典继承。

我们在第5章(范畴论)构建Maybe类时已经见过这种继承。下面是Maybe、None和Just类间的继承:
javascript 代码

这点表明JavaScript中的类继承是FP的一个促成因素。

一个常见的错误是把一个构造器传给Object.create,而不是一个对象来作为原型。之所以会有这个问题是因为不去调用子类中继承来的方法时,浏览器是不会抛出异常的。
javascript 代码

3.在JavaScript中混合使用FP和OOP

OOP数十年来一直是主流的编程范式。全世界都会计算机科学101课程中传授OOP,而FP不然。软件架构师用OOP设计应用程序,而FP不然。尤其因为OOP更容易使想法概念化,让人轻松写出代码,更加剧了此现象。

因此,除非你能说服你的老板应用程序需要完全函数式的,否则我们只能在一个OOP的世界里使用FP。这一节将会探索这种做法。

3.1函数式继承

或许在JavaScript应用程序中应用FP最简单的方式,就是在OOP原则(比如继承)里写函数式风格的代码。

为了探讨可行性,我们建立一个计算产品价格的简单应用程序。首先,我们需要一些产品类:
javascript 代码

我们可以在Store中如下地组织它们:
javascript 代码

calculateTotal方法中使用了数组的reduce方法用来计算各产品的价格总和。

这么写是可以的,但如果我们需要一个动态的方式来计算价格时,又会怎样呢?为此,我们可以转向一个概念,策略模式。

3.1.1策略模式

策略模式是一种方式定义了一系列相互可替换的算法。OOP程序设计师经常用它来操作运行时行为,但是他基于几个FP原则的:
[quote]-逻辑和数据的分离
-函数的组合
-函数是一等对象[/quote]
同时也基于几个OOP原则的:
[quote]-封装
-继承[/quote]

在我们计算产品价格的应用程序中,像先前说的那样,假如说我们想给一些消费者提供优惠,那么不得不变动计算价格方法来反映这一点。
因此,让我们创造几个消费者类
javascript 代码

每一个Customer类都封装了算法。现在我们需要在Store类中调用Customer类的calculateTotal方法。
javascript 代码

Customer类做计算,Product类保存数据(价格),而Store类维系上下文。这就做到了高内聚和混合使用了OOP和FP。JavaScript高水平的表现力使这点成为可能,并相当简单。

3.2混入(mixin)

一句话说,mixin就是类,可以允许其他类使用它们的方法。其方法旨在单独地被其他类使用,mixin类本身不该实例化的。这有助于避免继承的不确定性。而且mixin是混合使用FP和OOP的重要方式。

在不同语言中实现mixin的方式也不同。多亏了JavaScript的灵活性和表现力,可以通过只有方法的对象来实现mixin。尽管mixin可以被定义为函数,比如var mixin = function() {...},为了代码结构化原则,最好把它们定义成对象字面量,比如var mixin = {...}。这样就会帮助我们更好地好地区分类和mixin。毕竟,mixin应该被当成处理程序而不是对象。

让我们先声明一些mixin作为开始。我们会拓展上节Store应用程序,使用mixin对类进行拓展。
javascript 代码

当然,我们并不局限于此。更多mixin可以添加进来,比如颜色或布料等。我们不得不稍微重写下我们的Shirt类:
javascript 代码

现在我们做好使用mixin的准备了。

3.2.1经典混入

你可能会好奇,这些mixin是怎么会和类混合在一起的。经典的实现方式是通过把minin里的函数拷贝到目标上。可以对Function原型进行如下的拓展:
javascript 代码

然后,现在minxin可以通过如下的方式添加进去:
javascript 代码

然而,这会有一个很大的问题。当我们再一次计算p1的价格时,结果是15,是large那项的价格。本该是small那项的价格。
javascript 代码

问题在于每次添加mixin时,Shirt的prototype.getPrice方法都被重写;这一点并不很函数式,也不是我们想要的。

3.2.2函数式混入

还有另外一种方式使用mixin,更与FP相匹配。

不通过拷贝mixin的方法到目标对象的那种方式,我们创建一个新的对象,是目标对象的副本,并把混入的方法添加进去。如何克隆对象?这可以通过创建一个新的对象继承它来做到。这种方式这里称为plusMixin。
javascript 代码

有趣的部分来了。现在我们可以通过mixin让应用很函数式,可以创建产品和mixin所有的可能组合。
javascript 代码

为个更加面向对象,我们可以重写Store,给Store类(而不是产品类)添加了一个展示价格函数,保持逻辑和数据分离。
javascript 代码

接下来要创建一个Store对象,然后调用它的displayProducts方法,来展示产品和价格列表。
html 代码

下面的代码需要被添加到产品类中:
javascript 代码

这样,我们就得到了一个高度模块化和可拓展的电子商务网站应用程序了。添加新种类衬衫相当容易,只需定义一个新的Shirt子类并添加到Store类中的数组productClasses中即可。添加新的mixin也是一样。假如现在你的boss说,“嘿,现在有一种新型的衬衫和外套,各种标准颜色都有的,在你回家之前需要添加到我们的网站上。”我们可以确信无疑,自己不会在公司停留太长时间。
译:完整案例如下:
html 代码

小结

JavaScript富有很高表现力。这一点使混合使用FP和OOP称为可能。现代JavaScript不单单是OOP或FP,它是二者的混合物。譬如策略模式和混入的概念,对于JavaScript原型结构来说是完美的,表明JavaScript的FP和OOP如今有着等量的最佳实践。0

如果你成从本书中只能得到一个东西,我希望它是如何把FP技巧应用到真实世界里的应用程序中。而本章向你展示了怎么正确地做到这点。

评论 ( 12 )
最新评论
老姚 11F 2017-05-09 15:14:48 12F

谢谢支持,另外刚开了一个“递归”系列,敬请关注。

ququ 9F 2017-05-09 15:03:12 11F

讲的很明白,是到目前为止看到javascript讲的最透彻 以及最实用的

老姚 9F 2017-05-09 11:20:51 10F

这招好使的,比如阮一峰的es6之前看了很多遍,看了之后,感觉都会,不过,过段时间后又会感觉模棱两可。然后我把自己说不清的知识点所在章节从头到尾敲。敲完之后,效果确实比之前只是看的方式要好很多,理解得更透彻。这确实需要花时间的,不过一些重要知识就得需要这种精读过程的。

老姚 8F 2017-05-09 11:12:19 9F

,某种程度也说明笔记写得不是那么好。敲别人的代码就是放缓思路的过程,允许自己有时间充分思考。

ququ 2017-05-09 10:34:10 8F

“老姚笔记的代码都敲一遍”

,不一定什么时候敲另一段代码的时候就理解了

th7583362 2017-05-08 10:27:20 7F

只能理解部分

老姚 5F 2017-05-07 08:42:07 6F

众成上面应该没有书籍翻译吧。只有文章,因为还要原文贴个链接的。

知行合一 2F 2017-05-06 20:54:05 5F

可以投稿到众成翻译。

Oo╭(╯ε╰)╮o 2017-05-05 15:12:40 4F

好深,理解不透

xunxincao 2F 2017-05-05 13:36:43 3F

其实我没理解多少,,,不能给出太多建议,

老姚 1F 2017-05-05 12:34:11 2F

毕竟是一章内容,其实能写成3篇文章。帮着看看,哪块儿翻译得不通顺。

xunxincao 2017-05-05 11:44:30 1F

这篇深度有点偏深了