JavaScript函数式编程

978-7-115-39060-8
作者: 【美】Michael Fogus(佛格斯)
译者: 欧阳继超王妮
编辑: 陈冀康

图书目录:

详情

本书介绍了如何通过JavaScript的函数式编程风格,来发挥这种语言最好的编程实践。本书的每个主题都有针对性很强的示例, 作者将会Underscore.js库及其惯用法为例子进行讲解和深刻剖析。本书内容包括闭包、应用性编程、高级函数、纯粹性、柯里化等核心和高级话题。

图书摘要

版权信息

书名:JavaScript函数式编程

ISBN:978-7-115-39060-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Michael Fogus

  译    欧阳继超 王 妮

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2013 by O’Reilly Media.Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2015. Authorized translation of the English edition, 2013 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体版由O’Reilly Media, Inc. 授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


JavaScript是近年来非常受瞩目的一门编程语言,它既支持面向对象编程,也支持函数式编程。本书专门介绍JavaScript函数式编程的特性。

全书共9章,分别介绍了JavaScript函数式编程、一等函数与Applicative编程、变量的作用域和闭包、高阶函数、由函数构建函数、递归、纯度和不变性以及更改政策、基于流的编程、无类编程。除此之外,附录中还介绍了其他和JavaScript函数式编程相关的知识。

本书内容全面,示例丰富,适合想要了解函数式编程的JavaScript程序员和学习JavaScript的函数式程序员阅读。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


这是一本令人兴奋的书。

尽管开始是想作为嵌入HTML文档中的精简版Java(Java-lite)的脚本语言,由此可以进行一点交互,但JavaScript却成为通用编程最灵活的语言之一。

你可以随意根据最适合你的特定方式来写JavaScript。在JavaScript中这样做比其他更刚性的语言更自然,究其原因,是支撑JavaScript的核心理念:更大程度上比面向对象语言如Ruby和Java扩展了一切都是对象(一切都是一个值)的理念。函数既是对象,也是值。任何对象都可以作为其他对象的原型(默认值)。函数只有一种,你可以随意使用它,可以作为一个纯函数、一个突变过程,或作为一个对象的方法。

JavaScript 可以但不会强制使用不同的编程风格。早期,我们倾向于把传统期望和“最佳”实践套用到JavaScript的学习中。当然,这会使得JavaScript很像没有类型的Java,或是将类型写到每个方法上面的注释里面。渐渐地,有人开始实验:在运行时生成函数,使用不可变的数据结构,创建不同于面向对象的设计模式,发现链式API的魔力,或是扩展内置原型来得到定制化的功能。

我最近特别热衷于使用函数式思路构建丰富的JavaScript应用。随着JavaScript从简单的表单验证和DOM动画到全功能的应用程序,JavaScript开始面临着各种特定问题,函数式也有了更有趣的舞台,如下所示。

本书正是探索这片领域的理想书籍。在书中的9章和附录里,友好的导游和疯狂的科学家Michael Fogus,将函数式编程层层剥开,让你一探究竟。一般很少有编程书籍能带给读者惊喜,但是这一本绝对可以。

好好享受吧!

Jeremy Ashkenas


我还记得当我第一次读到Douglas Crockford的《JavaScript精粹》时,我不仅从中学到了东西,而且Crockford只用了172页,就能带领读者避开JavaScript的各种问题,实在令人印象深刻。Crockford的书既简洁,又能让读者充分消化并从中受益。

接下来,你会发现,Michael Fogus给了我们一本类似Crockford的书。他吸收了Crockford以及其他前辈的中肯的意见,带我们深入函数式JavaScript编程的世界。我经常听说或看到(甚至我自己也会写到),JavaScript是一种函数式编程语言,但这种说法(包括我自己)都似乎难以领会。甚至Crock-ford也只用了一章阐述函数,像许多作家一样,都集中于JavaScript的对象支持。Fogus填补了这些重要的细节。

函数式编程从一开始就是计算机领域的重要的一部分,但它一直没有受到实践软件人员的广泛关注。但由于计算硬件速度和容量的不断提高,再加上我们的行业在创造并发、分布和大规模软件系统的需求不断扩大,函数式编程正迅速地普及。能获得这样的增长是因为对于开发者来说,函数式更容易推理、构建和维护。对函数式编程语言,如Scala、Clojure中,Erlang和Haskell的关注也达到了历史新高,并仍在增加,至今仍不见削减。

当你读完Michael具有深刻见解的JavaScript的函数式编程,你会对他所提供的信息的深度和广度留下深刻的印象。他首先会让事情保持简单,解释如何避免使用JavaScript功能强大的对象原型系统,而使用函数和“数据抽象”来建模类的方式。在之后的章节中,解释了函数式数据转换的简单模型可以产生复杂而高效的更高层次的抽象。我猜你会随着Fogus的每个章节对这种方式的层次深入感到惊讶。

大部分软件开发工作需要实用主义,好在Fogus也强调了这一项。如果不实用,就算有优雅、精致和简洁的代码,最终都是毫无意义的。这也是函数式编程隐藏在阴影中这么多年的很大一部分原因。Fogus通过帮助读者了解和评估与函数式编程相关的计算成本来解决这一问题。

当然,书就像软件,都少不了沟通。就像Crockford和Fogus的写作方式,既简短,又内容翔实,恰到好处。我没有夸大Michael的简洁和清晰的重要性,不然你会失去他所提供的令人难以置信的想法和见解的。你会发现,不仅Fogus所提供的方法和代码优雅,他表达的方式也一样优雅。

Steve Vinoski


Underscore.js(以下简称Underscore)是支持函数式编程的JavaScript库。Underscore网站是这样描述的:

Underscore为JavaScript提供了大量的函数式编程的支持,类似Prototype.js(或Ruby)的utility-belt,但没有扩展JavaScript内置对象。

“utility belt”指的是一套能帮助你解决很多常见问题的工具。

Underscore网站上有最新的版本。你可以从网站上下载并放入应用目录。

你可以像使用所有其他库一样在你的项目中使用Underscore。然而,有几点需要注意的是,首先,默认情况下,Underscore定义了一个包含其所有函数的全局对象。要调用一个Underscore的函数,只需要调用“_”里的方法,如下面的代码:

很简单吧?

但如果你已经定义一个全局_变量,事情就没这么简单了。在这种情况下,Underscore提供了一个_.noConflict函数,将重新绑定旧的_,并返回Underscore的引用。_.noConflict使用方式如下:

本书会介绍更多Underscore的细节,但记住,虽然我广泛使用(并认可)Underscore,但这并不是一本关于Underscore的书。

许多年前,我想写基于函数式编程技术的JavaScript库。跟许多程序员一样,我曾通过实验、实践以及阅读Douglas Crockford的文章对JavaScript有所认识。虽然我继续完成了我的函数式库(Doris),但甚至连我自己都很少用它。

完成Doris后,我继续尝试广泛的函数式编程语言如Scala和Clojure。此外,我花了很多时间编写ClojureScript,特别是它的JavaScript编译器。基于这些经验,我非常了解函数式编程技术。因此,我决定尝试使用随后这几年学到的技术复活Doris。我将其命名为Lemonad,最后几乎是与本书同时完成的。

本书中大多数函数都是为了教学目的,但我扩展了一些并贡献到我的Lemonad库,以及Underscore-contrib库。

本书的源代码可以在GitHub上https://github.com/funjs获取。此外,你也可以进入本书的网址使用上面的REPL试试本书所有定义的函数。

在编写本书(和一般编写JavaScript)的过程中,我得出以下比较好的约定。

此外,我在本书中还用到了一些约定。

基本上除了函数式方面,这本书中的JavaScript代码就像现实中的大多数的JavaScript代码。

我在几年前写一本Scheme编程语言的函数式编程的入门书籍的时候,就产生了写这本书的想法。尽管Scheme和JavaScript有一些共同的特点,但在许多重要方面截然不同。我想撇开语言来说说函数式编程。我写这本书介绍函数式编程是什么,什么是不可能在JavaScript中找到的。

我期望读者对JavaScript有基本的理解。可以通过很多书籍以及网上的资源和讨论来学习这门语言,这里就不占用本书篇幅介绍了。我还期望读者能对面向对象编程有所了解,如Java和Ruby,Python和JavaScript。了解面向对象编程可以帮助你理解我偶尔使用的一些短语,但并不需要专家级的了解。

本书的合适读者是希望了解函数式编程的JavaScript程序员,或希望学习JavaScript的函数式程序员。对于后一种读者,还可以研究一些JavaScript的……古怪的部分,特别是可以参考Douglas Crockford的《JavaScript精粹》(O’Reilly出版)。最后,这本书还适合任何希望了解函数式编程,包括不打算使用JavaScript的读者。

下面是JavaScript函数式编程的大纲。

第1章 JavaScript函数式编程简介

这本书通过引入一些主题来开始,包括函数式编程和Underscore.js。

第2章 一等函数与Applicative编程

第2章定义了一等函数,展示如何使用它们,并介绍了一些常见的应用。其中介绍了一个特别的技术,即利用一等函数实现Applicative编程。本章结尾还对软件开发中函数式编程的重要途径,即“数据思想”进行了探讨。

第3章 变量的作用域和闭包

第3章是一个过渡章,涵盖要了解函数式JavaScript编程需要注意的两个核心主题。通过覆盖变量作用域,包括在JavaScript中使用的方式:词法作用域,动态作用域和函数作用域。本章以闭包的介绍结尾,解释了工作原理,以及如何和为什么可能需要使用闭包。

第4章 高阶函数

本章建立在第2、3章基础上,介绍了一个重要的一等函数:高阶函数。虽然“高阶函数”听起来很复杂,本章会说明它其实是很直白的。

第5章 由函数构建函数

本章介绍了如何用其他函数“组合”新函数。组合函数是函数式编程的重要技术,本章将引导你了解这项技术。

第6章 递归

第6章是另一个过渡章节,将讨论递归,即一个直接或间接调用自身的函数。因为递归在JavaScript中是有局限的,因此不被经常使用;但是,本章会介绍几个绕过这些局限的方法。

第7章 纯度、不变性和更改政策

第7章介绍如何编写不会改变任何东西的函数。简单地说,函数式编程的便利性来源于不可变变量。本章将带你理解其中的含义。

第8章 基于流的编程

第8章涉及如何将任务甚至是整个系统,看作变换数据的“装配线”。

第9章 无类编程

最后一章的重点是介绍函数式编程是完全不同于基于类的面向对象编程的结构化应用程序的方式。

在这些章节之后补充了附录A。

本书使用以下字体排版约定。

斜体

表示新的术语、网址、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序代码清单,出现在段落之内则表示程序中的元素,如变量、函数名、数据库、数据类型、环境变量、程序语句和关键字。

等宽加粗体

显示命令或其他应当由用户键入的文本。

等宽斜体

表示该文本应当更换为用户提供的值或者由上下文所决定的值。

本书的目的是为了帮助你完成工作任务。在一般情况下,这本书中包括的代码示例,你可以将其应用到你的程序和文档中。除非需要复制这些示例代码的相当大部分,否则无需联系我们以获得许可。比如说,当你编写的程序用到了本书中的若干示例代码,这并不需要特别许可。但是,销售或分发含有O’Reilly书籍附带的示例程序的光盘则需要获得许可。当你在回答他人问题时援引本书内容,或者引用书中的范例代码,也不用申请许可;而如果要把本书中的代码大量地引用到你的产品文档中,则需要许可。

对于引用时署名本书,我们表示感谢,但并不要求。一个署名通常包括标题、作者、出版商和ISBN,例如“Functional JavaScript Michael Fogus(O’Reilly出版)。版权所有2013 Michael·Fogus,978-1-449-36072-6”。

如果你觉得你使用示例代码的情况超出了以上描述的不需要许可的范围,请随时联系我们:
permissions@oreilly.com

记录

Safari在线图书(www.safaribooksonline.com)是一个虚拟图书馆,让你可以轻松搜寻成千上万的顶尖技术书籍。

科技人才,软件开发者,网页设计师,以及商业和创意专业人士都将Safari在线图书作为研究、解决问题、学习和认证培训的主要资源。

Safari的联机丛书提供了一系列的产品并为组织、政府机构和个人提供不同定价。用户可以访问和搜索出版社O’Reilly Media, Prentice Hall Professional, Addison-Wesley Professional, Microsoft Press, Sams, Que, Peachpit Press, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, Course Technology等的数字内容。有关Safari在线图书的更多信息,请访问我们的在线网站。

请将对本书的有关意见和问题告知出版商:

美国:

  O’Reilly Media,Inc.

  1005 Gravenstein Highway North

  Sebastopol,CA 95472

中国:

  北京市西城区西直门南大街2号成铭大厦C座807室(100035)

  奥菜利技术咨询(北京)有限公司

我们为这本书制作了网页,其中包含了勘误表、范例,以及其他补充资料。你可以通过这个地址访问:http://oreilly/functional_js

请发送电子邮件至bookquestions@oreilly.com发表评论或询问关于本书的技术问题。

更多关于这本书、课程、会议和新闻的信息,查看下面的网站:http://www.oreilly.com

我们的Facebook网站:http://facebook.com/oreilly

在Twitter上follow我们:http://twitter.com/oreillymedia

YouTube上的频道:http://www.youtube.com/oreillymedia

写一本书需要各方面的支持和努力,本书也不例外。首先,我要感谢我的好朋友Rob Friesel抽出时间提供反馈意见。此外,我要感谢Jeremy Ashkenas把我介绍给O’Reilly,能让本书得以出版。而且是他写了非同小可的Underscore.js库。

我还要感谢这些人给我的反馈和启发:Chris Houser、David Nolen、Stuart Halloway、Tim Ewa-ld、Russ Olsen、Alan Kay、Peter Seibel、Sam Aaron、Brenton Ashworth、Craig Andera、Lynn Grogan、Matthew Flatt、Brian McKenna、Bodil Stokke、Oleg Kiselyov、Dave Herman、Mashaaricda Barmajada ee Mahmud、Patrick Logan、Alan Dipert、Alex Redington、Justin Gehtland、Carin Meier、Phil Bagwell、Steve Vinoski、Reginald Braithwaite、Daniel Fried-man、Jamie Kite、William Byrd、Larry Albright、Michael Nygard、Sacha Chua、Daniel Spiewak、Christophe Grand、Sam Aaron、Meikel Brandmeyer、Dean Wampler、Clinton Dreisbach、Matthew Podwysocki、Steve Yegge、David Liebke、Rich Hickey。

我编写本书时的配乐由Pantha du Prince、Black Ace、Brian Eno、Béla Bartók、Dieter Moebius、Sun Ra、Broadcast、Scientist、John Coltrane提供。

最后,所有这一切都要感谢我生命中的三位挚爱:Keita、Shota、Yuki。

[1]  跟所有强大的工具一样,eval和Function常量都是双刃剑,我并不反对使用,但是建议尽可能少用。


本章是本书后续内容的基础。本章将介绍什么是Underscore以及如何开始使用它;除此之外,也将对后续用到的术语和本书的目标进行解释。

“为什么选择JavaScript”,这个问题的答案很简单:灵活。换句话说,或许除了Java,目前没有比JavaScript更加流行的语言了。所有浏览器以及现有新兴技术的大范围支持,使得JavaScript成了满足可移植性的不错的选择,甚至是唯一的选择。

随着客户端服务和单页布局应用架构的再度出现,JavaScript越来越广泛地应用于附加在大量网络服务中的分离式应用当中(如单页布局)。比如所有的谷歌应用程序都由JavaScript编写,这也是单页布局应用的范例。

如果在学习JavaScript之前,你已对函数式编程有所研究,那么好消息是JavaScript“天然”支持函数式技术(例如,函数是JavaScript的一个核心概念)。举个例子,如果你对JavaScript有所了解,那么应该见过下面的代码:

Array#forEach方法于ECMA-262语言标准第5版加入,它接收一个函数(本例中的alert)并将数组中的每一个元素依次交给该函数执行。除此之外,JavaScript提供大量的能够以其他函数为参数的方法和函数。在本书的后续内容中,我将进一步以此类编程风格进行讨论。

JavaScript有坚实的语言原语基础,这是非常好的事情,但同时也是一把双刃剑。从函数、闭包、原型,到相当不错的动态核心,JavaScript提供了一系列非常好的工具集[1]。此外,JavaScript也提供了一种非常开放和灵活的执行模型。举一个小例子:所有的JavaScript函数都有一个apply方法,它使得我们可以用一个数组来调用函数,其中,数组的元素作为函数的参数。使用apply,我们可以创建一个名为splat的函数。它接受一个函数fun,并返回另一个函数,该函数接受一个数组并用apply来执行函数fun。这样一来,传入函数splat的数组的元素是函数fun的参数:

这是我们的函数式编程初试——一个返回函数的函数——我们待会再来仔细研究。需要注意的是,JavaScript是一种非常灵活的语言,apply仅仅只是它实现函数式编程的一种方法而已。

另外一个展现JavaScript灵活性的地方是,我们可以随时以任意多个任意类型的参数来调用任意一个函数。我们可以创建一个与splat功能相反的函数unsplat,它接受一个函数fun并返回另一个接受任意多个参数的函数,将参数转为数组传入函数fun并调用它:

每个JavaScript函数都可以访问一个名为arguments的局部对象,它以类似于数组的形式存储了调用本函数时的实际参数。arguments非常强大,并能够产生惊人的效果。另外,call方法与apply方法类似,只不过apply方法将参数放到数组中来调用函数,而call方法则是直接将参数逐一传递给函数。Apply、call和arguments的同时存在只是JavaScript强大灵活性的一个小例子。

随着用JavaScript来创建各种规模应用的趋势,你可能会担心该语言本身发展及其运行时支持会停滞不前。但是随意阅读一下ECMAScript.next就可以发现,JavaScript是一种不断发展(尽管速度缓慢)的语言[2]。同样,会不断有像V8引擎那样经得起时间考验的新兴技术来改进和提升JavaScript的速度和效率。

从JavaScript的出现、演变、发展到普遍存在的角度来讲,JavaScript的局限性很小。很多人会诟病JavaScript的种种奇怪用法和鲁棒性缺陷,但事实上,JavaScript确实存活了下来,并且将会一直存在下去。但无论如何,我们需要承认JavaScript是一门存在缺陷的语言[3]。事实上,目前最流行的介绍JavaScript的书:Douglas Crockford的JavaScript: The Good Parts(O’Reilly出版),花了大量的篇幅来讨论JavaScript的不好的部分。这门语言确实有古怪之处,而且总体来说在表达方面也不是很简洁。然而,修复JavaScript中存在的问题恐怕会“破坏网络世界”,这恐怕也是不能被大众所接受的。也正是因为这些问题的存在,针对于JavaScript的编译平台不断增多;这确实是一片非常多产的领域[4]

从语言支持角度来讲,经过时间的选择,我们发现命令式语言技术和对全局作用域的依赖使得JavaScript存在不安全性。这是因为,若在创建程序时将关键点放在处理易变性上,会给程序扩展带来潜在的混乱。同样,这门语言也提供了很多可用来实现其他语言中默认存在的高级功能的方法。如在主要版本的ECMAScript 6之前,JavaScript没有提供模块系统,但其实可用原生对象来简便地创建模块。这个版本的JavaScript提供了一系列松散的、互不兼容的基本部分集合,可以用来保证一系列自定义模块的实现。

古怪的语言、不安全的功能以及一系列互相竞争的库,这三个理由使得我们很难考虑选择JavaScript。然而,希望还是有的。利用一系列的规范和规约,JavaScript代码可以做到不仅安全,而且容易理解和测试,除此之外,也能够成比例缩减代码库大小。本书将带你掌握这样的方法:函数式编程。

或许你已经从最喜欢的新闻聚合网站上听说过函数式编程,也可能你已使用过支持函数式编程的技术。如果你写过JavaScript代码(在本书中,我们默认读者写过),那么你确实已使用过支持函数式编程的语言。然而,有种情况是,你可能没有从函数式编程的角度使用过JavaScript。本书突出了函数式编程风格,它可以帮助我们简化自己的库和应用程序,并帮助我们驯服那只使得JavaScript变得复杂的“野兽”。

我们可以用下面一句话来直白地描述函数式编程:

函数式编程通过使用函数来将值转换成抽象单元,接着用于构建软件系统。

这是一种简单粗糙的解释,但对于本书的前面部分来说已够用。本书使用Underscore作为库来实现函数式表达式,并且大部分内容也都遵循上面的定义。然而,这个定义并没有解释清楚“为什么”使用函数式编程。

对我来说,重大演变还是向更加函数式的风格的发展,它使得我们放弃很多旧的习惯,并从一些面向对象思想中逐渐退出。

——John Carmack

如果你熟悉面向对象编程,那么你可能会同意它的主要目标是问题分解,如图1-1 所示(Gamma,1995)。

图1-1 将一个问题分解为面向对象的几个部件

同样,如图1-2 所示,这些部件/对象可以被聚集在一起,并组合成更大的部件。

图1-2 对象“组合”在一起形成更大的对象

基于这些部件和它们之间的组合关系,我们就可以从部件之间的交互和值来描述一个系统,如图1-3所示。

图1-3 一个描述面向对象系统及其交互的序列图

这里只对如何构建面向对象系统进行了简单粗糙的解释,但这种抽象的解释已经能够说明问题。

相比较而言,用严格的函数式编程的方法来解决问题,也会将一个问题分成几部分(函数)来解决,如图1-4所示。

图1-4 将一个问题分解成几个函数式的部分

与面向对象方法将问题分解成多组“名词”或对象不同,函数式方法将相同的问题分解成多组“动词”或函数[5]。与面向对象编程类似的是,函数式编程也通过“黏结”或“组合”其他函数的方式来构建更大的函数,以实现更加抽象的行为,如图1-5 所示。

图1-5 通过函数组合来实现更多的行为

最终,一种将函数式的部件组成一个完整的系统的方法(见图1-6 )是取一个值,逐渐地将它“改变”——通过一个原始的或组合的函数——成另一个值。

图1-6 一个通过数据转换进行交互的函数式系统

在一个面向对象系统的内部,我们发现对象间的交互会引起各个对象内部状态的变化,而整个系统的状态转变则是由许许多多小的、细微的状态变化混合来形成的。这些相互关联的状态变化形成了一个概念上的“变化网”,我们时不时会因为它而感到困惑。当需要了解其带来的微妙且广泛的状态变化时,这种困惑就会成为一个问题。

相比之下,函数式系统则努力减少可见的状态修改。因此,向一个遵循函数式原则的系统添加新功能就成了理解如何在存在局限的上下文环境中——无破坏性的数据转换(例如原始数据永不发生变化)——来实现新的函数。然而,我并不愿意在函数式风格和面向对象风格之间画一条明显的界线,说它们应该是对立关系。因为既然JavaScript同时支持这两种模式,那就说明一个系统可以也应该由这两种模式共同组成。如何平衡函数式风格和面向对象风格是一件需要技巧的事情,我们将会在第9章讨论mixin时解答这个问题。然而,既然本书是在介绍函数式编程在JavaScript中实现,那么我们将会将大量篇幅放在函数式风格而非面向对象风格上。

这样说来,一个美好的基于函数式原则而构建的系统将是一个能够从输入终端接收未加工原料并逐渐从输出终端生产出产品的装配线设备(见图1-7 )。

图1-7 函数式程序类似于一个用来转换数据的机器

当然,这种装配线的类比并不完全准确,因为我们知道每个机器生产产品都需要消耗加工原料。相比之下,函数式编程以命令式的方式构建系统,并通过将显性的状态改变缩减到最小来变得更加模块化(Hughes,1984)。实践中的函数式编程并不以消除状态改变为主要目的,而是将任何已知系统中突变的出现尽量压缩到最小区域中去。

抽象方法是指隐藏了实现细节的函数。事实上,函数是一种非常好的工作单元,它使得我们能够坚持一句由巴特勒兰普森提出的、在UNIX社区长期奉行的格言:

使之运行,使之正确,使之快速。

同样,抽象函数使得我们能够完全理解Kent Beck的关于测试驱动开发(TDD)的类似的说法:

使之运行,再使之正确,再使之快速。

例如,对于错误和警告的报告,我们可以写出如下代码:

虽然这个函数并不能全面地解析年龄字符串,但却是一个好例子。我们可以用如下方法来调用parseAge:

parseAge函数工作正常,但如果我们想修改输出错误、信息和警告呈现的方式,那么就需要修改相应的代码行,以及其他地方的类似输出模式。一个较好的方法是将错误、信息和警告的概念抽象成不同的函数:

有了这些函数,我们就可将parseAge函数改写成:

下面是新函数的行为:

新的行为与旧的行为差别不大,不同的是现在报告错误、信息和警告的想法已经被抽象化了。错误、信息和警告的报告结果也因此完全被修改:

因此,由于行为包含在单一的函数中,所以函数可以被能够提供类似行为的新函数取代,或直接被完全不同的行为所取代(Abelson and Sussman,1996)。

多年来,我们一直被教导说封装是面向对象的基石。在面向对象术语中,封装是指一种将若干个数据与用来操纵它们的特定操作包装起来的方式,如图1-8所示。

图1-8 大多数面向对象语言使用对象边界来包装数据元素和它们的操作;因此,一个Stack类将一个元素的数组和用来操作这个数组的push、pop和peek方法包装在一起

JavaScript提供了一个对象系统,它也确实能够封装数据与操作。然而,有时封装被用来限制某些元素的可见性,称为数据隐藏。在JavaScript的对象系统中,并没有提供直接隐藏数据的方式,因此使用一种叫做闭包的方式来隐藏数据,如图 1-9所示。

图1-9 使用闭包来封装数据是一种函数式的向客户端隐藏细节的方式

在第3章之前,我们不会深入介绍闭包,但现在需要你记住的是,闭包也一种函数。通过使用包含了闭包的函数式技术,我们能够与大多数面向对象语言一样,实现有效的数据隐藏,尽管我不愿说函数式封装和面向对象式封装究竟谁更好。虽然在实践中它们是不同的,但它们实际上都提供了建立某种抽象的类似的方法。事实上,本书并不鼓励大家为了学习函数式编程而扔掉曾经学到的一切,从而喜欢学习函数式编程;相反,我们旨在就其本身来讨论函数式编程,这样你就可以确定它是否合适你的需求。

隐藏数据和行为(通常不方便于快速修改)只是一种将函数作为抽象单元的方式。另外一种方式是提供一种简单地存储和传递基本行为的离散单元。举个例子,用JavaScript语法来索引数组中的一个值:

虽然数组索引是JavaScript的一个核心行为,但并没有办法可以在不把它放到函数里的前提下,获取这个行为并根据需要来使用它。因此,举一个函数的简单例子就是抽象数组索引行为,我们称它为nth。nth的简单实现如下所示:

或许正如你所猜测的,nth的主逻辑工作正常:

然而,当传入意想不到的值时,nth就会出错:

因此,如果想围绕nth来实现函数抽象,我们或许会设计出下面的声明:nth返回一个存储在允许索引访问的数据类型中的有效元素。这段声明的关键在于索引数据类型的概念。为了判断什么是索引的数据类型,我们可以创建一个isIndexed函数,实现如下所示:

函数isIndexed也是一个提供了判断某个数据是否是字符串或数组的函数抽象。在抽象之上实现新的抽象,nth的实现也就如下所示:

完整的nth函数的使用方法如下所示:

与我们从index抽象中构建nth函数抽象的方式一样,我们也可以以同样的方式来构建一个second抽象:

函数second允许我们在一个不同但相关的情况下,正确使用nth函数:

另外一个JavaScript的基本行为单元是比较器。比较器是一个函数,它接受两个参数,如果第一个参数值小于第二个参数值,则返回<1;如果第一个参数值大于第二个参数值,则返回>1;如果两个参数值相等,则返回0。事实上,JavaScript本身似乎可以利用数字本身的性质,提供一个默认的sort方法:

但是当有不同数据混合出现时,就会出错:

问题在于,在没有给定参数的情况下,Array#sort方法执行字符串的比较。然而,每一个JavaScript程序员都知道,Array#sort需要一个比较器,因此应该写成:

现在看起来似乎好多了,但是有更为通用的方法。毕竟我们可能还要在其他的代码中用到这样的排序,所以把这个匿名函数抽出来,给它起一个名字,或许会更好一些:

但函数compareLessThanOrEqual的问题在于,它被耦合到了“比较器”的概念当中,并不容易被单独当作一个通用的比较器来用:

为了达到预期的效果,我们需要了解函数compareLessThanOrEqual作为一个比较器的性质:

但这并不令人满意,特别是将来函数compareLessThanOrEqual的返回值可能会被其他开发人员修改为用−42来代表比较结果。一个较好的实现compareLessThanOrEquall函数的方式是:

总是返回一个布尔值(只会返回true或false)的函数被称为谓词。因此,函数lessOrEqual事实上并不是一个精心设计的比较器,它只是对内建操作符<=的一个简单包装。

看到这里,你恐怕会有转行的意向。但是,更深一步考虑,这事实上是合理的。如果sort函数需要一个比较器,并且lessThan函数只会返回true或false,那么我们需要通过某种方式在不重复一堆的if/then/else模板的情况下,从后者的世界转换到前者当中来。解决方案是创建一个comparator函数,它接受一个谓词,并将其结果转化成comparator函数所期待的−1/0/1:

现在,我们可以用comparator函数来返回一个能够将谓词lessOrEqual的结果(true或false)“映射”成比较器所期待的结果(−1,0或1)的新函数了,如图1-10 所示。

图1-10 用比较器功能来桥接两个“世界”之间的差异

在函数式编程中,我们将经常会看到这类用于允许将一种类型数据转换为另一种类型数据的函数。我们来看看comparator的用法:

comparator函数可以将任何返回“真”或“假”的函数映射到“比较器”的概念上来。这个话题将在第4章进行更深入的讨论,但是值得我们现在注意的是,comparator是一个高阶函数(因为它接受一个函数,并返回一个新的函数)。请记住,并不是每一个谓词都应该与comparator函数一起使用。例如,将_.isEqual函数作为一个comparator的基础意味着什么?尝试一下,看看会发生什么。

在本书中,我们将会讨论多种用函数式技术提供并促进创建抽象的方式。正如我们接下来要讨论的,抽象的函数与数据之间有一个漂亮的协同作用。

JavaScript的对象原型模型是一个丰富且基础的数据方案。就其本身而言,原型模型提供了在许多其他主流编程语言中没有发现的一定级别的灵活性。然而,出于习惯,许多JavaScript程序员会立即尝试利用原型或闭包(或两者都用)来建立一个基于类的对象系统[6]。尽管类系统有其长处,但很多时候一个JavaScript应用程序的数据需求比类中的简单得多[7]

相反,使用JavaScript的原始数据、对象和数组,以及目前由类创建的大部分数据模型任务都属于一个范畴。从历史上看,函数式编程已经致力于构建能够实现更高层次行为以及能够工作在非常简单的数据结构上的函数[8]。事实上,在这本书(以及Underscore)中,重点是如何处理数组和对象。这两个简单数据类型的灵活性是惊人的。不幸的是,它们常常被因为另一种基于类的系统而被忽视。

假设我们有一个任务,需要用编写JavaScript程序来处理逗号分隔值(CSV)文件(一种用来代表数据表的标准方法)。例如,假设我们有一个如下所示的 CSV文件:

很明显,这个数据代表一个有三列(姓名、年龄和头发)和三行(第一个是标题行,并且其余的为数据行)的表。一个用来解析这个非常有限的CSV格式表示的字符串的小函数的实现如下:

我们发现,函数lameCSV一行接一行地处理,用\n分离出行,再将每一个表格中的空白去除[9]。整个数据表是一个包含了数组的数组,每个数组都包含了字符串。从表1-1 所示的概念图可以看出,嵌套的数组可以被看作一个表。

表1-1 简单的嵌套数组是一种抽象的数据表的方式

姓 名

年 龄

头 发 颜 色

Merble

35

红色

Bob

64

金黄

如下所示,使用lameCSV来解析存储在一个字符串中的数据:

使用选择性间距突出了返回数组的表性质。在函数式编程中,像lameCSV这样的函数以及先前定义的comparator是将一个数据类型转换为另一个的关键。图 1-11 描述了一般的数据转换是如何被看作从一个“世界”进入另一个“世界”的。

图1-11 函数是跨越两个“世界”之间的桥梁

我们有更好的方法来表示表数据,但是这个嵌套数组目前来说已经足够了。事实上,很少有动机建立一个复杂的类层次结构来代表无论是表本身、行、人或任何其他数据。相反,保持数据表示最小,使得我们可以方便地使用已有的数组字段和数组处理函数和方法:

同样,由于我们知道原始数据的形式,可以创建更有描述性的名字选择函数来访问数据:

这里定义的select函数使用了现有的数组处理函数,帮助我们流畅地访问简单数据类型:

一个能令人信服的说法是,实施和使用的简易性是使用JavaScript的核心数据结构进行数据建模的目的。这并不是说面向对象或基于类的方法就完全没有用。根据我的经验,我发现以处理集合为中心的函数式方式更适合处理与人有关的数据,而面向对象的方法最适合模拟人[10]

如果愿意的话,我们可以将数据表改为自定义的基于类的模型。只要你使用选择器抽象,那么用户将永远不知道,也不关心。然而,在这本书中,我努力保持数据需求尽可能简单,并构建操作这些数据的抽象函数。有趣的是,通过将自己约束在对简单数据的操作上,增加了灵活性。你可能会对这些基本类型将会带我们走多远感到惊讶。

这不是一本围绕JavaScript众多怪癖的书。已经有很多其他的书以这种方式来帮助你学习JavaScript。然而,在开始任何JavaScript项目之前,这里定义了两个我们常常会需要的有用的函数:existy和truthy。

函数existy旨在定义事物的存在。JavaScript中有两个值表示不存在——null和undefined。因此,existy函数主要检查其参数是否是这类值,它的实现如下:

使用松散不等式运算符(!=),就可以区分null,undefined和其他所有对象。existy函数的使用方法如下所示:

使用existy函数简化了JavaScript中对象是否存在的判断。至少,它将存在性检查并置成了一个简单易用的函数。上面说到的第二个函数truthy的定义如下所示[11]

函数truthy用来判断一个对象是否应该被认为是true的同义词,它的使用方法如下所示[12]

在JavaScript中,有时只有在某个条件为真的情况下执行某些操作,否则返回类似undefined或null的值。一般模式如下所示:

使用truthy函数,我们可以将该逻辑通过以下方式封装起来:

现在,每当出现这种丑陋的模式,我们可以用以下操作来代替[13]

函数executeIfHasField的成功执行和出错的情况如下所示:

没什么大不了的,对不对?所以,我们定义了两个函数,这很难称得上是函数式编程。函数式理念来自于它们的使用。你可能已经熟悉了在许多JavaScript实现中的Array#map的方法。它旨在接收一个函数,用一个数组中的每一个元素来调用它,并返回一个存储了新值的新数组。它的使用方法如下所示:

以下就是函数式编程。

类似于这样的代码会遍布在本书之中。

我知道此刻你在想什么。这函数式编程执行起来必定慢得让人受不了,是吗?

没有办法否认的是,使用数组索引形式的array[0]会执行得比任何nth(array, 0)或_.first(array)都快。同样,以下形式的命令式循环也是非常快的[14]

类似的功能,在所有因素都相同的情况下,使用Underscore的_.each函数确实会慢一些:

然而,很有可能的是所有因素不会相等。当然,如果一个函数对执行速度有需求,那么一个将内部使用的_.each转换成类似功能的for或while的工作是合理的。令人高兴的是,笨重缓慢的JavaScript的日子即将结束,在某些情况下已经是过去的事情了。例如,谷歌的V8引擎的发布引来了一直在向所有JavaScript引擎供应商推动的(BAK 2012)运行时优化的时代[15]。即使其他厂商没有跟随谷歌的带领,V8引擎的使用率仍然在增长,而事实上,它驱动着非常流行的Chrome浏览器和Node.js本身。然而,其他厂商都是跟着V8引擎的引领,并引入了运行时速度增强功能,如本机代码的执行,即时编译,更快的垃圾收集,客户端缓存,并嵌入到他们自己的JavaScript引擎中[16]

然而,对于一些JavaScript程序员来说,对老浏览器——如Internet Explorer 6——的支持是一个非常现实的要求。当面对传统平台时,有两个因素要考虑:(1)IE6及其同类型浏览器的使用正在逐渐消失,(2)在代码到达浏览器之前,有其他的方法来提高速度[17]。例如,在代码内联是一个有趣的话题,因为许多采用代码内联的优化工作可以静态地展开,或者甚至是在代码运行之前展开。内联代码是将一段代码放在一个函数中,并“粘贴”到调用函数的地方中去。让我们来看一个例子,以便更清楚地理解。在Underscore的_.each方法的实现内部,是一个类似于如前面所示for循环的循环代码(为清楚起见,对原代码做了一些修改):

假设我们有一段代码,类似于:

静态优化器可以将performTask的函数体转换为如下样子:

成熟的优化工具可以通过完全消除函数调用来对其进行进一步的优化:

最后,非常棒的静态分析器甚至可以将它进一步优化为五个独立的调用:

最理想的优化转换器是这样的:假设上面的调用不会带来什么影响或根本不会被调用,那么最优变换为:

也就是说,如果一段代码可以被确定为“死代码”(不被调用),那么它可以安全地通过代码省略(code elision)来消除。目前已经有针对于JavaScript的此类优化器——以谷歌的闭包编译器(Google’s Closure compiler)为主。闭包编译器是一个非常好的能够将JavaScript高度优化的工程[18]

有很多种不同的基于最佳实践和优化工具组合的方式,能够实现加速甚至是高度函数化的代码。可是,很多时候我们太急于考虑运算速度,甚至是在写出一段正确的代码之前就开始考虑运算速度。同样,有时候我发现我在不断依据速度来考虑问题,即使我所创建的系统根本不要考虑速度。Underscore是一个非常流行的函数式编程JavaScript库,而大量的应用程序只是用用它而已。对于成就了许多函数式习语的JavaScript库重量级冠军——jQuery——也是同样。

当然,也有以速度考虑为首的场景(例如游戏编程和低延迟系统)。然而,即使在这样的系统的执行要求面前,函数式技术也并不一定会降低速度。我们应该不会希望将类似于nth的函数放在渲染界面的核心代码中,但总体而言,函数式结构仍然可以带来好处。

对我个人而言,编程风格的第一条规则是:写漂亮的代码。在我的职业生涯中,我已在不同程度上实现了这个目标,但它仍然是我追求的东西。写漂亮的代码使得我从另一个方面优化了时间:坐在一张桌子边打字的时间。我发现如果做得好,函数式代码风格可以很漂亮。我希望在快要看完本书的时候,你会同意这个观点。

在开始有趣的内容之前,我想解释一下为什么选择用Underscore来作为本书的表述方式。首先,Underscore是一个非常好的库,它提供了一套漂亮实用的函数式风格API。如果我从头开始重新实现一遍所有对理解函数式编程有用的函数是没有意义的。为什么需要在“映射化”的理念更重要的时候来实现map方法?当然,并不是说我不会在本书中实现核心的函数式工具,但还是以Underscore为基础[19]

其次,读者在练习时可能会发现对Array#map调用不起作用。这种问题的原因可能是,运行环境中没有实现数组的 map 方法。另外,我也想尽可能地避免陷入跨浏览器兼容问题泥潭。在学习过程中,这种类型的影响是非常重要的,它会使得我在向大家介绍函数式编程的过程中分心去处理这类问题。使用Underscore几乎可以完全消除这类影响[20]

最后,JavaScript的本质使得程序员能够经常重新发明轮子。JavaScript本身可将强大的低级别构建与中高层语言完美地组合。正是这种奇怪的情况,使得人们几乎不敢用较低级别部件来创造新的语言特性。语言本身的进化可以消除重新造轮子的需求(例如模块系统),但我们不太可能看到这种需求的完全消失[21]。不过,我们相信在可以的条件下,应该重用已有的高品质代码库[22]。重新实现Underscore的功能将会非常有趣,但这并不能给你我(或者是我的员工们)带来很大的益处。

以学习和使用JavaScript为动机,本章涵盖了一些介绍性主题。在现有稳定的主流编程语言中,很少有可以跟JavaScript的增长趋势相匹敌的。同样,这种增长潜力似乎是无限的。然而,JavaScript是一门有缺陷的语言,它需要依赖于强大的技术、规范或两者的混合才能有效地被运用起来。一种用于构建JavaScript应用程序的技术称为“函数式编程”。概括地说,它包括以下技术。

但是,仅仅只是构建函数是不够的。事实上,与强大的数据抽象相结合来实现函数式编程往往效果最好。在函数式编程和数据之间,存在一个美丽的对称性。下一章会对该对称性进行深入的讨论。

[1] 与所有的工具一样,如果你不小心,还是可能切到或粉碎你的拇指。

[2] 可以在http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts查看ES.next的草案。

[3] 值得讨论的是缺陷有多严重。

[4] 一些能编译成JavaScript的语言有ClojureScript, CoffeeScript, Roy, Elm, TypeScript, Dart, Flapjax, Java。

[5] 这种思路比较容易从面向对象转换到函数式编程上,而且我将在接下来的书中混合这两种思路。

[6] ECMAScript.next 正讨论支持类的可能性。然而,这个特性颇受争议。不管怎么样,类不知道什么时候才能加入到JavaScript。

[7] 基于类的对象系统的一个有力的论据是实现用户界面的历史使用。

[8] 不久你就能看到函数式是注重列表数据结构。而对于JavaScript来说,array就可以替代该数据结构。

[9] 函数lameCSV在这里只用作说明用途,并不是完整功能的CSV 解释器。

[10] 这种面向对象的范式从模拟社区如雨后春笋般以simula的编程语言形式涌现出来并不是巧合。通过写模拟系统,我强烈感觉到面向对象或基于角色的模型很适合用simula。

[11] 我这里定义的truthy是指JavaScript的原始真类型。虽然知道JavaScript认为什么为真很重要,我还是在我的应用中简化这些规则。

[12] 这里数字0是故意设计成“truthy”的。原本代表false是继承自C语言。如果你还想使用该属性,就在期待0的时候不要用truthy函数。

[13] 我使用existy(target[name])而不是Underscore的has(target, name),是因为后者只会检查自己的字段。

[14] 我用来评测JavaScript性能的网站是 http://www.jsperf.com

[15] 在任何故事中都是有前传的。在V8之前,WebKit项目用SquirrelFish引擎编译JavaScript代码。在SquirrelFish之前是Tamarin VM,它由Mozilla基于Adobe的ActionScript VM 2开发。有趣的是,大部分JavaScript的优化技术都来自Self和Smalltalk这些老的语言。

[16] 不懂这些优化技术没关系,它们不是本书的重点。但我还是强烈推荐研究这些话题。

[17] 有一个有意思的网站监测全球IE6的使用率:http://www.ie6countdown.com

[18] 使用Google Closure compiler时生成的特殊风格的代码颇受争议。但是正如我在ClojureScript编译器工作时学到的,如果有用的话,就会特别有用。

[19] 还有一些其他的函数式编程库如Functional JavaScript,Bilby甚至JQuery。然而我选择的是Underscore。

[20] 当接触到底层方法调用时,Underscore会考虑到浏览器兼容性的问题。

[21] 我觉得这是编程的本质。

[22] 我特别迷恋于用microjs网站来发现有趣的JavaScript库(译者注:也可以到该网站的github上添加你觉得有趣的JavaScript库——https://github.com/madrobby/microjs.com)。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
JavaScript核心原理:规范、逻辑与设计
JavaScript核心原理:规范、逻辑与设计
JavaScript入门经典(第7版)
JavaScript入门经典(第7版)
JavaScript函数式编程指南
JavaScript函数式编程指南
PHP、MySQL和JavaScript入门经典(第6版)
PHP、MySQL和JavaScript入门经典(第6版)
JavaScript学习指南(第3版)
JavaScript学习指南(第3版)

相关文章

相关课程