├── .gitignore ├── fig1.png ├── fig2.png ├── fig3.png ├── fig4.png ├── fig5.png ├── fig6.png ├── fig7.png ├── fig8.png ├── fig9.png ├── fig10.png ├── fig11.png ├── fig12.png ├── fig13.png ├── fig14.png ├── fig15.png ├── fig16.png ├── fig17.png ├── fig18.png ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── SUMMARY.md ├── ch11-code ├── stock-ticker.css ├── index.html ├── stock-ticker-events.js ├── mock-server.js ├── fp-helpers.js └── stock-ticker.js ├── CONTRIBUTING.md ├── toc.md ├── foreword.md ├── preface.md ├── README.md ├── fig17.svg ├── apC.md ├── ch1.md ├── fig18.svg ├── fig5.svg ├── fig6.svg ├── fig15.svg ├── fig8.svg ├── fig16.svg ├── apB.md ├── fig3.svg ├── ch10.md ├── fig10.svg ├── fig4.svg ├── ch6.md ├── fig9.svg ├── fig12.svg ├── fig11.svg ├── ch7.md └── fig13.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | _book/ 4 | -------------------------------------------------------------------------------- /fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig1.png -------------------------------------------------------------------------------- /fig2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig2.png -------------------------------------------------------------------------------- /fig3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig3.png -------------------------------------------------------------------------------- /fig4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig4.png -------------------------------------------------------------------------------- /fig5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig5.png -------------------------------------------------------------------------------- /fig6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig6.png -------------------------------------------------------------------------------- /fig7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig7.png -------------------------------------------------------------------------------- /fig8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig8.png -------------------------------------------------------------------------------- /fig9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig9.png -------------------------------------------------------------------------------- /fig10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig10.png -------------------------------------------------------------------------------- /fig11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig11.png -------------------------------------------------------------------------------- /fig12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig12.png -------------------------------------------------------------------------------- /fig13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig13.png -------------------------------------------------------------------------------- /fig14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig14.png -------------------------------------------------------------------------------- /fig15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig15.png -------------------------------------------------------------------------------- /fig16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig16.png -------------------------------------------------------------------------------- /fig17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig17.png -------------------------------------------------------------------------------- /fig18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikcamp/Functional-Light-JS/HEAD/fig18.png -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **是,我保证我已经阅读过 [贡献指导](CONTRIBUTING.md)** (*如果你确实阅读过它*,你可以移除这一行 ). 2 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **是,我保证我已经阅读过 [贡献指导](CONTRIBUTING.md)** (*如果你确实阅读过它*, 你可以移除这一行 ). 2 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [介绍](README.md) 4 | * [序](foreword.md) 5 | * [前言](preface.md) 6 | * [第一章:为什么使用函数式编程?](ch1.md) 7 | 8 | * [第二章:函数基础](ch2.md) 9 | * [第三章:管理函数的输入](ch3.md) 10 | * [第四章:组合函数](ch4.md) 11 | * [第五章:减少副作用](ch5.md) 12 | * [第六章:值的不可变性](ch6.md) 13 | * [第七章:闭包 vs 对象](ch7.md) 14 | * [第八章:列表操作](ch8.md) 15 | * [第九章:递归](ch9.md) 16 | * [第十章:异步的函数式](ch10.md) 17 | * [第十一章:融会贯通](ch11.md) 18 | * [附录 A :Transducing](apA.md) 19 | * [附录 B :谦虚的 Monad](apB.md) 20 | * [附录 C :函数式编程函数库](apC.md) 21 | -------------------------------------------------------------------------------- /ch11-code/stock-ticker.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | -moz-box-sizing: border-box; 3 | box-sizing: border-box; 4 | } 5 | 6 | #stock-ticker { 7 | background-color: #efefef; 8 | padding: 1em; 9 | margin: 0; 10 | width: 14em; 11 | } 12 | 13 | .stock { 14 | list-style-type: none; 15 | padding: 0; 16 | margin: 0 0 1em 0; 17 | } 18 | 19 | .stock:last-child { 20 | margin-bottom: 0; 21 | } 22 | 23 | .stock > span { 24 | display: inline-block; 25 | width: 4em; 26 | text-align: right; 27 | overflow: hidden; 28 | } 29 | 30 | .stock-name { 31 | font-weight: bold; 32 | text-align: center; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /ch11-code/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Functional-Light JavaScript, Ch11: Stock Ticker 6 | 7 | 8 | 9 |

Functional-Light JavaScript, Ch11: Stock Ticker

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献 2 | 请通过提交PR,随时为提高本内容的质量做出贡献,比如改进代码片段,说明等。如果你觉得有用语和措辞让你感到困惑, **在提交 PR 前,先 open 一个 issue 询问下.** 3 | 4 | 不过,当你选择了为这个仓库贡献内容(不仅仅是拼写纠正),就意味着同意授予我(以及任何未来的出版者)非独占性许可,以便在合适的时候我们可以使用该内容。也许你已经知道了,但是我必须明确声明以确保不会有法律的麻烦。 5 | 6 | ## 请先搜索! 7 | 8 | 如果你有任何疑问或者关注点,请确保先在 issue(包括 open 和 close 状态)里面搜一下,把issue尽可能减少。我希望尽可能把我的精力放在写书上。 9 | 10 | ## 拼写? 11 | 12 | 这本书最终会经过官方编辑,拼写错误将会在那时候被纠正。所以 **关于拼写我现在不会太注重**. 13 | 14 | 如果你要因为拼写错误发起 PR , 请考虑将几个小的校正合成一个 PR(通过不同的 commits )。 或者, **暂时不用太在意它们** 因为我保证我们到时会统一处理它们。 15 | 16 | ## 阅读体验(章节/单元 链接,等) 17 | 18 | 我能理解,当我们阅读一篇长的 md 文件时,如果没有相关联的链接指向其他章节时,它的体验不是特别好。出于这个原因,你们可能觉得可以发起一个 issue/PR 去增加这些功能。 19 | 20 | 这个话题被提起过很多次,我也考虑过,但我 **不** 接受这些改变合到仓库。 21 | 22 | 我的书的仓库主要目的是为了对出版(付费的电子书和印刷版本)进行内容的管理和追踪。我把它公开,是因为我同样在意提供一个免费并且可以提早获得内容的途径,以确保不会因为付费的壁垒阻碍学习。 23 | 24 | 所以, 这个仓库 **不是用来优化你的阅读体验的** 。而是用来优化出版流程的。 25 | 26 | 对你们大多数人而言,最合适的阅读体验就是我(最终)出售的电子书或者印刷版本。提供免费的内容,但是付费的阅读体验,这是我着重的平衡点。对于平衡点其他作者可能有不同的想法,但这是我现在选择的。 27 | 28 | 我希望你们从内容里面获益,同时希望你们觉得通过购买电子/印刷书(一旦可以销售)来获得最好的阅读体验是值得的。 29 | -------------------------------------------------------------------------------- /toc.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | 3 | ## 目录 4 | 5 | * 序言 6 | * 前言 7 | * 第 1 章:为什么使用函数式编程? 8 | * 置信度 9 | * 交流渠道 10 | * 接受 11 | * 你不需要它 12 | * 资源 13 | * 第 2 章:函数基础 14 | * 什么是函数? 15 | * 函数输入 16 | * 函数输出 17 | * 函数功能 18 | * 句法 19 | * 来说说 This ? 20 | * 第 3 章:管理函数的输入(Inputs) 21 | * 立即传参和稍后传参 22 | * 一次传一个 23 | * 只要一个实参 24 | * 扩展在参数中的妙用 25 | * 参数顺序的那些事儿 26 | * 无形参风格 27 | * 第 4 章:组合函数 28 | * 输出到输入 29 | * 重排序组合 30 | * 抽象 31 | * 回顾形参 32 | * 第 5 章:减少副作用 33 | * 什么是副作用 34 | * 一次就好 35 | * 纯粹的快乐 36 | * 有或者无 37 | * 纯化 38 | * 第 6 章:值的不可变性 39 | * 原始值的不可变性 40 | * 从值到值 41 | * 重新赋值 42 | * 性能 43 | * 以不可变的眼光看待数据 44 | * 第 7 章: 闭包 vs 对象 45 | * 达成共识 46 | * 相像 47 | * 同根异枝 48 | * 第 8 章:列表操作 49 | * 非函数式编程列表处理 50 | * 映射 51 | * 过滤器 52 | * Reduce 53 | * 高级列表操作 54 | * 方法 vs 独立 55 | * 查寻列表 56 | * 融合 57 | * 列表之外 58 | * 第 9 章:递归 59 | * 定义 60 | * 声明式递归 61 | * 栈、堆 62 | * 重构递归 63 | * 第 10 章:异步的函数式 64 | * 时间状态 65 | * 积极的 vs 惰性的 66 | * 响应式函数式编程 67 | * 第 11 章:融会贯通 68 | * 准备 69 | * 股票信息 70 | * 股票行情界面 71 | * 附录 A: Transducing 72 | * 附录 B: 谦虚的 Monad 73 | * 附录 C: 函数式编程函数库 74 | -------------------------------------------------------------------------------- /ch11-code/stock-ticker-events.js: -------------------------------------------------------------------------------- 1 | var server = connectToServer(); 2 | 3 | var formatDecimal = unboundMethod( "toFixed" )( 2 ); 4 | var formatPrice = pipe( formatDecimal, formatCurrency ); 5 | var formatChange = pipe( formatDecimal, formatSign ); 6 | var processNewStock = pipe( addStockName, formatStockNumbers ); 7 | var observableMapperFns = [ processNewStock, formatStockNumbers ]; 8 | var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server ); 9 | 10 | var [ newStocks, stockUpdates ] = pipe( 11 | map( makeObservableFromEvent ), 12 | curry( zip )( observableMapperFns ), 13 | map( spreadArgs( transformObservable ) ) 14 | ) 15 | ( [ "stock", "stock-update" ] ); 16 | 17 | 18 | // ********************* 19 | 20 | function addStockName(stock) { 21 | return setProp( "name", stock, stock.id ); 22 | } 23 | 24 | function formatStockNumbers(stock) { 25 | var updateTuples = [ 26 | [ "price", formatPrice( stock.price ) ], 27 | [ "change", formatChange( stock.change ) ] 28 | ]; 29 | 30 | return reduce( function formatter(stock,[propName,val]){ 31 | return setProp( propName, stock, val ); 32 | } ) 33 | ( stock ) 34 | ( updateTuples ); 35 | } 36 | 37 | function formatSign(val) { 38 | if (Number(val) > 0) { 39 | return `+${val}`; 40 | } 41 | return val; 42 | } 43 | 44 | function formatCurrency(val) { 45 | return `$${val}`; 46 | } 47 | 48 | function transformObservable(mapperFn,obsv){ 49 | return obsv.map( mapperFn ); 50 | } 51 | -------------------------------------------------------------------------------- /foreword.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 序 3 | 4 | 众所周知,我是一个函数式编程迷。我尝试阅读最新的学术论文,业余时间乃至工作间隙研究抽象代数(译者注:抽象代数又称近世代数,是研究各种抽象公理化代数系统的数学学科,也是现代计算机理论基础之一),并四处传播函数式编程的理念和语言。我所书写的 JavaScript 代码,每一条语句都是纯的。没错,我就是一个彻头彻尾的函数式编程教条式的狂热者。关于为什么要写纯的语句,请看[我写的这本书](https://github.com/MostlyAdequate/mostly-adequate-guide)。 5 | 6 | 其实我以前并不是这样子... 我曾痴迷于面向对象,并热衷于使用面向对象的方法来构建“真实世界”。我是人造机器人的发明者,夜以继日地修正机器人以达到更高精度的控制力。我也是有意识木偶的创造者,手指在键盘上的轻舞飞扬赋予了它们生命。做为黑客界的盖比特(译者注:盖比特是玩具之父),在连续不间断的写了 5 年面向对象的代码后,我对于这些成果还是不甚满意。整个过程也并不顺利,我一直感觉自己是一个糟糕的程序员,甚至失去了信心,认为写出既简单,又灵活同时又很好扩展的代码是不可能的。 7 | 8 | 我想是时候去尝试一些新的方法了,我开始涉足函数式编程的理念,并把它用在我的代码中。我的同事对此非常惊诧,他们根本不知道我在干什么。那段时间里我写的代码非常糟糕、另人生厌、简直是垃圾。造成这样结果的原因是我缺少一个目标或者说愿景。当然现在那个会编码的蟋蟀杰明尼(译者注:原文使用 Jiminy-Coding-Cricket 迪士尼动画人物蟋蟀杰明尼来暗指之前蹩脚的自己)已经不在了。在花费了好长时间,写了好多垃圾程序后我才弄明白怎样正确进行函数式编程。 9 | 10 | 现在,经历了那些乱七八糟的探索后,我感觉到纯函数编程实现了它所承诺的代码可读性和可复用。我不再发明而是发现我的模型,我像一个正在揭开巨大阴谋的侦探,在软木板上钉满了数学证据。一个数字时代的库斯托(译者注:库斯托是个传奇式的人物,探险家、电影制片人,一个享有戴高乐将军一样世界性声誉的法国人,作者比喻自己学习函数式编程就像库斯托探索海洋一般)以科学的名义记录下了这片奇特土地的特征!虽然并不完美,仍有很多东西要学习,但我对我的工作和产出从未有过现在这般满意! 11 | 12 | 假如一开始就有这本书,我探索纯函数式编程世界的道路就会更平坦一点,而不是荆棘满地。本书有两层:第一层教会你如何在每天的编码工作中,有效地使用各种各样的函数式构造方法。另一层则更重要,本书会提供一个准星,确保你不会偏离函数式编程的原则。 13 | 14 | 函数式编程是一种编程范式,Kyle 倡导使用它来实现声明式编程和函数式编程,同时该范式还可以与 JavaScript 世界形成平衡和互动。通过学习本书,你无需彻底理解范式的一切,就能了解纯函数式编程的基础;你无需重新创造轮子,就能获得练习和探索函数式编程的技能,并让代码运行良好;你无需像我之前一样漫无目的地徘徊、甚至走回头路就能让你的职业生涯更上一层楼。你的合作者和同事们一定会欣喜若狂! 15 | 16 | Kyle (译者注:Kyle 是火爆全球的《你不知道的 JavaScript》一书原作者)是一位伟大的老师,他对函数式编程的宏伟蓝图不懈追求,不放过任何一个角落和缝隙,同时他也苦学习者之苦。他的风格与行业产生共鸣,将大家的水平整体提高了一个档次。他的工作成果不仅出现在很多人的收藏夹中,也在 JavaScript 发展历史上占据坚实地位。Kyle 老师是绝世高手,你值得拥有。 17 | 18 | 函数式编程有很多种定义。Lisp 程序员和 Haskell 程序员对于函数式编程的定义截然不同。OCaml 和 Erlang 语言对于函数式编程范式的看法也大相径庭。即使在同一种语言 JavaScript 中,你也能看到函数式编程不同的定义。但总有一种纽带把这些不同的函数式编程连接在一起,这个纽带是一个有些模糊的“我一看就知道”的定义,这听起来有点下流(有人确实觉得函数式编程下流)。本书旨在抓住这个纽带,并不让你学习某些圈子的固定习语,而是让你获取相关知识,这些知识不论在哪个语言的函数式编程中都适用。 19 | 20 | 本书是你开启函数式编程旅途的绝佳起点。开始吧,Kyle 老师... 21 | 22 | *-Brian Lonsdorf (@drboolean)* 23 | -------------------------------------------------------------------------------- /ch11-code/mock-server.js: -------------------------------------------------------------------------------- 1 | function connectToServer() { 2 | // faking an event emitter attached to a server-event stream 3 | return evtEmitter; 4 | } 5 | 6 | 7 | // *********************************** 8 | // MOCK SERVER 9 | // *********************************** 10 | 11 | // simple/mock event emitter 12 | var evtEmitter = { 13 | handlers: {}, 14 | on(evtName,cb) { 15 | this.handlers[evtName] = this.handlers[evtName] || []; 16 | this.handlers[evtName][this.handlers[evtName].length] = cb; 17 | }, 18 | addEventListener(...args) { 19 | return this.on( ...args ); 20 | }, 21 | removeEventListener(){}, 22 | emit(evtName,...args) { 23 | for (let handler of (this.handlers[evtName] || [])) { 24 | handler(...args); 25 | } 26 | } 27 | }; 28 | 29 | var stocks = { 30 | "AAPL": { price: 121.95, change: 0.01 }, 31 | "MSFT": { price: 65.78, change: 1.51 }, 32 | "GOOG": { price: 821.31, change: -8.84 }, 33 | }; 34 | 35 | setTimeout( function initialStocks(){ 36 | for (let id in stocks) { 37 | // !!SIDE EFFECTS!! 38 | evtEmitter.emit( "stock", Object.assign( { id }, stocks[id] ) ); 39 | } 40 | }, 100 ); 41 | 42 | setTimeout( function randomStockUpdate(){ 43 | var stockIds = Object.keys( stocks ); 44 | var stockIdx = randInRange( 0, stockIds.length - 1 ); 45 | var change = (randInRange( 1, 10 ) > 7 ? -1 : 1) * 46 | (randInRange( 1, 10 ) / 1E2); 47 | 48 | var newStock = Object.assign( stocks[stockIds[stockIdx]] ); 49 | newStock.price += change; 50 | newStock.change += change; 51 | 52 | // !!SIDE EFFECTS!! 53 | stocks[stockIdx[stockIdx]] = newStock; 54 | evtEmitter.emit( "stock-update", Object.assign( { id: stockIds[stockIdx] }, newStock ) ); 55 | 56 | setTimeout( randomStockUpdate, randInRange( 300, 1500 ) ); 57 | }, 1000 ); 58 | 59 | 60 | // !!SIDE EFFECTS!! 61 | function randInRange(min = 0,max = 1E9) { 62 | return (Math.round(Math.random() * 1E4) % (max - min)) + min; 63 | } 64 | -------------------------------------------------------------------------------- /preface.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 前言 3 | 4 | > 单子是自函子范畴上的一个幺半群 5 | 6 | 有晕头转向吗?不要担心,我自己也被绕晕了!对于那些已经了解函数式编程的人来说,这些专业术语才有意义,然而对于大部分人而言,它们没有任何意义。 7 | 8 | 这本书并不打算教你以上那些专业术语的具体含义。如果那正是你想查找的,请继续查阅。事实上,已经有很多从头到尾(正确的方式)介绍函数式编程的书了。如果你在深入学习函数式编程,这些专业术语有很重要的意义,你肯定会对这些专业术语越来越熟悉。 9 | 10 | 但是本书打算以另一种方式讲解函数式编程。我将从函数式编程的一些基础概念讲起,并尽可能少用晦涩难懂的专业术语。我们将尝试以更实用的方法来探讨函数式编程,而非纯粹的学术角度。毫无疑问,**肯定会有专业术语**。但是我将会小心谨慎的引入这些术语并解释为何它们如此重要。 11 | 12 | 可悲的是我并非酷酷的函数式编程俱乐部的一员。我从没有正式学过函数式编程。尽管我有计算机方面的教育背景并对数学有一定了解,但数学符号跟我理解的编程完全是两回事。我从来没写过一行 Scheme、Clojure 或 Haskell 代码,也不是老派的 Lisp 程序员。 13 | 14 | 我曾参加过不计其数的讨论函数式编程的会议,每次都希望能彻底搞明白函数式编程中那些神秘的概念到底是什么意思。然而每次我都失望而归,那些概念在我脑海里乱成一团,我甚至不清楚自己学了些什么。也许我学到了些东西吧,但是很长时间以来我都不能确定自己学到了什么。 15 | 16 | 通过不断的编程实践,而非站在学术的角度,我慢慢的理解了那些对函数式编程者[\[1\]](#note1) 17 | 来说很简单直白的重要概念。你是否也有类似的经历 —— 你早就知道一件事,但直到很久之后你突然发现它竟然还有一个你从来不知道的名字!? 18 | 19 | 也许你像我一样;好几年前就听说过像“map-reduce”,“big data”等这些术语,但并不懂它们的实际意义。最终我明白了map(..)函数到底做了哪些事情 —— 在我知道列表操作是通向函数式编程者之路的基石,并且为何它们如此重要之后。我知道*映射*很久了,甚至在我知道它叫`map(..)`之前。 20 | 21 | 最终我开始整理这些想法并将它们称之为「轻量级函数式编程」(FLP)。 22 | 23 | ## 使命 24 | 25 | 但是,为什么学习函数式编程如此重要,即便只是学习轻量级函数式编程? 26 | 27 | 最近几年我越来越深刻的理解到编程的核心是人,而不是代码,我甚至将其视为一种信仰。我坚信代码只是人类交流的手段,只是它产生的**副作用**(仿佛听到了自我引用的笑声)才对电脑发出具体指令。 28 | 29 | 在我看来,函数式编程的核心在于让你在编程时使用一些广为人知、易于理解的模式。经过验证,这些模式可以有效隔离让代码难以理解的错误。所以,函数式编程 —— 咳,轻量级函数式编程 —— 是每个开发者都可以掌握的重要工具之一。 30 | 31 | > monad的含义是,一旦你搞懂了,你就无法跟别人解释什么是monad了。 32 | > 33 | > Douglas Crockford 2012 "Monads and Gonads" 34 | > 35 | > https://www.youtube.com/watch?v=dkZFtimgAcM 36 | 37 | 我希望这本书有可能打破上面的诅咒,尽管我们要到最后的附录部分才开始讨论「monad」。 38 | 39 | 科班出身的函数式编程者经常宣称只有 100% 使用函数式编程才算是真正地使用函数式编程:这是一种要么全有要么全无的主张。它会让人觉得如果编程时只有一部分使用了函数式编程而另一部分没用到,整个程序会被那些没有使用函数式编程的部分污染,从而认为使用函数式编程并不值得。 40 | 41 | 我想明确地说:**我认为绝对主义并不存在**。这没有意义,就像愚蠢地建议我只有使用完美的语法,这本书才算完美,如果犯了一点点错误,就会让整本书质量变低一样。 42 | 43 | 我写地越清楚,前后越一致,你阅读此书的体验将越好。但我不是一个完美无缺的作者。有些章节可能比另外一些写的好。但是那些有待提高的章节不会使书中写的好的部分黯然失色。 44 | 45 | 同样的道理也适用于代码。随着你越来越多的使用函数式编程的模式,你的代码质量会越来越高。25% 的时间使用它们,你会得到一些好处。80% 的时间使用它们,你将收益更多。 46 | 47 | 除了几处仅存的特例,你不会在本书里看到很多绝对的论断。我们讨论的是要追求的目标和现实中方方面面的权衡。 48 | 49 | 欢迎来到最实用的函数式编程的学习之旅。我们将共同探讨学习! 50 | 51 | 1. FPer,本书统称为函数式编程者。 52 | 53 | -------------------------------------------------------------------------------- /ch11-code/fp-helpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // curried list operators 4 | var map = unboundMethod( "map", 2 ); 5 | var filter = unboundMethod( "filter", 2 ); 6 | var filterIn = filter; 7 | var reduce = unboundMethod( "reduce", 3 ); 8 | var each = unboundMethod( "forEach", 2 ); 9 | var flatMap = curry( function flatMap(mapperFn,arr) { 10 | return arr.reduce( function reducer(list,v) { 11 | return list.concat( mapperFn( v ) ); 12 | }, [] ); 13 | } ); 14 | 15 | 16 | // ************************************ 17 | 18 | function filterOut(predicateFn,arr) { 19 | return filterIn( not( predicateFn ), arr ); 20 | } 21 | 22 | function not(predicate) { 23 | return function negated(...args){ 24 | return !predicate( ...args ); 25 | }; 26 | } 27 | 28 | function reverseArgs(fn) { 29 | return function argsReversed(...args){ 30 | return fn( ...args.reverse() ); 31 | }; 32 | } 33 | 34 | function spreadArgs(fn) { 35 | return function spreadFn(argsArr) { 36 | return fn( ...argsArr ); 37 | }; 38 | } 39 | 40 | function partial(fn,...presetArgs) { 41 | return function partiallyApplied(...laterArgs){ 42 | return fn( ...presetArgs, ...laterArgs ); 43 | }; 44 | } 45 | 46 | function curry(fn,arity = fn.length) { 47 | return (function nextCurried(prevArgs){ 48 | return function curried(nextArg){ 49 | var args = prevArgs.concat( [nextArg] ); 50 | 51 | if (args.length >= arity) { 52 | return fn( ...args ); 53 | } 54 | else { 55 | return nextCurried( args ); 56 | } 57 | }; 58 | })( [] ); 59 | } 60 | 61 | function uncurry(fn) { 62 | return function uncurried(...args){ 63 | var ret = fn; 64 | 65 | for (let i = 0; i < args.length; i++) { 66 | ret = ret( args[i] ); 67 | } 68 | 69 | return ret; 70 | }; 71 | } 72 | 73 | function zip(arr1,arr2) { 74 | var zipped = []; 75 | arr1 = arr1.slice(); 76 | arr2 = arr2.slice(); 77 | 78 | while (arr1.length > 0 && arr2.length > 0) { 79 | zipped.push( [ arr1.shift(), arr2.shift() ] ); 80 | } 81 | 82 | return zipped; 83 | } 84 | 85 | function compose(...fns) { 86 | return function composed(result){ 87 | // copy the array of functions 88 | var list = fns.slice(); 89 | 90 | while (list.length > 0) { 91 | result = list.pop()( result ); 92 | } 93 | 94 | return result; 95 | }; 96 | } 97 | 98 | function pipe(...fns) { 99 | return function piped(result){ 100 | var list = fns.slice(); 101 | 102 | while (list.length > 0) { 103 | result = list.shift()( result ); 104 | } 105 | 106 | return result; 107 | }; 108 | } 109 | 110 | function prop(name,obj) { 111 | return obj[name]; 112 | } 113 | 114 | function setProp(name,obj,val) { 115 | var o = Object.assign( {}, obj ); 116 | o[name] = val; 117 | return o; 118 | } 119 | 120 | function unboundMethod(methodName,argCount = 2) { 121 | return curry( 122 | (...args) => { 123 | var obj = args.pop(); 124 | return obj[methodName]( ...args ); 125 | }, 126 | argCount 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | 3 | [![沪江Web前端团队](https://cdn.rawgit.com/Hujiang-FE/icons/fff32467/logo.svg)](https://github.com/hujiang-fe) 4 | 5 | > 参与者(排名不分先后):[阿希](https://github.com/aximario)、[blueken](https://github.com/blueken)、[brucecham](https://github.com/brucecham)、[cfanlife](https://github.com/cfanlife)、[dail](https://github.com/dail)、[kyoko-df](https://github.com/kyoko-df)、[l3ve](https://github.com/l3ve)、[lilins](https://github.com/lilins)、[LittlePineapple](https://github.com/LittlePineapple)、[MatildaJin](https://github.com/MatildaJin)、[冬青](https://github.com/miaodongqing)、[pobusama](https://github.com/pobusama)、[Cherry](https://github.com/sunshine940326)、[萝卜](https://github.com/torrac12)、[vavd317](https://github.com/vavd317)、[vivaxy](https://github.com/vivaxy)、[萌萌](https://github.com/yanyixin)、[zhouyao](https://github.com/zhouyao) 6 | 7 | > 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。 8 | 9 | 本书主要探索函数式编程[\[1\]](#note1)(FP)的核心思想。在此过程中,作者不会执着于使用大量复杂的概念来进行诠释,这也是本书的特别之处。我们在 JavaScript 中应用的仅仅是一套基本的函数式编程概念的子集。我称之为“轻量级函数式编程(FLP)”。 10 | 11 | **注释:** 题目中使用了“轻量”二字,然而这并不是一本“轻松的”“入门级”书籍。本书是严谨的,充斥着各种复杂的细节,适合拥有扎实 JS 知识基础的阅读者进行研读。“轻量”意味着范围缩小。通常来说,关于函数式编程的 JavaScript 书籍都热衷于拓展阅读者的知识面,并企图覆盖更多的知识点。而本书则对于每一个话题都进行了深入的探究,尽管这种探究是小范围进行的。 12 | 13 | 让我们面对这个事实:除非你已经是函数式编程高手中的一员(至少我不是!),否则类似“一个单子仅仅是自函子中的幺半群”这类说法对我们来说毫无意义。 14 | 15 | 这并不是说,各种复杂繁琐的概念是**无意义**的,更不是说,函数式编程者滥用了它们。一旦你完全掌握了轻量的函数式编程内容,你将会/但愿会想要对函数式编程的各种概念进行更正式更系统的学习,并且你一定会对它们的意义和原因有更深入的理解。 16 | 17 | 但是我更想要让你能够**现在**就把一些函数式编程的基础运用到 JavaScript 编程过程中去,因为我相信这会帮助你写出更优秀的,更**符合逻辑**的代码。 18 | 19 | **更多关于本书背后的动机和各种观点讨论,请参看[前言](preface.md)。** 20 | 21 | ## 本书 22 | 23 | [目录](toc.md) 24 | 25 | * [引言](foreword.md) (by [Brian Lonsdorf aka "Prof Frisby"](https://twitter.com/DrBoolean)) 26 | * [前言](preface.md) 27 | * [第一章:为什么使用函数式编程?](ch1.md) 28 | * [第二章:函数基础](ch2.md) 29 | * [第三章:管理函数的输入](ch3.md) 30 | * [第四章:组合函数](ch4.md) 31 | * [第五章:减少副作用](ch5.md) 32 | * [第六章:值的不可变性](ch6.md) 33 | * [第七章:闭包 vs 对象](ch7.md) 34 | * [第八章:列表操作](ch8.md) 35 | * [第九章:递归](ch9.md) 36 | * [第十章:异步的函数式](ch10.md) 37 | * [第十一章:融会贯通](ch11.md) 38 | * [附录 A :Transducing](apA.md) 39 | * [附录 B :谦虚的 Monad](apB.md) 40 | * [附录 C :函数式编程函数库](apC.md) 41 | 42 | ## 关于出版 43 | 44 | 本书主要在 [on Leanpub](https://leanpub.com/fljs/) 平台上以电子版本的形式进行出版。我也尝试出售本书的纸质版本,但没有确定的方案。 45 | 46 | 除了购买本书以外,如果你想要对本书作一些物质上的捐赠,请在 [patreon](https://www.patreon.com/getify) 上进行操作。本书作者感谢你的慷慨解囊。 47 | 48 | [![patreon.png](https://s11.postimg.org/axpzguh77/patreon.png)](https://www.patreon.com/getify) 49 | 50 | ## 真人教学课程 51 | 52 | 本书内容大多源自于我教授的一个同名课程(以公司举办的公开或内部研讨会这样的形式进行)。 53 | 54 | [http://getify.me](http://getify.me) 55 | 56 | 如果你喜欢本书的内容,并希望组织此类课程,或者组织关于其他 JS/HTML5/Node.js 课程,请通过以下方式联系我: 57 | [http://getify.me](http://getify.me) 58 | 59 | ## 在线视频课程 60 | 61 | 我还提供一些可以在线点播的 JS 培训课程。我在 [Frontend Masters](https://FrontendMasters.com) 上开办课程,例如我的 [Functional-Lite JS](https://frontendmasters.com/courses/functional-js-lite/) 研讨会。还有一些课程发布在 [PluralSight](https://www.pluralsight.com/search?q=kyle%20simpson&categories=all) 上。 62 | 63 | ## Contributions 64 | 65 | ## 关于内容贡献 66 | 67 | **非常欢迎**对于本书的任何内容贡献。但是在提交 PR 之前**请务必**认真阅读 [Contributions Guidelines](CONTRIBUTING.md)。 68 | 69 | ## License & Copyright 70 | 71 | ## 版权 72 | 73 | 本书所有的材料和内容都归属 (c) 2016-2017 Kyle Simpson 所有。 74 | 75 | Creative Commons License
本书根据Creative Commons Attribution-NonCommercial-NoDerivs 4.0 Unported License 进行授权许可. 76 | 77 | 1. FP,本书统称为函数式编程。 78 | 79 | -------------------------------------------------------------------------------- /fig17.svg: -------------------------------------------------------------------------------- 1 | 2 |
Imperative
Imperative
Declarative / FP
Declarative / FP
Readability
Readability
Where many developers give up on learning FP
[Not supported by viewer]
-------------------------------------------------------------------------------- /ch11-code/stock-ticker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var stockTickerUI = { 4 | 5 | updateStockElems(stockInfoChildElemList,data) { 6 | var getDataVal = curry( reverseArgs( prop ), 2 )( data ); 7 | var extractInfoChildElemVal = pipe( 8 | getClassName, 9 | stripPrefix( /\bstock-/i ), 10 | getDataVal 11 | ); 12 | var orderedDataVals = 13 | map( extractInfoChildElemVal )( stockInfoChildElemList ); 14 | var elemsValsTuples = 15 | filterOut( function updateValueMissing([infoChildElem,val]){ 16 | return val === undefined; 17 | } ) 18 | ( zip( stockInfoChildElemList, orderedDataVals ) ); 19 | 20 | // !!SIDE EFFECTS!! 21 | compose( each, spreadArgs )( setDOMContent ) 22 | ( elemsValsTuples ); 23 | }, 24 | 25 | updateStock(tickerElem,data) { 26 | var getStockElemFromId = curry( getStockElem )( tickerElem ); 27 | var stockInfoChildElemList = pipe( 28 | getStockElemFromId, 29 | getStockInfoChildElems 30 | ) 31 | ( data.id ); 32 | 33 | return stockTickerUI.updateStockElems( 34 | stockInfoChildElemList, 35 | data 36 | ); 37 | }, 38 | 39 | addStock(tickerElem,data) { 40 | var [stockElem, ...infoChildElems] = map( 41 | createElement 42 | ) 43 | ( [ "li", "span", "span", "span" ] ); 44 | var attrValTuples = [ 45 | [ ["class","stock"], ["data-stock-id",data.id] ], 46 | [ ["class","stock-name"] ], 47 | [ ["class","stock-price"] ], 48 | [ ["class","stock-change"] ] 49 | ]; 50 | var elemsAttrsTuples = 51 | zip( [stockElem, ...infoChildElems], attrValTuples ); 52 | 53 | // !!SIDE EFFECTS!! 54 | each( function setElemAttrs([elem,attrValTupleList]){ 55 | each( 56 | spreadArgs( partial( setElemAttr, elem ) ) 57 | ) 58 | ( attrValTupleList ); 59 | } ) 60 | ( elemsAttrsTuples ); 61 | 62 | // !!SIDE EFFECTS!! 63 | stockTickerUI.updateStockElems( infoChildElems, data ); 64 | reduce( appendDOMChild )( stockElem )( infoChildElems ); 65 | tickerElem.appendChild( stockElem ); 66 | } 67 | 68 | }; 69 | 70 | var getDOMChildren = pipe( 71 | listify, 72 | flatMap( 73 | pipe( 74 | curry( prop )( "childNodes" ), 75 | Array.from 76 | ) 77 | ) 78 | ); 79 | var createElement = document.createElement.bind( document ); 80 | var getElemAttrByName = curry( reverseArgs( getElemAttr ), 2 ); 81 | var getStockId = getElemAttrByName( "data-stock-id" ); 82 | var getClassName = getElemAttrByName( "class" ); 83 | var ticker = document.getElementById( "stock-ticker" ); 84 | var stockTickerUIMethodsWithDOMContext = map( 85 | curry( reverseArgs( partial ), 2 )( ticker ) 86 | ) 87 | ( [ stockTickerUI.addStock, stockTickerUI.updateStock ] ); 88 | var subscribeToObservable = 89 | pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) ); 90 | var stockTickerObservables = [ newStocks, stockUpdates ]; 91 | 92 | // !!SIDE EFFECTS!! 93 | each( subscribeToObservable ) 94 | ( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) ); 95 | 96 | 97 | // ********************* 98 | 99 | function stripPrefix(prefixRegex) { 100 | return function mapperFn(val) { 101 | return val.replace( prefixRegex, "" ); 102 | }; 103 | } 104 | 105 | function listify(listOrItem) { 106 | if (!Array.isArray( listOrItem )) { 107 | return [ listOrItem ]; 108 | } 109 | return listOrItem; 110 | } 111 | 112 | function isTextNode(node) { 113 | return node && node.nodeType == 3; 114 | } 115 | 116 | function getElemAttr(elem,prop) { 117 | return elem.getAttribute( prop ); 118 | } 119 | 120 | function setElemAttr(elem,prop,val) { 121 | // !!SIDE EFFECTS!! 122 | return elem.setAttribute( prop, val ); 123 | } 124 | 125 | function matchingStockId(id) { 126 | return function isStock(node){ 127 | return getStockId( node ) == id; 128 | }; 129 | } 130 | 131 | function isStockInfoChildElem(elem) { 132 | return /\bstock-/i.test( getClassName( elem ) ); 133 | } 134 | 135 | function getStockElem(tickerElem,stockId) { 136 | return pipe( 137 | getDOMChildren, 138 | filterOut( isTextNode ), 139 | filterIn( matchingStockId( stockId ) ) 140 | ) 141 | ( tickerElem ); 142 | } 143 | 144 | function getStockInfoChildElems(stockElem) { 145 | return pipe( 146 | getDOMChildren, 147 | filterOut( isTextNode ), 148 | filterIn( isStockInfoChildElem ) 149 | ) 150 | ( stockElem ); 151 | } 152 | 153 | function appendDOMChild(parentNode,childNode) { 154 | // !!SIDE EFFECTS!! 155 | parentNode.appendChild( childNode ); 156 | return parentNode; 157 | } 158 | 159 | function setDOMContent(elem,html) { 160 | // !!SIDE EFFECTS!! 161 | elem.innerHTML = html; 162 | return elem; 163 | } 164 | -------------------------------------------------------------------------------- /apC.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 附录 C:函数式编程函数库 3 | 4 | 如果您已经从头到尾通读了此书,请花一分钟的时间停下来回顾一下从第 1 章到现在的收获。相当漫长的一段旅程,不是吗?希望您已经收获了大量新知识,并用函数式的方式思考你的程序。 5 | 6 | 在本书即将完结时,我想给你提供一些关于使用官方函数式编程函数库的快速指南。注意这并不是一个详细的文档,而是将你在结束“轻量级函数式编程”后进军真正的函数式编程时应该注意的东西快速梳理一下。 7 | 8 | 如果有可能,我建议你**不要**做重新造轮子这样的事情。如果你找到了一个能满足你需求的函数式编程函数库,那么用它就对了。只有在你实在找不到合适的库来应对你面临的问题时,才应该使用本书提供的辅助实用函数 —— 或者自己造轮子。 9 | 10 | ## 目录 11 | 12 | 在本书第 1 章曾列出了一个函数式编程库的列表,现在我们来扩展这个列表。我们不会涉及所有的库(它们之中有许多重复的内容),但下面这些你应该有所关注: 13 | 14 | * [Ramda](http://ramdajs.com):通用函数式编程实用函数 15 | * [Sanctuary](https://github.com/sanctuary-js/sanctuary):函数式编程类型 Ramda 伴侣 16 | * [lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide):通用函数式编程实用函数 17 | * [functional.js](http://functionaljs.com/):通用函数式编程实用函数 18 | * [Immutable](https://github.com/facebook/immutable-js):不可变数据结构 19 | * [Mori](https://github.com/swannodette/mori):(受到 ClojureScript 启发)不可变数据结构 20 | * [Seamless-Immutable](https://github.com/rtfeldman/seamless-immutable):不可变数据助手 21 | * [tranducers-js](https://github.com/cognitect-labs/transducers-js):数据转换器 22 | * [monet.js](https://github.com/cwmyers/monet.js):Monad 类型 23 | 24 | 上面的列表只列出了所有函数式编程库的一小部分,并不是说没有在列表中列出的库就不好,也不是说列表中列出的就是最佳选择,总之这只是 JavaScript 函数式编程世界中的一瞥。您可以前往[这里](https://github.com/stoeffel/awesome-fp-js)查看更完整的函数式编程资源。 25 | 26 | [Fantasy Land](https://github.com/fantasyland/fantasy-land)(又名 FL)是函数式编程世界中十分重要的学习资源之一,与其说它是一个库,不如说它是一本百科全书。 27 | 28 | Fantasy Land 不是一份为初学者准备的轻量级读物,而是一个完整而详细的 JavaScript 函数式编程路线图。为了尽可能提升互通性,FL 已经成为 JavaScript 函数式编程库遵循的实际标准。 29 | 30 | Fantasy Land 与“轻量级函数式编程”的概念相反,它以火力全开的姿态进军 JavaScript 的函数式编程世界。也就是说,当你的能力超越本书时,FL 将会成为你接下来前进的方向。我建议您将其保存在收藏夹中,并在您使用本书的概念进行至少 6 个月的实战练习之后再回来。 31 | 32 | ## Ramda (0.23.0) 33 | 34 | 摘自 [Ramda 文档](http://ramdajs.com/): 35 | 36 | > Ramda 函数自动地被柯里化。 37 | > 38 | > Ramda 函数的参数经过优化,更便于柯里化。需要被操作的数据往往放在最后提供。 39 | 40 | 我认为合理的设计是 Ramda 的优势之一。值得注意的是,Ramda 的柯里化形式(似乎大多数的库都是这种形式)是我们在第 3 章中讨论过的“松散柯里化”。 41 | 42 | 第 3 章的最后一个例子 —— 我们定义无值(point-free)工具函数 `printIf()` —— 可以在 Ramda 中这样实现: 43 | 44 | ```js 45 | function output(msg) { 46 | console.log( msg ); 47 | } 48 | 49 | function isShortEnough(str) { 50 | return str.length <= 5; 51 | } 52 | 53 | var isLongEnough = R.complement( isShortEnough ); 54 | 55 | var printIf = R.partial( R.flip( R.when ), [output] ); 56 | 57 | var msg1 = "Hello"; 58 | var msg2 = msg1 + " World"; 59 | 60 | printIf( isShortEnough, msg1 ); // Hello 61 | printIf( isShortEnough, msg2 ); 62 | 63 | printIf( isLongEnough, msg1 ); 64 | printIf( isLongEnough, msg2 ); // Hello World 65 | ``` 66 | 67 | 与我们在第 3 章中的实现相比有几处不同: 68 | 69 | * 我们使用 `R.complement(..)` 而不是 `not(..)` 在 `isShortEnough(..)` 周围新建一个否定函数 `isLongEnough(..)`。 70 | 71 | * 使用 `R.flip(..)` 而不是 `reverseArgs(..)` 函数,值得一提的是,`R.flip(..)` 仅交换头两个参数,而 `reverseArgs(..)` 会将所有参数反向。在这种情景下,`flip(..)` 更加方便,所以我们不再需要使用 `partialRight(..)` 或其他投机取巧的方式进行处理。 72 | 73 | * `R.partial(..)` 所有的后续参数以单个数组的形式存在。 74 | 75 | * 因为 Ramda 使用松散柯里化,因此我们不需要使用 `R.uncurryN(..)` 来获得一个包含所有参数的 `printIf(..)`。如果我们这样做了,就相当于使用 `R.uncurryN(2, ..)` 包裹 `R.partial(..)` 进行调用,这是完全没有必要的。 76 | 77 | Ramda 是一个受欢迎的、功能强大的库。如果你想要在你的代码中实践 FP,从 Ramda 开始是个不错的选择。 78 | 79 | ## Lodash/fp (4.17.4) 80 | 81 | Lodash 是整个 JS 生态系统中最受欢迎的库。Lodash 团队发布了一个“FP 友好”的 API 版本 —— ["lodash/fp"](https://github.com/lodash/lodash/wiki/FP-Guide)。 82 | 83 | 在第 8 章中,我们讨论了合并独立列表操作(`map(..)`、`filter(..)` 以及 `reduce(..)`)。使用“lodash/fp”时,你可以这样做: 84 | 85 | ```js 86 | var sum = (x,y) => x + y; 87 | var double = x => x * 2; 88 | var isOdd = x => x % 2 == 1; 89 | 90 | fp.compose( [ 91 | fp.reduce( sum )( 0 ), 92 | fp.map( double ), 93 | fp.filter( isOdd ) 94 | ] ) 95 | ( [1,2,3,4,5] ); // 18 96 | ``` 97 | 98 | 与我们所熟知的 `_.` 命名空间前缀不同,“lodash/fp”将 `fp.` 定义为其命名空间前缀。我发现一个很有用的区别,就是 `fp.` 比 `_.` 更容易识别。 99 | 100 | 注意 `fp.compose(..)`(在常规 lodash 版本中又名 `_.flowRight(..)`)接受一个函数数组,而不是独立的函数作为参数。 101 | 102 | lodash 拥有良好的稳定性、广泛的社区支持以及优秀的性能,是你探索 FP 世界时的坚实后盾。 103 | 104 | ## Mori (0.3.2) 105 | 106 | 在第 6 章中,我们已经快速浏览了一下 Immutable.js 库,该库可能是最广为人知的不可变数据结构库了。 107 | 108 | 让我们来看一下另一个流行的库:[Mori](https://github.com/swannodette/mori)。Mori 设计了一套与众不同(从表面上看更像函数式编程)的 API:它使用独立的函数而不直接在值上操作。 109 | 110 | ```js 111 | var state = mori.vector( 1, 2, 3, 4 ); 112 | 113 | var newState = mori.assoc( 114 | mori.into( state, Array.from( {length: 39} ) ), 115 | 42, 116 | "meaning of life" 117 | ); 118 | 119 | state === newState; // false 120 | 121 | mori.get( state, 2 ); // 3 122 | mori.get( state, 42 ); // undefined 123 | 124 | mori.get( newState, 2 ); // 3 125 | mori.get( newState, 42 ); // "meaning of life" 126 | 127 | mori.toJs( newState ).slice( 1, 3 ); // [2,3] 128 | ``` 129 | 130 | 这是一个指出关于 Mori 的一些有趣的事情的例子: 131 | 132 | * 使用 `vector` 而不是 `list`(你可能会想用的),主要是因为文档说它的行为更像 JavaScript 中的数组。 133 | 134 | * 不能像在操作原生 JavaScript 数组那样在任意位置设置值,在 vector 结构中,这将会抛出异常。因此我们必须使用 `mori.into(..)`,传入一个合适长度的数组来扩展 vector 的长度。在上例中,vector 有 43 个可用位置(4 + 39),所以我们可以在最后一个位置(索引为 42)上写入 `"meaning of life"` 这个值。 135 | 136 | * 使用 `mori.into(..)` 创建一个较大的 vector,再用 `mor.assoc(..)` 根据这个 vector 创建另一个 vector 的做法听起来效率低下。但是,不可变数据结构的好处在于数据不会进行克隆,每次“改变”发生,新的数据结构只会追踪其与旧数据结构的不同之处。 137 | 138 | Mori 受到 ClojureScript 极大的启发。如果您有 ClojureScript 编程经验,那您应该对 Mori 的 API 感到非常熟悉。由于我没有这种编程经验,因此我感觉 Mori 中的方法名有点奇怪。 139 | 140 | 但相比于在数据上直接调用方法,我真的很喜欢调用独立方法这样的设计。Mori 还有一些自动返回原生 JavaScript 数组的方法,用起来非常方便。 141 | 142 | ## 总结 143 | 144 | JavaScript 不是作为函数式编程语言来特别设计的。不过其自身的确拥有很多对函数式编程非常友好基础语法(例如可作为变量的函数、闭包等)。本章提及的库将使你更方便的进行函数式编程。 145 | 146 | 有了本书中函数式编程概念的武装,相信你已经准备好开始处理现实世界的代码了。找一个优秀的函数式编程库来用,然后练习,练习,再练习。 147 | 148 | 就是这样了。我已经将我目前所知道的知识分享给你了。我在此正式认证您为“JavaScript 轻量级函数式编程”程序员!好了,是时候结束我们一起学习 FP 这部分的“章节”了,但我的学习之旅还将继续。我希望,你也是! 149 | -------------------------------------------------------------------------------- /ch1.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 第 1 章:为什么使用函数式编程? 3 | 4 | > 函数式编程人员: 没有任何一个函数式编程者会把变量命名为 x,函数命名为 f,模块代码命名为“zygohistomorphic prepromorphism”。 5 | > 6 | > James Iry ‏@jamesiry 5/13/15 7 | > 8 | > https://twitter.com/jamesiry/status/598547781515485184 9 | 10 | 函数式编程(FP),不是一个新的概念,它几乎贯穿了整个编程史。我不确定这么说是否合理,但是很确定的一点是:直到最近几年,函数式编程才成为整个开发界的主流观念。所以我觉得函数式编程领域更像学者的领域。 11 | 12 | 然而一切都在变。不只是从编程语言的角度,一些库和框架都对函数式编程的兴趣空前高涨。你很可能也在读相关内容,因为你终于意识到函数式编程是不容忽视的东西。或者你跟我一样,已经尝试很多次去学函数式编程,但却很难理解所有的术语或数学符号。 13 | 14 | 无论你出于何目的翻阅本书,欢迎加入我们! 15 | 16 | ## 置信度 17 | 18 | 我有一个非常简单的前提,这是我作为软件开发老师(JavaScript)所做的一切基础:你不能信任的代码是你不明白的代码。此外,对你不信任或不明白的代码,你将不能确定这些代码是否符合你的业务场景。代码运行时也只能祈求好运。 19 | 20 | 信任是什么意思?信任是指你通过读代码,不仅是跑代码,就能理解这段代码能干什么事,而不只是停留在它可能是干什么的层面。也许我们不应该总倾向于通过运行测试程序,来验证程序的正确性。我并不是说测试不好,而是说我们应该对代码了如指掌,这样我们在运行测试代码之前就会知道它肯定能跑通。 21 | 22 | 通过读代码就能对我们的程序更有信心,我相信函数式编程技术的基础构成,是本着这种心态设计的。理解函数式编程并在程序中用心实践的人,得益于函数式编程已经被证实的原则,能够写出可读性高和可验证的代码,来达到他们想要的目的。 23 | 24 | 我希望你能通过理解轻量级函数式编程的原则,对你编写的代码更有信心,并且能在之后的路上越走越好。 25 | 26 | ## 交流渠道 27 | 28 | 函数式编程为何如此重要?为了回答这个问题,我们退一万步先来讨论一下编程本身的重要性。 29 | 30 | 我认为代码不是电脑中的一堆指令,这么说你可能感到很奇怪。事实上,代码能指示电脑运行就是一个意外的惊喜。 31 | 32 | 我深信代码的主要作用是方便人与人交流。 33 | 34 | 根据以往经验你可能知道,有时候花很多时间“编程”其实只是读现有的代码。我们的大部分时间其实都是在维护别人的代码(或自己的老代码),只有少部分时间是在敲新代码。 35 | 36 | 你知道研究过这个话题的专家给出了怎样的数据吗?我们在维护代码过程中 70% 的时间花在了阅读和理解代码上。 也难怪全球程序员每天的平均代码行数是 5 行。我们一天花七个半小时用来读代码,然后找出这 5 行代码应该写在哪里。 37 | 38 | 我想我们应该更多的关注一下代码的可读性。可能的话,不妨多花点时间在可读性上。顺便提一句,可读性并不意味着最少的代码量,对代码的熟悉程度也会影响代码的可读性(这一点也是被证实过的)。 39 | 40 | 因此,如果我们要花费更多的时间来关注代码的可读性和可理解性,那么函数式编程为我们提供了一种非常方便的模式。函数式编程的原则是完善的,经过了深入的研究和审查,并且可以被验证。 41 | 42 | 如果我们使用函数式编程原则,我相信我们将写出更容易理解的代码。一旦我们知道这些原则,它们将在代码中被识别和熟悉,这意味着当我们读取一段代码时,我们将花费更少的时间来进行定位。我们的重点将在于如何组建所有已知的“乐高片段”,而不是这些“乐高片段”是什么意思。 43 | 44 | 函数式编程是编写可读代码的最有效工具之一(可能还有其他)。这就是为什么函数式编程如此重要。 45 | 46 | ### 可读性曲线 47 | 48 | 很重要的是,我先花点时间来讲述一种多年来让我感到困惑和沮丧的现象,在写本书时该问题尤为尖锐。 49 | 50 | 这也可能是许多开发人员会遇到的问题。亲爱的读者,当你读这篇文章的时候,你可能会发现自己也会遇到同样的状况。但是要振作起来,坚持下去,陡峭的学习曲线总会过去。 51 | 52 |

53 | 54 |

55 | 56 | 我们将在下一章更深入的讨论这个问题。但是你可能写过一些命令式的代码,像 `if` 语句和 `for` 循环这样的语句。这些语句旨在精确地指导计算机**如何**完成一件事情。声明式代码,以及我们努力遵循函数式编程原则所写出的代码,更专注于描述最终的结果。 57 | 58 | 还有个残酷的问题摆在眼前,我在写本书时花费了很多时间在此问题上:我需要花费更多的精力和编写更多的代码来提高代码的可读性,尽量减少乃至消除可能会引入程序错误的代码部分。 59 | 60 | 如果你期望用函数式编程重构过的代码能够立刻变得更美观、优雅、智能和简洁的话,这个有点不太现实,这个变化是需要一个过程的。 61 | 62 | 函数式编程以另一种方式来思考代码应该如何组织才能使数据流更加明显,并能让读者很快理解你的思想。这种努力是非常值得的,然而过程很艰辛,你可能需要花很多时间基于函数式编程来调整代码直到代码可读性变得好一些。 63 | 64 | 另外,我的经验是,转换为声明式的代码之前,大约需要做六次尝试。对我来说,编写符合函数式编程的代码更像是一个过程,而不是从一个范例到另一个范例的二进制转换。 65 | 66 | 我也会经常对写过的代码进行重构。就是说,写完一段代码,过几个小时或一天再看会有不一样的感觉。通常,重构之前的代码是比较混乱不堪,所以需要反复调整。 67 | 68 | 函数式编程的过程并没有让我在艺术的画布上笔下生辉,让观众拍案叫好。相反,编程的过程很艰辛且历历在目,感觉像坐在一辆不靠谱的马车穿过一片杂草丛生的灌木树林。 69 | 70 | 我并不是试图打消你的激情,而是真切希望你也能够在编程的道路上披荆斩棘。过后我终于看到可读性曲线向上延伸,所有付出都是值得的,我相信你也会有同样的感受。 71 | 72 | ## 接受 73 | 74 | 我们要系统的学习函数式编程,探索发现最基本的原则,我相信规范的函数式编程编程者会遵循这些原则并把它们作为开发的框架。但在大多数情况下,我们大都选择避开晦涩的术语或数学符号,否则很容易使学习者受挫。 75 | 76 | 我觉得一项技术你怎么称呼它不重要,重要的是理解它是什么并且它是怎么工作的。这并不是说共享术语不重要,它无疑可以简化经验丰富的专业人士之间的交流。但对学习者来说,它有点分散人的注意力。 77 | 78 | 所以我希望这本书能更多地关注基本概念而不是花哨的术语。这并不是说没有术语,肯定会有。但不要太沉迷于华丽的词藻,追寻其背后的含义,这正是本书的目的。 79 | 80 | 我把这种欠缺正式实践的编程思想称为“轻量级函数式编程”,因为我认为真正的函数式编程的形式主义在于, 因为我认为如果你还不习惯函数式编程主张的思想,你可能很难用它。这不仅仅只是猜测,而是我的亲身经历。即使在传教函数式编程过程和完成这本书之后,我仍然可以说,函数式编程中术语和符号的形式化对于我来说是非常非常困难的。我已经再三尝试,发现大部分都是很难掌握的。 81 | 82 | 我知道很多函数式编程编程者会认为形式主义本身有助于学习。但我认为这是一个坑,当你试图用形式主义获得某种安慰时,你就会踩坑。但如果碰巧你有数学背景,甚至还有一些 CS 经验,这些问题对你来说就可能驾轻就熟。但是我们中的一些人不具备这些条件,不管我们怎么努力,形式主义总是阻碍我们前进。 83 | 84 | 因此,这本书介绍了一些我认为函数式编程会涉及到的概念,虽然不能直接让你受益但可以帮你逐步理解函数式编程整个过程。 85 | 86 | ## 你不需要它 87 | 88 | 如果你规划一个项目花了很长时间,那么别人一定会告诉你“YAGNI” —— “你不需要它”。这个原则主要来自极限编程,强调构建特性的高风险和成本,这个风险和成本源自于项目本身是否需要。 89 | 90 | 有时我们考虑到将来可能会用到一个功能,并且认为现在构建它能够使得构建其他应用时更容易,后来意识到我们猜错了,原来这个功能并不需要,或者需要的完全是另外一套。另外一种情形是我们预估的功能是正确的,但构建得太早的话,相当于占用了开发现有功能的时间。有点像赔了夫人又折兵。 91 | 92 | YAGNI 挑战,告诉我们:即使有的功能在某种情况下是反直觉的,我们也常常应该推迟构建,直到当前需要这个功能。我们倾向于夸大一个功能未来重构成本的心理估计,但往往这个重构是在将来需要时才会做。 93 | 94 | 上述情况对函数式编程也同样适用,不过我还是要先敲个警钟: 本书包含了大量你想去尝试的有趣的开发模式,但这不意味着你的代码一定要使用这些模式。 95 | 96 | 我与很多函数式编程开发人员的不同之处在于:你掌握了函数式编程并不意味着你一定得用它。此外,解决问题的方法很多,即使你掌握了更精炼的方法,能对维护和可扩展性更"经得起未来的考验",但更轻量的函数式编程模式可能更适合该场景。 97 | 98 | 一般来说,我建议你在代码中寻求平衡,并且当你掌握函数式编程的诀窍时,在应用的过程中也应保持谨慎。在决定某个模式或抽象概念是否能使得部分代码可读性提高,或是否只是引入更智能的库时,YAGNI 的原则同样适用。 99 | 100 | > 提醒一句,一些未曾用过的扩展点不仅浪费精力,而且可能妨碍你的工作。 101 | > 102 | > Jeremy D. Miller @jeremydmiller 2/20/15 103 | > 104 | > https://twitter.com/jeremydmiller/status/568797862441586688 105 | 106 | 记住,你编写的每一行代码之后都要有人来维护,这个人可能是你的团队成员,也可能是未来的你。如果代码写的太过复杂,那么无论谁来维护都会对你炫技式的故作聪明的做法倍感压力。 107 | 108 | 最好的代码是可读性高的代码,因为它在正确的(理想主义)和必然的(正确的)之间寻求到了恰到好处的平衡。 109 | 110 | ## 资源 111 | 112 | 我撰写这篇文章的过程参考了许多不同的资源。我相信你也会从中受益,所以我想花点时间把它们列出来。 113 | 114 | ### 书籍推荐 115 | 116 | 一些你务必要阅读的函数式编程 / JavaScript 书籍: 117 | 118 | * [Professor Frisby's Mostly Adequate Guide to Functional Programming](https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch1.html) by [Brian Lonsdorf](https://twitter.com/drboolean) 119 | * [JavaScript Allongé](https://leanpub.com/javascript-allonge) by [Reg Braithwaite](https://twitter.com/raganwald) 120 | * [Functional JavaScript](http://shop.oreilly.com/product/0636920028857.do) by [Michael Fogus](https://twitter.com/fogus) 121 | 122 | ### 博客和站点 123 | 124 | 一些其他作者和相关内容供查阅: 125 | 126 | * [Fun Fun Function Videos](https://www.youtube.com/watch?v=BMUiFMZr7vk) by [Mattias P Johansson](https://twitter.com/mpjme) 127 | * [Awesome函数式编程JS](https://github.com/stoeffel/awesome-fp-js) 128 | * [Kris Jenkins](http://blog.jenkster.com/2015/12/what-is-functional-programming.html) 129 | * [Eric Elliott](https://medium.com/@_ericelliott) 130 | * [James A Forbes](https://james-forbes.com/) 131 | * [James Longster](https://github.com/jlongster) 132 | * [André Staltz](http://staltz.com/) 133 | * [Functional Programming Jargon](https://github.com/hemanth/functional-programming-jargon#functional-programming-jargon) 134 | * [Functional Programming Exercises](https://github.com/InceptionCode/Functional-Programming-Exercises) 135 | 136 | ### 一些库 137 | 138 | 本书中的代码段不使用库。我们发现的每一个操作,将派生出如何在独立的、普通的 JavaScript 中实现它。然而,当你开始使用函数式编程构建更多的真正代码时,你很快就会使用现有库中所提供的更可靠高效的通用功能。 139 | 140 | 顺便说一下,你要确保检查你所使用的库函数的文档,以确保你知道它们是如何工作的。它与本文中构建的代码有许多相似之处,但毫无疑问即便跟最流行的库相比还是会存在一些差异。 141 | 142 | 下面是一些流行的 JavaScript 版本的函数式编程库,可以开启你的探索之路: 143 | 144 | * [Ramda](http://ramdajs.com) 145 | * [lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide) 146 | * [functional.js](http://functionaljs.com/) 147 | * [Immutable.js](https://github.com/facebook/immutable-js) 148 | 149 | 附录 C 展示了用到了本书中一些示例的库。 150 | 151 | ## 总结 152 | 153 | 这就是 JavaScript 轻量级函数式编程。我们的目标是学会与代码交流,而不是在符号或术语的大山下被压的喘不过气。希望这本书能开启你的旅程! 154 | -------------------------------------------------------------------------------- /fig18.svg: -------------------------------------------------------------------------------- 1 | 2 |
3
3
6
6
1
1
0
0
0: 4
0: 4
3: 1
3: 1
4: 2
4: 2
[4,6,1,1,2]
[4,6,1,1,2]
-------------------------------------------------------------------------------- /fig5.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fig6.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fig15.svg: -------------------------------------------------------------------------------- 1 | 2 |
baz()

var x;
[Not supported by viewer]
baz()

var x;
[Not supported by viewer]
bar()

var y;
[Not supported by viewer]
baz()

var x;
[Not supported by viewer]
bar()

var y;
[Not supported by viewer]
foo()

var z;
[Not supported by viewer]
Step 1
[Not supported by viewer]
Step 2
[Not supported by viewer]
Step 3
[Not supported by viewer]
-------------------------------------------------------------------------------- /fig8.svg: -------------------------------------------------------------------------------- 1 | 2 |
banana
banana
apple
apple
cherry
cherry
apricot
apricot
avocado
avocado
cantelope
cantelope
cucumber
cucumber
grape
grape
-------------------------------------------------------------------------------- /fig16.svg: -------------------------------------------------------------------------------- 1 | 2 |
baz()

var x;
[Not supported by viewer]
baz()

var x;
[Not supported by viewer]
bar()

var y;
[Not supported by viewer]
baz()

var x;
[Not supported by viewer]
bar()

var y;
[Not supported by viewer]
foo()

var z;
[Not supported by viewer]
Step 1
[Not supported by viewer]
Step 2
[Not supported by viewer]
Step 3
[Not supported by viewer]
-------------------------------------------------------------------------------- /apB.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 附录 B: 谦虚的 Monad 3 | 4 | 首先,我坦白:在开始写以下内容之前我并不太了解 Monad 是什么。我为了确认一些事情而犯了很多错误。如果你不相信我,去看看 [这本书 Git 仓库](https://github.com/getify/Functional-Light-JS) 中关于本章的提交历史吧! 5 | 6 | 我在本书中囊括了所有涉及 Monad 的话题。就像我写书的过程一样,每个开发者在学习函数式编程的旅程中都会经历这个部分。 7 | 8 | 尽管其他函数式编程的著作差不多都把 Monad 作为开始,而我们却只对它做了简要说明,并基本以此结束本书。在轻量级函数式编程中我确实没有遇到太多需要仔细考虑 Monad 的问题,这就是本文更有价值的原因。但是并不是说 Monad 是没用的或者是不普遍的 —— 恰恰相反,它很有用,也很流行。 9 | 10 | 函数式编程界有一个小笑话,几乎每个人都不得不在他们的文章或者博客里写 Monad 是什么,把它拎出来写就像是一个仪式。在过去的几年里,人们把 Monad 描述为卷饼、洋葱和各种各样古怪的抽象概念。我肯定不会重蹈覆辙! 11 | 12 | > 一个 Monad 仅仅是自函子 (endofunctor) 范畴中的一个 monoid 13 | 14 | 我们引用这句话来开场,所以把话题转到这个引言上面似乎是很合适的。可是才不会这样,我们不会讨论 Monad 、endofunctor 或者范畴论。这句引言不仅故弄玄虚而且华而不实。 15 | 16 | 我只希望通过我们的讨论,你不再害怕 Monad 这个术语或者这个概念了 —— 我曾经怕了很长一段时间 —— 并在看到该术语时知道它是什么。你可能,也只是可能,会正确地使用到它们。 17 | 18 | ## 类型 19 | 20 | 在函数式编程中有一个巨大的兴趣领域:类型论,本书基本上完全远离了该领域。我不会深入到类型论,坦白的说,我没有深入的能力,即使干了也吃力不讨好。 21 | 22 | 但是我要说,Monad 基本上是一个值类型。 23 | 24 | 数字 `42` 有一个值类型(number),它带有我们依赖的特征和功能。字符串 `"42"` 可能看起来很像,但是在编程里它有不同的用途。 25 | 26 | 在面向对象编程中,当你有一组数据(甚至是一个单独的离散值),并且想要给它绑上一些行为,那么你将创建一个对象或者类来表示 "type"。接着实例就成了该类型的一员。这种做法通常被称为 “数据结构”。 27 | 28 | 我将会非常宽泛的使用数据结构这个概念,而且我断定,当我们在编程中为一个特定的值定义一组行为以及约束条件,并且将这些特征与值一起绑定在一个单一抽象概念上时,我们可能会觉得很有用。这样,当我们在编程中使用一个或多个这种值的时候,它们的行为会自然的出现,并且会使它们更方便的工作。方便的是,对你的代码的读者来说,是更有描述性和声明性的。 29 | 30 | Monad 是一种数据结构。是一种类型。它是一组使处理某个值变得可预测的特定行为。 31 | 32 | 回顾第 8 章,我们谈到了函子(functor):包括一个值和一个用来对构成函子的数据执行操作的类 map 实用函数。Monad 是一个包含一些额外行为的函子(functor)。 33 | 34 | ## 松散接口 35 | 36 | 实际上,Monad 并不是单一的数据类型,它更像是相关联的数据类型集合。它是一种根据不同值的需要而用不同方式实现的接口。每种实现都是一种不同类型的 Monad。 37 | 38 | 例如,你可能阅读 "Identity Monad"、"IO Monad"、"Maybe Monad"、"Either Monad" 或其他形形色色的字眼。他们中的每一个都有基本的 Monad 行为定义,但是它根据每个不同类型的 Monad 用例来继承或者重写交互行为。 39 | 40 | 可是它不仅仅是一个接口,因为它不只是使对象成为 Monad 的某些 API 方法的实现。对这些方法的交互的保障是必须的,是 monadic 的。这些众所周知的常量对于使用 Monad 提高可读性是至关重要的;另外,它是一个特殊的数据结构,读者必须全部阅读才能明白。 41 | 42 | 事实上,这些 Monad 方法的名字和真实接口授权的方式甚至没有一个统一的标准;Monad 更像是一个松散接口。有些人称这些方法为 `bind(..)`,有些称它为 `chain(..)`,还有些称它为 `flatMap(..)`,等等。 43 | 44 | 所以,Monad 是一个对象数据结构,并且有充足的方法(几乎任何名称或排序),至少满足了 Monad 定义的主要行为需求。每一种 Monad 都基于最少数量的方法来进行不同的扩展。但是,因为它们在行为上都有重叠,所以一起使用两种不同的 Monad 仍然是直截了当和可控的。 45 | 46 | 从某种意义上说,Monad 更像是接口。 47 | 48 | ## Maybe 49 | 50 | 在函数式编程中,像 Maybe 这样涵盖 Monad 是很普遍的。事实上,Maybe Monad 是另外两个更简单的 Monad 的搭配:Just 和 Nothing。 51 | 52 | 既然 Monad 是一个类型,你可能认为我们应该定义 `Maybe` 作为一个要被实例化的类。这虽然是一种有效的方法,但是它引入了 `this` 绑定的问题,所以在这里我不想讨论;相反,我打算使用一个简单的函数和对象的实现方式。 53 | 54 | 以下是 Maybe 的最简单的实现: 55 | 56 | ```js 57 | var Maybe = { Just, Nothing, of/* 又称:unit,pure */: Just }; 58 | 59 | function Just(val) { 60 | return { map, chain, ap, inspect }; 61 | 62 | // ********************* 63 | 64 | function map(fn) { return Just( fn( val ) ); } 65 | // 又称:bind, flatMap 66 | function chain(fn) { return fn( val ); } 67 | function ap(anotherMonad) { return anotherMonad.map( val ); } 68 | 69 | function inspect() { 70 | return `Just(${ val })`; 71 | } 72 | } 73 | 74 | function Nothing() { 75 | return { map: Nothing, chain: Nothing, ap: Nothing, inspect }; 76 | 77 | // ********************* 78 | 79 | function inspect() { 80 | return "Nothing"; 81 | } 82 | } 83 | ``` 84 | 85 | **注意:** `inspect(..)` 方法只用于我们的示例中。从 Monad 的角度来说,它并没有任何意义。 86 | 87 | 如果现在大部分都没有意义的话,不要担心。我们将会更专注的说明我们可以用它做什么,而不是过多的深入 Monad 背后的设计细节和理论。 88 | 89 | 所有的 Monad 一样,任何含有 `Just(..)` 和 `Nothing()` 的 Monad 实例都有 `map(..)`、`chain(..)`(也叫 `bind(..)` 或者 `flatMap(..)`)和 `ap(..)` 方法。这些方法及其行为的目的在于提供多个 Monad 实例一起工作的标准化方法。你将会注意到,无论 `Just(..)` 实例拿到的是怎样的一个 `val` 值, `Just(..)` 实例都不会去改变它。所有的方法都会创建一个新的 Monad 实例而不是改变它。 90 | 91 | Maybe 是这两个 Monad 的结合。如果一个值是非空的,它是 `Just(..)` 的实例;如果该值是空的,它则是 `Nothing()` 的实例。注意,这里由你的代码来决定 "空" 的意思,我们不做强制限制。下一节会详细介绍这一点。 92 | 93 | 但是 Monad 的价值在于不论我们有 `Just(..)` 实例还是 `Nothing()` 实例,我们使用的方法都是一样的。`Nothing()` 实例对所有的方法都有空操作定义。所以如果 Monad 实例出现在 Monad 操作中,它就会对 Monad 操作起短路(short-circuiting)作用。 94 | 95 | Maybe 这个抽象概念的作用是隐式地封装了操作和无操作的二元性。 96 | 97 | ### 与众不同的 Maybe 98 | 99 | JavaScript Maybe Monad 的许多实现都包含 `null` 和 `undefined` 的检查(通常在 `map(..)`中),如果是空的话,就跳过该 Monad 的特性行为。事实上,Maybe 被声称是有价值的,因为它自动地封装了空值检查得以在某种程度上短路了它的特性行为。 100 | 101 | 这是 Maybe 的典型说明: 102 | 103 | ```js 104 | // 代替不稳定的 `console.log( someObj.something.else.entirely )`: 105 | 106 | Maybe.of( someObj ) 107 | .map( prop( "something" ) ) 108 | .map( prop( "else" ) ) 109 | .map( prop( "entirely" ) ) 110 | .map( console.log ); 111 | ``` 112 | 113 | 换句话说,如果我们在链式操作中的任何一环得到一个 `null` 或者 `undefined` 值,Maybe 会智能的切换到空操作模式 —— 它现在是一个 `Nothing()` Monad 实例! —— 把剩余的链式操作都停止掉。如果一些属性丢失或者是空的话,嵌套的属性访问能安全的抛出 JS 异常。这是非常酷的而且很实用。 114 | 115 | 但是,我们这样实现的 Maybe 不是一个纯 Monad。 116 | 117 | Monad 的核心思想是,它必须对所有的值都是有效的,不能对值做任何检查 —— 甚至是空值检查。所以为了方便,这些其他的实现都是走的捷径。这是无关紧要的。但是当学习一些东西的时候,你应该先学习它的最纯粹的形式,然后再学习更复杂的规则。 118 | 119 | 我早期提供的 Maybe Monad 的实现不同于其他的 Maybe,就是它没有空置检查。另外,我们将 `Maybe` 作为 `Just(..)` 和 `Nothing()` 的非严格意义上的结合。 120 | 121 | 等一下,如果我们没有自动短路,那 Maybe 是怎么起作用的呢?!?这似乎就是它的全部意义。 122 | 123 | 不要担心,我们可以从外部提供简单的空值检查,Maybe Monad 其他的短路行为也还是可以很好的工作的。你可以在之前做一些 `someObj.something.else.entirely` 属性嵌套,但是我们可以做的更 “正确”: 124 | 125 | ```js 126 | function isEmpty(val) { 127 | return val === null || val === undefined; 128 | } 129 | 130 | var safeProp = curry( function safeProp(prop,obj){ 131 | if (isEmpty( obj[prop] )) return Maybe.Nothing(); 132 | return Maybe.of( obj[prop] ); 133 | } ); 134 | 135 | Maybe.of( someObj ) 136 | .chain( safeProp( "something" ) ) 137 | .chain( safeProp( "else" ) ) 138 | .chain( safeProp( "entirely" ) ) 139 | .map( console.log ); 140 | ``` 141 | 142 | 我们设计了一个用于空值检查的 `safeProp(..)` 函数,并选择了 `Nothing()` Monad 实例。或者把值包装在 `Just(..)` 实例中(通过 `Maybe.of(..)`)。然后我们用 `chain(..)` 替代 `map(..)`,它知道如何 “展开” `safeProp(..)` 返回的 Monad。 143 | 144 | 当遇到空值的时候,我们得到了一连串相同的短路。只是我们把这个逻辑从 Maybe 中排除了。 145 | 146 | 不管返回哪种类型的 Monad,我们的 `map(..)` 和 `chain(..)` 方法都有不变且可预测的反馈,这就是 Monad,尤其是 Maybe Monad 的好处。这难道不酷吗? 147 | 148 | ## Humble 149 | 150 | 现在我们对 Maybe 和它的作用有了更多的了解,我将会在它上面加一些小的改动 —— 我将通过设计 Maybe + Humble Monad 来添加一些转折并且加一些诙谐的元素。从技术上来说,`Humble(..)` 并不是一个 Monad,而是一个产生 Maybe Monad 实例的工厂函数。 151 | 152 | Humble 是一个使用 Maybe 来跟踪 `egoLevel` 数字状态的数据结构包装器。具体来说,`Humble(..)` 只有在他们自身的水平值足够低(少于 `42`)到被认为是 Humble 的时候才会执行生成的 Monad 实例;否则,它就是一个 `Nothing()` 空操作。这听起来真的和 Maybe 很像! 153 | 154 | 这是一个 Maybe + Humble Monad 工厂函数: 155 | 156 | ```js 157 | function Humble(egoLevel) { 158 | // 接收任何大于等于 42 的数字 159 | return !(Number( egoLevel ) >= 42) ? 160 | Maybe.of( egoLevel ) : 161 | Maybe.Nothing(); 162 | } 163 | ``` 164 | 165 | 你可能会注意到,这个工厂函数有点像 `safeProp(..)`,因为,它使用一个条件来决定是选择 Maybe 的 `Just(..)` 还是 `Nothing()`。 166 | 167 | 让我们来看一个基础用法的例子: 168 | 169 | ```js 170 | var bob = Humble( 45 ); 171 | var alice = Humble( 39 ); 172 | 173 | bob.inspect(); // Nothing 174 | alice.inspect(); // Just(39) 175 | ``` 176 | 177 | 如果 Alice 赢得了一个大奖,现在是不是在为自己感到自豪呢? 178 | 179 | ```js 180 | function winAward(ego) { 181 | return Humble( ego + 3 ); 182 | } 183 | 184 | alice = alice.chain( winAward ); 185 | alice.inspect(); // Nothing 186 | ``` 187 | 188 | `Humble( 39 + 3 )` 创建了一个 `chain(..)` 返回的 `Nothing()` Monad 实例,所以现在 Alice 不再有 Humble 的资格了。 189 | 190 | 现在,我们来用一些 Monad : 191 | 192 | ```js 193 | var bob = Humble( 41 ); 194 | var alice = Humble( 39 ); 195 | 196 | var teamMembers = curry( function teamMembers(ego1,ego2){ 197 | console.log( `Our humble team's egos: ${ego1} ${ego2}` ); 198 | } ); 199 | 200 | bob.map( teamMembers ).ap( alice ); 201 | // Humble 队列:41 39 202 | ``` 203 | 204 | 由于 `teamMembers(..)` 是柯里化的,`bob.map(..)` 的调用传入了 `bob` 自身的级别(`41`),并且创建了一个被其余的方法包装的 Monad 实例。在 **这个** Monad 中调用的 `ap(alice)` 调用了 `alice.map(..)`,并且传递给来自 Monad 的函数。这样做的效果是,Monad 的值已经提供给了 `teamMembers(..)` 函数,并且把显示的结果给打印了出来。 205 | 206 | 然而,如果一个 Monad 或者两个 Monad 实际上是 `Nothing()` 实例(因为它们本身的水平值太高了): 207 | 208 | ```js 209 | var frank = Humble( 45 ); 210 | 211 | bob.map( teamMembers ).ap( frank ); 212 | 213 | frank.map( teamMembers ).ap( bob ); 214 | ``` 215 | 216 | `teamMembers(..)` 永远不会被调用(也没有信息被打印出来),因为,`frank` 是一个 `Nothing()` 实例。这就是 Maybe monad 的作用,我们的 `Humble(..)` 工厂函数允许我们根据自身的水平来选择。赞! 217 | 218 | ### Humility 219 | 220 | 再来一个例子来说明 Maybe + Humble 数据结构的行为: 221 | 222 | ```js 223 | function introduction() { 224 | console.log( "I'm just a learner like you! :)" ); 225 | } 226 | 227 | var egoChange = curry( function egoChange(amount,concept,egoLevel) { 228 | console.log( `${amount > 0 ? "Learned" : "Shared"} ${concept}.` ); 229 | return Humble( egoLevel + amount ); 230 | } ); 231 | 232 | var learn = egoChange( 3 ); 233 | 234 | var learner = Humble( 35 ); 235 | 236 | learner 237 | .chain( learn( "closures" ) ) 238 | .chain( learn( "side effects" ) ) 239 | .chain( learn( "recursion" ) ) 240 | .chain( learn( "map/reduce" ) ) 241 | .map( introduction ); 242 | // 学习闭包 243 | // 学习副作用 244 | // 歇息递归 245 | ``` 246 | 247 | 不幸的是,学习过程看起来已经缩短了。我发现学习一大堆东西而不和别人分享,会使自我太膨胀,这对你的技术是不利的。 248 | 249 | 让我们尝试一个更好的方法: 250 | 251 | ```js 252 | var share = egoChange( -2 ); 253 | 254 | learner 255 | .chain( learn( "closures" ) ) 256 | .chain( share( "closures" ) ) 257 | .chain( learn( "side effects" ) ) 258 | .chain( share( "side effects" ) ) 259 | .chain( learn( "recursion" ) ) 260 | .chain( share( "recursion" ) ) 261 | .chain( learn( "map/reduce" ) ) 262 | .chain( share( "map/reduce" ) ) 263 | .map( introduction ); 264 | // 学习闭包 265 | // 分享闭包 266 | // 学习副作用 267 | // 分享副作用 268 | // 学习递归 269 | // 分享递归 270 | // 学习 map/reduce 271 | // 分享 map/reduce 272 | // 我只是一个像你一样的学习者 :) 273 | ``` 274 | 275 | 在学习中分享。是学习更多并且能够学的更好的最佳方法。 276 | 277 | ## 总结 278 | 279 | 说了这么多,那什么是 Monad ? 280 | 281 | Monad 是一个值类型,一个接口,一个有封装行为的对象数据结构。 282 | 283 | 但是这些定义中没有一个是有用的。这里尝试做一个更好的解释:Monad 是一个用更具有声明式的方式围绕一个值来组织行为的方法。 284 | 285 | 和这本书中的其他部分一样,在有用的地方使用 Monad,不要因为每个人都在函数式编程中讨论他们而使用他们。Monad 不是万金油,但它确实提供了一些有用的实用函数。 286 | -------------------------------------------------------------------------------- /fig3.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ch10.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 第 10 章:异步的函数式 3 | 4 | 阅读到这里,你已经学习了我所说的所有轻量级函数式编程的基础概念,在本章节中,我们将把这些概念应有到不同的情景当中,但绝对不会有新的知识点。 5 | 6 | 到目前为止,我们所说的一切都是同步的,意味着我们调用函数,传入参数后马上就会得到返回值。大部分的情况下是没问题的,但这几乎满足不了现有的 JS 应用。为了能在当前的 JS 环境里使用上函数式编程,我们需要去了解异步的函数式编程。 7 | 8 | 本章的目的是拓展我们对用函数式编程管理数据的思维,以便之后我们在更多的业务上应用。 9 | 10 | ## 时间状态 11 | 12 | 在你所有的应用里,最复杂的状态就是时间。当你操作的数据状态改变过程比较直观的时候,是很容易管理的。但是,如果状态随着时间因为响应事件而隐晦的变化,管理这些状态的难度将会成几何级增长。 13 | 14 | 我们在本文中介绍的函数式编程可以让代码变得更可读,从而增强了可靠性和可预见性。但是当你添加异步操作到你的项目里的时候,这些优势将会大打折扣。 15 | 16 | 必须明确的一点是:并不是说一些操作不能用同步来完成,或者触发异步行为很容易。协调那些可能会改变应用程序的状态的响应,这需要大量额外的工作。 17 | 18 | 所以,作为作者的你最好付出一些努力,或者只是留给阅读你代码的人一个难题,去弄清楚如果 A 在 B 之前完成,项目中状态是什么,还有相反的情况是什么?这是一个浮夸的问题,但以我的观点来看,这有一个确切的答案:如果可以把复杂的代码变得更容易理解,作者就必须花费更多心思。 19 | 20 | ### 减少时间状态 21 | 22 | 异步编程最为重要的一点是通过抽象时间来简化状态变化的管理。 23 | 24 | 为说明这一点,让我们先来看下一种有竞争状态(又称,时间复杂度)的糟糕情况,且必须手动去管理里面的状态: 25 | 26 | ```js 27 | var customerId = 42; 28 | var customer; 29 | 30 | lookupCustomer( customerId, function onCustomer(customerRecord){ 31 | var orders = customer ? customer.orders : null; 32 | customer = customerRecord; 33 | if (orders) { 34 | customer.orders = orders; 35 | } 36 | } ); 37 | 38 | lookupOrders( customerId, function onOrders(customerOrders){ 39 | if (!customer) { 40 | customer = {}; 41 | } 42 | customer.orders = customerOrders; 43 | } ); 44 | ``` 45 | 46 | 回调函数 `onCustomer(..)` 和 `onOrders(..)` 之间是互为竞争关系。假设他们都在运行,两者都有可能先运行,那将无法预测到会发生什么。 47 | 48 | 如果我们可以把 `lookupOrders(..)` 写到 `onCustomer(..)` 里面,那我们就可以确认 `onOrders(..)` 会在 `onCustomer(..)` 之后运行,但我们不能这么做,因为我们需要让 2 个查询同时执行。 49 | 50 | 所以,为了让这个基于时间的复杂状态正常化,我们用相应的 `if`-声明在各自的回调函数里来检查外部作用域的变量 `customer`。当各自的回调函数被执行,将会去检测 `customer` 的状态,从而确定各自的执行顺序,如果 `customer` 在回调函数里还没被定义,那他就是先运行的,否则则是第二个运行的。 51 | 52 | 这些代码可以运行,但是他违背了可读性的原则。时间复杂度让这个代码变得难以阅读。 53 | 54 | 让我们改用 JS promise 来把时间因素抽离出来: 55 | 56 | ```js 57 | var customerId = 42; 58 | 59 | var customerPromise = lookupCustomer( customerId ); 60 | var ordersPromise = lookupOrders( customerId ); 61 | 62 | customerPromise.then( function onCustomer(customer){ 63 | ordersPromise.then( function onOrders(orders){ 64 | customer.orders = orders; 65 | } ); 66 | } ); 67 | ``` 68 | 69 | 现在 `onOrders(..)` 回调函数存在 `onCustomer(..)` 回调函数里,所以他们各自的执行顺序是可以保证的。在各自的 `then(..)` 运行之前 `lookupCustomer(..)` 和 `lookupOrders(..)` 被分别的调用,两个查询就已经并行的执行完了。 70 | 71 | 这可能不太明显,但是这个代码里还有其他内在的竞争状态,那就是 promise 的定义没有被体现出来。如果 `orders` 的查询在把 `onOrders(..)` 回调函数被 `ordersPromise.then(..)` 调用前完成,那么就需要一些比较智能的 **东西** 来保存 `orders` 直到 `onOrders(..)` 能被调用。 同理,`record` (或者说`customer`)对象是否能在 `onCustomer(..)` 执行时被接收到。 72 | 73 | 这里的 **东西** 和我们之前讨论过的时间复杂度类似。但我们不必去担心这些复杂性,无论是编码或者是读(更为重要)这些代码的时候,因为对我们来说,promise 所处理的就是时间复杂度上的问题。 74 | 75 | promise 以时间无关的方式来作为一个单一的值。此外,获取 promise 的返回值是异步的,但却是通过同步的方法来赋值。或者说, promise 给 `=` 操作符扩展随时间动态赋值的功能,通过可靠的(时间无关)方式。 76 | 77 | 接下来我们将探索如何以相同的方式,在时间上异步地拓展本书之前同步的函数式编程操作。 78 | 79 | ## 积极的 vs 惰性的 80 | 81 | 积极的和惰性的在计算机科学的领域并不是表扬或者批评的意思,而是描述一个操作是否立即执行或者是延时执行。 82 | 83 | 我们在本例子中看到的函数式编程操作可以被称为积极的,因为它们同步(即时)地操作着离散的即时值或值的列表/结构上的值。 84 | 85 | 回忆下: 86 | 87 | ```js 88 | var a = [1,2,3] 89 | 90 | var b = a.map( v => v * 2 ); 91 | 92 | b; // [2,4,6] 93 | ``` 94 | 95 | 这里 `a` 到 `b` 的映射就是积极的,因为它在执行的那一刻映射了数组 `a` 里的所有的值,然后生成了一个新的数组 `b` 。即使之后你去修改 `a` ,比如说添加一个新的值到数组的最后一位,也不会影响到 `b` 的内容。这就是积极的函数式编程。 96 | 97 | 但是如果是一个惰性的函数式编程操作呢?思考如下情况: 98 | 99 | ```js 100 | var a = []; 101 | 102 | var b = mapLazy( a, v => v * 2 ); 103 | 104 | a.push( 1 ); 105 | 106 | a[0]; // 1 107 | b[0]; // 2 108 | 109 | a.push( 2 ); 110 | 111 | a[1]; // 2 112 | b[1]; // 4 113 | ``` 114 | 115 | 我们可以想象下 `mapLazy(..)` 本质上 “监听” 了数组 `a`,只要一个新的值添加到数组的末端(使用 `push(..)`),它都会运行映射函数 `v => v * 2` 并把改变后的值添加到数组 `b` 里。 116 | 117 | **注意:** `mapLazy(..)` 的实现没有被写出来,是因为它是虚构的方法,是不存在的。如果要实现 `a` 和 `b` 之间的惰性的操作,那么简单的数组就需要变得更加聪明。 118 | 119 | 考虑下把 `a` 和 `b` 关联到一起的好处,无论何时何地,你添加一个值进 `a` 里,它都将改变且映射到 `b` 里。它比同为声明式函数式编程的 `map(..)` 更强大,但现在它可以随时地变化,进行映射时你不用知道 `a` 里面所有的值。 120 | 121 | ## 响应式函数式编程 122 | 123 | 为了理解如何在2个值之间创建和使用惰性的映射,我们需要去抽象我们对列表(数组)的想法。 124 | 125 | 让我们来想象一个智能的数组,不只是简单地获得值,还是一个懒惰地接受和响应(也就是“反应”)值的数组。考虑下: 126 | 127 | ```js 128 | var a = new LazyArray(); 129 | 130 | var b = a.map( function double(v){ 131 | return v * 2; 132 | } ); 133 | 134 | setInterval( function everySecond(){ 135 | a.push( Math.random() ); 136 | }, 1000 ); 137 | ``` 138 | 139 | 至此,这段代码的数组和普通的没有什么区别。唯一不同的是在我们执行 `map(..)` 来映射数组 `a` 生成数组 `b` 之后,定时器在 `a` 里面添加随机的值。 140 | 141 | 但是这个虚构的 `LazyArray` 有点不同,它假设了值可以随时的一个一个添加进去。就像随时可以 `push(..)` 你想要的值一样。可以说 `b` 就是一个惰性映射 `a` 最终值的数组。 142 | 143 | 此外,当 `a` 或者 `b` 改变时,我们不需要确切地保存里面的值,这个特殊的数组将会保存它所需的值。所以这些数组不会随着时间而占用更多的内存,这是 惰性数据结构和懒操作的重要特点。事实上,它看起来不像数组,更像是buffer(缓冲区)。 144 | 145 | 普通的数组是积极的,所以它会立马保存所有它的值。"惰性数组" 的值则会延迟保存。 146 | 147 | 由于我们不一定要知道 `a` 什么时候添加了新的值,所以另一个关键就是我们需要有去监听 `b` 并在有新值的时候通知它的能力。我们可以想象下监听器是这样的: 148 | 149 | ```js 150 | b.listen( function onValue(v){ 151 | console.log( v ); 152 | } ); 153 | ``` 154 | 155 | `b` 是反应性的,因为它被设置为当 `a` 有值添加时进行**反应**。函数式编程操作当中的 `map(..)` 是把数据源 `a` 里面的所有值转移到目标 `b` 里。每次映射操作都是我们使用同步函数式编程进行单值建模的过程,但是接下来我们将让这种操作变得可以响应式执行。 156 | 157 | **注意:** 最常用到这些函数式编程的是响应式函数式编程(FRP)。我故意避开这个术语是因为一个有关于 FP + Reactive 是否真的构成 FRP 的辩论。我们不会全面深入了解 FRP 的所有含义,所以我会继续称之为响应式函数式编程。或者,如果你不会感觉那么困惑,也可以称之为事件机制函数式编程。 158 | 159 | 我们可以认为 `a` 是生成值的而 `b` 则是去消费这些值的。所以为了可读性,我们得重新整理下这段代码,让问题归结于 **生产者** 和 **消费者**。 160 | 161 | ```js 162 | // 生产者: 163 | 164 | var a = new LazyArray(); 165 | 166 | setInterval( function everySecond(){ 167 | a.push( Math.random() ); 168 | }, 1000 ); 169 | 170 | 171 | // ************************** 172 | // 消费者: 173 | 174 | var b = a.map( function double(v){ 175 | return v * 2; 176 | } ); 177 | 178 | b.listen( function onValue(v){ 179 | console.log( v ); 180 | } ); 181 | ``` 182 | 183 | `a` 是一个行为本质上很像数据流的生产者。我们可以把每个值赋给 `a` 当作一个**事件**。`map(..)` 操作会触发 `b` 上面的 `listen(..)` 事件来消费新的值。 184 | 185 | 我们分离 **生产者** 和 **消费者** 的相关代码,是因为我们的代码应该各司其职。这样的代码组织可以很大程度上提高代码的可读性和维护性。 186 | 187 | ### 声明式的时间 188 | 189 | 我们应该非常谨慎地讨论如何介绍时间状态。具体来说,正如 promise 从单个异步操作中抽离出我们所担心的时间状态,响应式函数式编程从一系列的值/操作中抽离(分割)了时间状态。 190 | 191 | 从 `a` (生产者)的角度来说,唯一与时间相关的就是我们手动调用的 `setInterval(..)` 循环。但它只是为了示范。 192 | 193 | 想象下 `a` 可以被绑定上一些其他的事件源,比如说用户的鼠标点击事件和键盘按键事件,服务端来的 websocket 消息等。在这些情况下,`a` 没必要关注自己的时间状态。每当值准备好,它就只是一个与值连接的无时态管道。 194 | 195 | 从 `b` (消费者)的角度来说,我们不用知道或者关注 `a` 里面的值在何时何地来的。事实上,所有的值都已经存在。我们只关注是否无论何时都能取到那些值。或者说,`map(..)` 的转换操作是一个无时态(惰性)的建模过程。 196 | 197 | **时间** 与 `a` 和 `b` 之间的关系是声明式的,不是命令式的。 198 | 199 | 以 operations-over-time 这种方式来组织值可能不是很有效。让我们来对比下相同的功能如何用命令式来表示: 200 | 201 | ```js 202 | // 生产者: 203 | 204 | var a = { 205 | onValue(v){ 206 | b.onValue( v ); 207 | } 208 | }; 209 | 210 | setInterval( function everySecond(){ 211 | a.onValue( Math.random() ); 212 | }, 1000 ); 213 | 214 | 215 | // ************************** 216 | // 消费者: 217 | 218 | var b = { 219 | map(v){ 220 | return v * 2; 221 | }, 222 | onValue(v){ 223 | v = this.map( v ); 224 | console.log( v ); 225 | } 226 | }; 227 | ``` 228 | 229 | 这似乎很微妙,但这就是存在于命令式版本的代码和之前声明式的版本之间一个很重要的不同点,除了 `b.onValue(..)` 需要自己去调用 `this.map(..)` 之外。在之前的代码中, `b` 从 `a` 当中去拉取,但是在这个代码中,`a` 推送给 `b`。换句话说,把 `b = a.map(..)` 替换成 `b.onValue(v)`。 230 | 231 | 在上面的命令式代码中,以消费者的角度来说它并不清楚 `v` 从哪里来。此外命令式强硬的把代码 `b.onValue(..)` 夹杂在生产者 `a` 的逻辑里,这有点违反了关注点分离原则。这将会让分离生产者和消费者变得困难。 232 | 233 | 相比之下,在之前的代码中,`b = a.map(..)` 表示了 `b` 的值来源于 `a` ,对于如同抽象事件流的数据源 `a`,我们不需要关心。我们可以 **确信** 任何来自于 `a` 到 `b` 里的值都会通过 `map(..)` 操作。 234 | 235 | ### 映射之外的东西 236 | 237 | 为了方便,我们已经说明了通过随着时间一次一次的用 `map(..)` 来绑定 `a` 和 `b` 的概念。其实我们许多其他的函数式编程操作也可以做到这种效果。 238 | 239 | 思考下: 240 | 241 | ```js 242 | var b = a.filter( function isOdd(v) { 243 | return v % 2 == 1; 244 | } ); 245 | 246 | b.listen( function onlyOdds(v){ 247 | console.log( "Odd:", v ); 248 | } ); 249 | ``` 250 | 251 | 这里可以看到 `a` 的值肯定会通过 `isOdd(..)` 赋值给 `b`。 252 | 253 | 即使是 `reduce(..)` 也可以持续的运行: 254 | 255 | ```js 256 | var b = a.reduce( function sum(total,v){ 257 | return total + v; 258 | } ); 259 | 260 | b.listen( function runningTotal(v){ 261 | console.log( "New current total:", v ); 262 | } ); 263 | ``` 264 | 265 | 因为我们调用 `reduce(..)` 是没有给具体 `initialValue` 的值,无论是 `sum(..)` 或者 `runningTotal(..)` 都会等到有 2 个来自 `a` 的参数时才会被调用。 266 | 267 | 这段代码暗示了在 reduction 里面有一个 **内存空间**, 每当有新的值进来的时候,`sum(..)` 才会带上第一个参数 `total` 和第二个参数 `v`被调用。 268 | 269 | 其他的函数式编程操作会在内部作用域请求一个缓存区,比如说 `unique(..)` 可以追踪每一个它访问过的值。 270 | 271 | ### Observables 272 | 273 | 希望现在你可以察觉到响应式,事件式,类数组结构的数据的重要性,就像我们虚构出来的 `LazyArray` 一样。值得高兴的是,这类的数据结构已经存在的了,它就叫 observable。 274 | 275 | **注意:** 只是做些假设(希望):接下来的讨论只是简要的介绍 observables。这是一个需要我们花时间去探究的深层次话题。但是如果你理解本文中的轻量级函数式编程,并且知道如何通过函数式编程的原理来构建异步的话,那么接着学习 observables 将会变得得心应手。 276 | 277 | 现在已经有各种各样的 Observables 的库类, 最出名的是 RxJS 和 Most。在写这篇文章的时候,正好有一个直接向 JS 里添加 observables 的建议,就像 promise。为了演示,我们将用 RxJS 风格的 Observables 来完成下面的例子。 278 | 279 | 这是我们一个较早的响应式的例子,但是用 Observables 来代替 `LazyArray`: 280 | 281 | ```js 282 | // 生产者: 283 | 284 | var a = new Rx.Subject(); 285 | 286 | setInterval( function everySecond(){ 287 | a.next( Math.random() ); 288 | }, 1000 ); 289 | 290 | 291 | // ************************** 292 | // 消费者: 293 | 294 | var b = a.map( function double(v){ 295 | return v * 2; 296 | } ); 297 | 298 | b.subscribe( function onValue(v){ 299 | console.log( v ); 300 | } ); 301 | ``` 302 | 303 | 在 RxJS 中,一个 Observer 订阅一个 Observable。如果你把 Observer 和 Observable 的功能结合到一起,那就会得到一个 Subject。因此,为了保持代码的简洁,我们把 `a` 构建成一个 Subject,所以我们可以调用它的 `next(..)` 方法来添加值(事件)到他的数据流里。 304 | 305 | 如果我们要让 Observer 和 Observable 保持分离: 306 | 307 | ```js 308 | // 生产者: 309 | 310 | var a = Rx.Observable.create( function onObserve(observer){ 311 | setInterval( function everySecond(){ 312 | observer.next( Math.random() ); 313 | }, 1000 ); 314 | } ); 315 | ``` 316 | 317 | 在这个代码里,`a` 是 Observable,毫无疑问,`observer` 就是独立的 observer,它可以去“观察”一些事件(比如我们的`setInterval(..)`循环),然后我们使用它的 `next(..)` 方法来发送一些事件到 observable `a` 的流里。 318 | 319 | 除了 `map(..)`,RxJS 还定义了超过 100 个可以在有新值添加时才触发的方法。就像数组一样。每个 Observable 的方法都会返回一个新的 Observable,意味着他们是链式的。如果一个方法被调用,则它的返回值应该由输入的 Observable 去返回,然后触发到输出的 Observable里,否则抛弃。 320 | 321 | 一个链式的声明式 observable 的例子: 322 | 323 | ```js 324 | var b = 325 | a 326 | .filter( v => v % 2 == 1 ) // 过滤掉偶数 327 | .distinctUntilChanged() // 过滤连续相同的流 328 | .throttle( 100 ) // 函数节流(合并100毫秒内的流) 329 | .map( v = v * 2 ); // 变2倍 330 | 331 | b.subscribe( function onValue(v){ 332 | console.log( "Next:", v ); 333 | } ); 334 | ``` 335 | 336 | **注意:** 这里的链式写法不是一定要把 observable 赋值给 `b` 和调用 `b.subscribe(..)` 分开写,这样做只是为了让每个方法都会得到一个新的返回值。通常,`subscribe(..)` 方法都会在链式写法的最后被调用。 337 | 338 | ## 总结 339 | 340 | 这本书详细的介绍了各种各样的函数式编程操作,例如:把单个值(或者说是一个即时列表的值)转换到另一个值里。 341 | 342 | 对于那些有时态的操作,所有基础的函数式编程原理都可以无时态的应用。就像 promise 创建了一个单一的未来值,我们可以创建一个积极的列表的值来代替像惰性的observable(事件)流的值。 343 | 344 | 数组的 `map(..)` 方法会用当前数组中的每一个值运行一次映射函数,然后放到返回的数组里。而 observable 数组里则是为每一个值运行一次映射函数,无论这个值何时加入,然后把它返回到 observable 里。 345 | 346 | 或者说,如果数组对函数式编程操作是一个积极的数据结构,那么 observable 相当于持续惰性的。 347 | -------------------------------------------------------------------------------- /fig10.svg: -------------------------------------------------------------------------------- 1 | 2 |
a
a
b
b
c
c
d
d
e
e
predicate function
predicate function
list 1
list 1
list 2
list 2
b
b
d
d
e
e
-------------------------------------------------------------------------------- /fig4.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ch6.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 第 6 章:值的不可变性 3 | 4 | 在第 5 章中,我们探讨了减少副作用的重要性:副作用是引起程序意外状态改变的原因,同时也可能会带来意想不到的惊喜(bugs)。这样的暗雷在程序中出现的越少,开发者对程序的信心无疑就会越强,同时代码的可读性也会越高。本章的主题,将继续朝减少程序副作用的方向努力。 5 | 6 | 如果编程风格幂等性是指定义一个数据变更操作以便只影响一次程序状态,那么现在我们将注意力转向将这个影响次数从 1 降为 0。 7 | 8 | 现在我们开始探索值的不可变性,即只在我们的程序中使用不可被改变的数据。 9 | 10 | ## 原始值的不可变性 11 | 12 | 原始数据类型(`number`、`string`、`boolean`、`null` 和 `undefined`)本身就是不可变的;无论如何你都没办法改变它们。 13 | 14 | ```js 15 | // 无效,且毫无意义 16 | 2 = 2.5; 17 | ``` 18 | 19 | 然而 JS 确实有一个特性,使得看起来允许我们改变原始数据类型的值, 即“boxing”特性。当你访问原始类型数据时 —— 特别是 `number`、`string` 和 `boolean` —— 在这种情况下,JS 会自动的把它们包裹(或者说“包装”)成这个值对应的对象(分别是 `Number`、`String` 以及 `Boolean`)。 20 | 21 | 思考下面的代码: 22 | 23 | ```js 24 | var x = 2; 25 | 26 | x.length = 4; 27 | 28 | x; // 2 29 | x.length; // undefined 30 | ``` 31 | 32 | 数值本身并没有可用的 `length` 属性,因此 `x.length = 4` 这个赋值操作正试图添加一个新的属性,不过它静默地失败了(也可以说是这个操作被忽略了或被抛弃了,这取决于你怎么看);变量 `x` 继续承载那个简单的原始类型数据 —— 数值 `2`。 33 | 34 | 但是 JS 允许 `x.length = 4` 这条语句正常执行的事实着实令人困惑。如果这种现象真的无缘无故出现,那么代码的阅读者无疑会摸不着头脑。好消息是,如果你使用了严格模式(`"use strict";`),那么这条语句就会抛出异常了。 35 | 36 | 那么如果尝试改变那些明确被包装成对象的值呢? 37 | 38 | ```js 39 | var x = new Number( 2 ); 40 | 41 | // 没问题 42 | x.length = 4; 43 | ``` 44 | 45 | 这段代码中的 `x` 保存了一个对象的引用,因此可以正常地添加或修改自定义属性。 46 | 47 | 像 `number` 这样的原始数型,值的不可变性看起来相当明显,但字符串呢?JS 开发者有个共同的误解 —— 字符串和数组很像,所以应该是可变的。JS 使用 `[]` 访问字符串成员的语法甚至还暗示字符串真的就像数组。不过,字符串的确是不可变的: 48 | 49 | ```js 50 | var s = "hello"; 51 | 52 | s[1]; // "e" 53 | 54 | s[1] = "E"; 55 | s.length = 10; 56 | 57 | s; // "hello" 58 | ``` 59 | 60 | 尽管可以使用 `s[1]` 来像访问数组元素一样访问字符串成员,JS 字符串也并不是真的数组。`s[1] = "E"` 和 `s.length = 10` 这两个赋值操作都是失败的,就像刚刚的 `x.length = 4` 一样。在严格模式下,这些赋值都会抛出异常,因为 `1` 和 `length` 这两个属性在原始数据类型字符串中都是只读的。 61 | 62 | 有趣的是,即便是包装后的 `String` 对象,其值也会(在大部分情况下)表现的和非包装字符串一样 —— 在严格模式下如果改变已存在的属性,就会抛出异常: 63 | 64 | ```js 65 | "use strict"; 66 | 67 | var s = new String( "hello" ); 68 | 69 | s[1] = "E"; // error 70 | s.length = 10; // error 71 | 72 | s[42] = "?"; // OK 73 | 74 | s; // "hello" 75 | ``` 76 | 77 | ## 从值到值 78 | 79 | 我们将在本节详细展开从值到值这个概念。但在开始之前应该心中有数:值的不可变性并不是说我们不能在程序编写时不改变某个值。如果一个程序的内部状态从始至终都保持不变,那么这个程序肯定相当无趣!它同样不是指变量不能承载不同的值。这些都是对值的不可变这个概念的误解。 80 | 81 | 值的不可变性是指当需要改变程序中的状态时,我们不能改变已存在的数据,而是必须创建和跟踪一个新的数据。 82 | 83 | 例如: 84 | 85 | ```js 86 | function addValue(arr) { 87 | var newArr = [ ...arr, 4 ]; 88 | return newArr; 89 | } 90 | 91 | addValue( [1,2,3] ); // [1,2,3,4] 92 | ``` 93 | 94 | 注意我们没有改变数组 `arr` 的引用,而是创建了一个新的数组(`newArr`),这个新数组包含数组 `arr` 中已存在的值,并且新增了一个新值 `4`。 95 | 96 | 使用我们在第 5 章讨论的副作用的相关概念来分析 `addValue(..)`。它是纯的吗?它是否具有引用透明性?给定相同的数组作为输入,它会永远返回相同的输出吗?它无副作用吗?**答案是肯定的。** 97 | 98 | 设想这个数组 `[1, 2, 3]`, 它是由先前的操作产生,并被我们保存在一个变量中,它代表着程序当前的状态。我们想要计算出程序的下一个状态,因此调用了 `addValue(..)`。但是我们希望下一个状态计算的行为是直接的和明确的,所以 `addValue(..)` 操作简单的接收一个直接输入,返回一个直接输出,并通过不改变 `arr` 引用的原始数组来避免副作用。 99 | 100 | 这就意味着我们既可以计算出新状态 `[1, 2, 3, 4]`,也可以掌控程序的状态变换。程序不会出现过早的过渡到这个状态或完全转变到另一个状态(如 `[1, 2, 3, 5]`)这样的意外情况。通过规范我们的值并把它视为不可变的,我们大幅减少了程序错误,使我们的程序更易于阅读和推导,最终使程序更加可信赖。 101 | 102 | `arr` 所引用的数组是可变的,只是我们选择不去改变他,我们实践了值不可变的这一精神。 103 | 104 | 同样的,可以将“以拷贝代替改变”这样的策略应用于对象,思考下面的代码: 105 | 106 | ```js 107 | function updateLastLogin(user) { 108 | var newUserRecord = Object.assign( {}, user ); 109 | newUserRecord.lastLogin = Date.now(); 110 | return newUserRecord; 111 | } 112 | 113 | var user = { 114 | // .. 115 | }; 116 | 117 | user = updateLastLogin( user ); 118 | ``` 119 | 120 | ### 消除本地影响 121 | 122 | 下面的代码能够体现不可变性的重要性: 123 | 124 | ```js 125 | var arr = [1,2,3]; 126 | 127 | foo( arr ); 128 | 129 | console.log( arr[0] ); 130 | ``` 131 | 132 | 从表面上讲,你可能认为 `arr[0]` 的值仍然为 `1`。但事实是否如此不得而知,因为 `foo(..)` 可能会改变你传入其中的 `arr` 所引用的数组。 133 | 134 | 在之前的章节中,我们已经见到过用下面这种带有欺骗性质的方法来避免意外: 135 | 136 | ```js 137 | var arr = [1,2,3]; 138 | 139 | foo( arr.slice() ); // 哈!一个数组副本! 140 | 141 | console.log( arr[0] ); // 1 142 | ``` 143 | 144 | 当然,使得这个断言成立的前提是 `foo` 函数不会忽略我们传入的参数而直接通过相同的 `arr` 这个自由变量词法引用来访问源数组。 145 | 146 | 对于防止数据变化负面影响,稍后我们会讨论另一种策略。 147 | 148 | ## 重新赋值 149 | 150 | 在进入下一个段落之前先思考一个问题 —— 你如何描述“常量”? 151 | 152 | … 153 | 154 | 你可能会脱口而出“一个不能改变的值就是常量”,“一个不能被改变的变量”等等。这些回答都只能说接近正确答案,但却并不是正确答案。对于常量,我们可以给出一个简洁的定义:一个无法进行重新赋值(reassignment)的变量。 155 | 156 | 我们刚刚在“常量”概念上的吹毛求疵其实是很有必要的,因为它澄清了常量与值无关的事实。无论常量承载何值,该变量都不能使用其他的值被进行重新赋值。但它与值的本质无关。 157 | 158 | 思考下面的代码: 159 | 160 | ```js 161 | var x = 2; 162 | ``` 163 | 164 | 我们刚刚讨论过,数据 `2` 是一个不可变的原始值。如果将上面的代码改为: 165 | 166 | ```js 167 | const x = 2; 168 | ``` 169 | 170 | `const` 关键字的出现,作为“常量声明”被大家熟知,事实上根本没有改变 `2` 的本质,因为它本身就已经不可改变了。 171 | 172 | 下面这行代码会抛出错误,这无可厚非: 173 | 174 | ```js 175 | // 尝试改变 x,祝我好运! 176 | x = 3; // 抛出错误! 177 | ``` 178 | 179 | 但再次重申,我们并不是要改变这个数据,而是要对变量 `x` 进行重新赋值。数据被卷进来纯属偶然。 180 | 181 | 为了证明 `const` 和值的本质无关,思考下面的代码: 182 | 183 | ```js 184 | const x = [ 2 ]; 185 | ``` 186 | 187 | 这个数组是一个常量吗?**并不是。** `x` 是一个常量,因为它无法被重新赋值。但下面的操作是完全可行的: 188 | 189 | ```js 190 | x[0] = 3; 191 | ``` 192 | 193 | 为何?因为尽管 `x` 是一个常量,数组却是可变的。 194 | 195 | 关于 `const` 关键字和“常量”只涉及赋值而不涉及数据语义的特性是个又臭又长的故事。几乎所有语言的高级开发者都踩 `const` 地雷。事实上,Java 最终不赞成使用 `const` 并引入了一个全新的关键词 `final` 来区分“常量”这个语义。 196 | 197 | 抛开混乱之后开始思考,如果 `const` 并不能创建一个不可变的值,那么它对于函数式编程者来说又还有什么重要的呢? 198 | 199 | ### 意图 200 | 201 | `const` 关键字可以用来告知阅读你代码的读者该变量不会被重新赋值。作为一个表达意图的标识,`const` 被加入 JavaScript 不仅常常受到称赞,也普遍提高了代码可读性。 202 | 203 | 在我看来,这是夸大其词,这些说法并没有太大的实际意义。我只看到了使用这种方法来表明意图的微薄好处。如果使用这种方法来声明值的不可变性,与已使用几十年的传统方式相比,`const` 简直太弱了。 204 | 205 | 为了证明我的说法,让我们来做一个实践。`const` 创建了一个在块级作用域内的变量,这意味着该变量只能在其所在的代码块中被访问: 206 | 207 | ```js 208 | // 大量代码 209 | 210 | { 211 | const x = 2; 212 | 213 | // 少数几行代码 214 | } 215 | 216 | // 大量代码 217 | ``` 218 | 219 | 通常来说,代码块的最佳实践是用于仅包裹少数几行代码的场景。如果你有一个包含了超过 10 行的代码块,那么大多数开发者会建议你重构这一段代码。因此 `const x = 2` 只作用于下面的9行代码。 220 | 221 | 程序的其他部分不会影响 `x` 的赋值。 222 | 223 | 我要说的是:上述程序的可读性与下面这样基本相同: 224 | 225 | ```js 226 | // 大量代码 227 | 228 | { 229 | let x = 2; 230 | 231 | // 少数几行代码 232 | } 233 | 234 | // 大量代码 235 | ``` 236 | 237 | 其实只要查看一下在 `let x = 2`; 之后的几行代码,就可以判断出 x 这个变量是否被重新赋值过了。对我来说,“实际上不进行重新赋值”相对“使用容易迷惑人的 `const` 关键字告诉读者‘不要重新赋值’”**是一个更明确的信号**。 238 | 239 | 此外,让我们思考一下,乍看这段代码起来可能给读者传达什么: 240 | 241 | ```js 242 | const magicNums = [1,2,3,4]; 243 | 244 | // .. 245 | ``` 246 | 247 | 读者可能会(错误地)认为,这里使用 `const` 的用意是你永远不会修改这个数组 —— 这样的推断对我来说合情合理。想象一下,如果你的确允许 `magicNums` 这个变量所引用的数组被修改,那么这个 `const` 关键词就极具混淆性了 —— 的很确容易发生意外,不是吗? 248 | 249 | 更糟糕的是,如果你在某处故意修改了 `magicNums`,但对读者而言不够明显呢?读者会在后面的代码里(再次错误地)认为 `magicNums` 的值仍然是 `[1, 2, 3, 4]`。因为他们猜测你之前使用 `const` 的目的就是“这个变量不会改变”。 250 | 251 | 我认为你应该使用 `var` 或 `let` 来声明那些你会去改变的变量,它们确实相比 `const` 来说**是一个更明确的信号**。 252 | 253 | `const` 所带来的问题还没讲完。还记得我们在本章开头所说的吗?值的不可变性是指当需要改变某个数据时,我们不应该直接改变它,而是应该使用一个全新的数据。那么当新数组创建出来后,你会怎么处理它?如果你使用 `const` 声明变量来保存引用吗,这个变量的确没法被重新赋值了,那么……然后呢? 254 | 255 | 从这方面来讲,我认为 `const` 反而增加了函数式编程的困难度。我的结论是:`const` 并不是那么有用。它不仅造成了不必要的混乱,也以一种很不方便的形式限制了我们。我只用 `const` 来声明简单的常量,例如: 256 | 257 | ```js 258 | const PI = 3.141592; 259 | ``` 260 | 261 | `3.141592` 这个值本身就已经是不可变的,并且我也清楚地表示说“`PI` 标识符将始终被用于代表这个字面量的占位符”。对我来说,这才是 `const` 所擅长的。坦白讲,我在编码时并不会使用很多这样的声明。 262 | 263 | 我写过很多,也阅读过很多 JavaScript 代码,我认为由于重新赋值导致大量的 bug 这只是个想象中的问题,实际并不存在。 264 | 265 | 我们应该担心的,并不是变量是否被重新赋值,而是**值是否会发生改变**。为什么?因为值是可被携带的,但词法赋值并不是。你可以向函数中传入一个数组,这个数组可能会在你没意识到的情况下被改变。但是你的其他代码在预期之外重新给变量赋值,这是不可能发生的。 266 | 267 | ### 冻结 268 | 269 | 这是一种简单廉价的(勉强)将像对象、数组、函数这样的可变的数据转为“不可变数据”的方式: 270 | 271 | ```js 272 | var x = Object.freeze( [2] ); 273 | ``` 274 | 275 | `Object.freeze(..)` 方法遍历对象或数组的每个属性和索引,将它们设置为只读以使之不会被重新赋值,事实上这和使用 `const` 声明属性相差无几。`Object.freeze(..)` 也会将属性标记为“不可配置(non-reconfigurable)”,并且使对象或数组本身不可扩展(即不会被添加新属性)。实际上,而就可以将对象的顶层设为不可变。 276 | 277 | 注意,仅仅是顶层不可变! 278 | 279 | ```js 280 | var x = Object.freeze( [ 2, 3, [4, 5] ] ); 281 | 282 | // 不允许改变: 283 | x[0] = 42; 284 | 285 | // oops,仍然允许改变: 286 | x[2][0] = 42; 287 | ``` 288 | 289 | `Object.freeze(..)` 提供浅层的、初级的不可变性约束。如果你希望更深层的不可变约束,那么你就得手动遍历整个对象或数组结构来为所有后代成员应用 `Object.freeze(..)`。 290 | 291 | 与 `const` 相反,`Object.freeze(..)` 并不会误导你,让你得到一个“你以为”不可变的值,而是真真确确给了你一个不可变的值。 292 | 293 | 回顾刚刚的例子: 294 | 295 | ```js 296 | var arr = Object.freeze( [1,2,3] ); 297 | 298 | foo( arr ); 299 | 300 | console.log( arr[0] ); // 1 301 | ``` 302 | 303 | 可以非常确定 `arr[0]` 就是 `1`。 304 | 305 | 这是非常重要的,因为这可以使我们更容易的理解代码,当我们将值传递到我们看不到或者不能控制的地方,我们依然能够相信这个值不会改变。 306 | 307 | ## 性能 308 | 309 | 每当我们开始创建一个新值(数组、对象等)取代修改已经存在的值时,很明显迎面而来的问题就是:这对性能有什么影响? 310 | 311 | 如果每次想要往数组中添加内容时,我们都必须创建一个全新的数组,这不仅占用 CPU 时间并且消耗额外的内存。不再存在任何引用的旧数据将会被垃圾回收机制回收;更多的 CPU 资源消耗。 312 | 313 | 这样的取舍能接受吗?视情况而定。对代码性能的优化和讨论**都应该有个上下文**。 314 | 315 | 如果在你的程序中,只会发生一次或几次单一的状态变化,那么扔掉一个旧对象或旧数组完全没必要担心。性能损失会非常非常小 —— 顶多只有几微秒 —— 对你的应用程序影响甚小。追踪和修复由于数据改变引起的 bug 可能会花费你几分钟甚至几小时的时间,这么看来那几微秒简直没有可比性。 316 | 317 | 但是,如果频繁的进行这样的操作,或者这样的操作出现在应用程序的核心逻辑中,那么性能问题 —— 即性能和内存 —— 就有必要仔细考虑一下了。 318 | 319 | 以数组这样一个特定的数据结构来说,我们想要在每次操作这个数组时使每个更改都隐式地进行,就像结果是一个新数组一样,但除了每次都真的创建一个数组之外,还有什么其他办法来完成这个任务呢?像数组这样的数据结构,我们期望除了能够保存其最原始的数据,然后能追踪其每次改变并根据之前的版本创建一个分支。 320 | 321 | 在内部,它可能就像一个对象引用的链表树,树中的每个节点都表示原始值的改变。从概念上来说,这和 **git** 的版本控制原理类似。 322 | 323 | 324 |

325 | 326 |

327 | 328 | 想象一下使用这个假设的、专门处理数组的数据结构: 329 | 330 | ```js 331 | var state = specialArray( 1, 2, 3, 4 ); 332 | 333 | var newState = state.set( 42, "meaning of life" ); 334 | 335 | state === newState; // false 336 | 337 | state.get( 2 ); // 3 338 | state.get( 42 ); // undefined 339 | 340 | newState.get( 2 ); // 3 341 | newState.get( 42 ); // "meaning of life" 342 | 343 | newState.slice( 1, 3 ); // [2,3] 344 | ``` 345 | 346 | `specialArray(..)` 这个数据结构会在内部追踪每个数据更新操作(例如 `set(..)`),类似 *diff*,因此不必要为原始的那些值(`1`、`2`、`3` 和 `4`)重新分配内存,而是简单的将 `"meaning of life"` 这个值加入列表。重要的是,`state` 和 `newState` 分别指向两个“不同版本”的数组,因此**值的不变性这个语义得以保留**。 347 | 348 | 发明你自己的性能优化数据结构是个有趣的挑战。但从实用性来讲,找一个现成的库会是个更好的选择。**Immutable.js**(http://facebook.github.io/immutable-js) 是一个很棒的选择,它提供多种数据结构,包括 `List`(类似数组)和 `Map`(类似普通对象)。 349 | 350 | 思考下面的 `specialArray` 示例,这次使用 `Immutable.List`: 351 | 352 | ```js 353 | var state = Immutable.List.of( 1, 2, 3, 4 ); 354 | 355 | var newState = state.set( 42, "meaning of life" ); 356 | 357 | state === newState; // false 358 | 359 | state.get( 2 ); // 3 360 | state.get( 42 ); // undefined 361 | 362 | newState.get( 2 ); // 3 363 | newState.get( 42 ); // "meaning of life" 364 | 365 | newState.toArray().slice( 1, 3 ); // [2,3] 366 | ``` 367 | 368 | 像 Immutable.js 这样强大的库一般会采用非常成熟的性能优化。如果不使用库而是手动去处理那些细枝末节,开发的难度会相当大。 369 | 370 | 当改变值这样的场景出现的较少且不用太关心性能时,我推荐使用更轻量级的解决方案,例如我们之前提到过的内置的 `Object.freeze(..)`。 371 | 372 | ## 以不可变的眼光看待数据 373 | 374 | 如果我们从函数中接收了一个数据,但不确定这个数据是可变的还是不可变的,此时该怎么办?去修改它试试看吗?**不要这样做。** 就像在本章最开始的时候所讨论的,不论实际上接收到的值是否可变,我们都应以它们是不可变的来对待,以此来避免副作用并使函数保持纯度。 375 | 376 | 回顾一下之前的例子: 377 | 378 | ```js 379 | function updateLastLogin(user) { 380 | var newUserRecord = Object.assign( {}, user ); 381 | newUserRecord.lastLogin = Date.now(); 382 | return newUserRecord; 383 | } 384 | ``` 385 | 386 | 该实现将 `user` 看做一个不应该被改变的数据来对待;`user` 是否真的不可变完全不会影响这段代码的阅读。对比一下下面的实现: 387 | 388 | ```js 389 | function updateLastLogin(user) { 390 | user.lastLogin = Date.now(); 391 | return user; 392 | } 393 | ``` 394 | 395 | 这个版本更容易实现,性能也会更好一些。但这不仅让 `updateLastLogin(..)` 变得不纯,这种方式改变的值使阅读该代码,以及使用它的地方变得更加复杂。 396 | 397 | **应当总是将 user 看做不可变的值**,这样我们就没必要知道数据从哪里来,也没必要担心数据改变会引发潜在问题。 398 | 399 | JavaScript 中内置的数组方法就是一些很好的例子,例如 `concat(..)` 和 `slice(..)` 等: 400 | 401 | ```js 402 | var arr = [1,2,3,4,5]; 403 | 404 | var arr2 = arr.concat( 6 ); 405 | 406 | arr; // [1,2,3,4,5] 407 | arr2; // [1,2,3,4,5,6] 408 | 409 | var arr3 = arr2.slice( 1 ); 410 | 411 | arr2; // [1,2,3,4,5,6] 412 | arr3; // [2,3,4,5,6] 413 | ``` 414 | 415 | 其他一些将参数看做不可变数据且返回新数组的原型方法还有:`map(..)` 和 `filter(..)` 等。`reduce(..)` / `reduceRight(..)` 方法也会尽量避免改变参数,尽管它们并不默认返回新数组。 416 | 417 | 不幸的是,由于历史问题,也有一部分不纯的数组原型方法:`splice(..)`、`pop(..)`、`push(..)`、`shift(..)`、`unshift(..)`、`reverse(..)` 以及 `fill(..)`。 418 | 419 | 有些人建议禁止使用这些不纯的方法,但我不这么认为。因为一些性能面的原因,某些场景下你仍然可能会用到它们。不过你也应当注意,如果一个数组没有被本地化在当前函数的作用域内,那么不应当使用这些方法,避免它们所产生的副作用影响到代码的其他部分。 420 | 421 | 不论一个数据是否是可变的,永远将他们看做不可变。遵守这样的约定,你程序的可读性和可信赖度将会大大提升。 422 | 423 | ## 总结 424 | 425 | 值的不可变性并不是不改变值。它是指在程序状态改变时,不直接修改当前数据,而是创建并追踪一个新数据。这使得我们在读代码时更有信心,因为我们限制了状态改变的场景,状态不会在意料之外或不易观察的地方发生改变。 426 | 427 | 由于其自身的信号和意图,`const` 关键字声明的常量通常被误认为是强制规定数据不可被改变。事实上,`const` 和值的不可变性声明无关,而且使用它所带来的困惑似乎比它解决的问题还要大。另一种思路,内置的 `Object.freeze(..)` 方法提供了顶层值的不可变性设定。大多数情况下,使用它就足够了。 428 | 429 | 对于程序中性能敏感的部分,或者变化频繁发生的地方,处于对计算和存储空间的考量,每次都创建新的数据或对象(特别是在数组或对象包含很多数据时)是非常不可取的。遇到这种情况,通过类似 **Immutable.js** 的库使用不可变数据结构或许是个很棒的主意。 430 | 431 | 值不变在代码可读性上的意义,不在于不改变数据,而在于以不可变的眼光看待数据这样的约束。 432 | -------------------------------------------------------------------------------- /fig9.svg: -------------------------------------------------------------------------------- 1 | 2 |
a
a
b
b
c
c
d
d
e
e
V
V
W
W
X
X
Y
Y
Z
Z
mapper function
mapper function
list 1
list 1
list 2
list 2
-------------------------------------------------------------------------------- /fig12.svg: -------------------------------------------------------------------------------- 1 | 2 |
a
a
b
b
c
c
d
d
e
e
reducer function
reducer function
list 1
list 1
acc0
[Not supported by viewer]
acc1
[Not supported by viewer]
result
result
acc2
[Not supported by viewer]
initial value
initial value
-------------------------------------------------------------------------------- /fig11.svg: -------------------------------------------------------------------------------- 1 | 2 |
a
a
b
b
c
c
d
d
e
e
reducer function
reducer function
list 1
list 1
initial
value
[Not supported by viewer]
acc0
[Not supported by viewer]
acc1
[Not supported by viewer]
acc2
[Not supported by viewer]
result
result
acc3
[Not supported by viewer]
-------------------------------------------------------------------------------- /ch7.md: -------------------------------------------------------------------------------- 1 | # JavaScript 轻量级函数式编程 2 | # 第 7 章: 闭包 vs 对象 3 | 4 | 数年前,Anton van Straaten 创造了一个非常有名且被常常引用的 [禅理](https://www.merriam-webster.com/dictionary/koan) 来举例和证实一个闭包和对象之间重要的关系。 5 | 6 | > 德高望重的大师 Qc Na 曾经和他的学生 Anton 一起散步。Anton 希望引导大师到一个讨论里,说到:大师,我曾听说对象是一个非常好的东西,是这样么?Qc Na 同情地看着他的学生回答到, “愚笨的弟子,对象只不过是可怜人的闭包” 7 | > 8 | > 被批评后,Anton 离开他的导师并回到了自己的住处,致力于学习闭包。他认真的阅读整个“匿名函数:终极……”系列论文和它的姐妹篇,并且实践了一个基于闭包系统的小的 Scheme 解析器。他学了很多,盼望展现给他导师他的进步。 9 | > 10 | > 当他下一次与 Qc Na 一同散步时,Anton 试着提醒他的导师,说到 “导师,我已经勤奋地学习了这件事,我现在明白了对象真的是可怜人的闭包。” ,Qc Na 用棍子戳了戳 Anton 回应到,“你什么时候才能学会,闭包才是可怜人的对象”。在那一刻, Anton 明白了什么。 11 | > 12 | > Anton van Straaten 6/4/2003 13 | > 14 | > http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html 15 | 16 | 原帖尽管简短,却有更多关于起源和动机的内容,我强烈推荐为了理解本章去阅读原帖来调整你的观念。 17 | 18 | 我观察到很多人读完这个会对其中的聪明智慧傻笑,却继续不改变他们的想法。但是,这个禅理(来自 Bhuddist Zen 观点)促使读者进入其中对立真相的辩驳中。所以,返回并且再读一遍。 19 | 20 | 到底是哪个?是闭包是可怜的对象,还是对象是可怜的闭包?或都不是?或都是?或者这只是为了说明闭包和对象在某些方面是相同的方式? 21 | 22 | 还有它们中哪个与函数式编程相关?拉一把椅子过来并且仔细考虑一会儿。如果你愿意,这一章将是一个精彩的迂回之路,一个远足。 23 | 24 | ## 达成共识 25 | 26 | 先确定一点,当我们谈及闭包和对象我们都达成了共识。我们显然是在 JavaScript 如何处理这两种机制的上下文中进行讨论的,并且特指的是讨论简单函数闭包(见第 2 章的“保持作用域”)和简单对象(键值对的集合)。 27 | 28 | 一个简单的函数闭包: 29 | 30 | ```js 31 | function outer() { 32 | var one = 1; 33 | var two = 2; 34 | 35 | return function inner(){ 36 | return one + two; 37 | }; 38 | } 39 | 40 | var three = outer(); 41 | 42 | three(); // 3 43 | ``` 44 | 45 | 一个简单的对象: 46 | 47 | ```js 48 | var obj = { 49 | one: 1, 50 | two: 2 51 | }; 52 | 53 | function three(outer) { 54 | return outer.one + outer.two; 55 | } 56 | 57 | three( obj ); // 3 58 | ``` 59 | 60 | 但提到“闭包“时,很多人会想很多额外的事情,例如异步回调甚至是封装和信息隐藏的模块模式。同样,”对象“会让人想起类、`this`、原型和大量其它的工具和模式。 61 | 62 | 随着深入,我们会需要小心地处理部分额外的相关内容,但是现在,尽量只记住闭包和对象最简单的释义 —— 这会减少很多探索过程中的困惑。 63 | 64 | ## 相像 65 | 66 | 闭包和对象之间的关系可能不是那么明显。让我们先来探究它们之间的相似点。 67 | 68 | 为了给这次讨论一个基调,让我简述两件事: 69 | 70 | 1. 一个没有闭包的编程语言可以用对象来模拟闭包。 71 | 2. 一个没有对象的编程语言可以用闭包来模拟对象。 72 | 73 | 换句话说,我们可以认为闭包和对象是一样东西的两种表达方式。 74 | 75 | ### 状态 76 | 77 | 思考下面的代码: 78 | 79 | ```js 80 | function outer() { 81 | var one = 1; 82 | var two = 2; 83 | 84 | return function inner(){ 85 | return one + two; 86 | }; 87 | } 88 | 89 | var obj = { 90 | one: 1, 91 | two: 2 92 | }; 93 | ``` 94 | 95 | `inner()` 和 `obj` 对象持有的作用域都包含了两个元素状态:值为 `1` 的 `one` 和值为 `2` 的 `two`。从语法和机制来说,这两种声明状态是不同的。但概念上,他们的确相当相似。 96 | 97 | 事实上,表达一个对象为闭包形式,或闭包为对象形式是相当简单的。接下来,尝试一下: 98 | 99 | ```js 100 | var point = { 101 | x: 10, 102 | y: 12, 103 | z: 14 104 | }; 105 | ``` 106 | 107 | 你是不是想起了一些相似的东西? 108 | 109 | ```js 110 | function outer() { 111 | var x = 10; 112 | var y = 12; 113 | var z = 14; 114 | 115 | return function inner(){ 116 | return [x,y,z]; 117 | } 118 | }; 119 | 120 | var point = outer(); 121 | ``` 122 | 123 | **注意:** 每次被调用时 `inner()` 方法创建并返回了一个新的数组(亦然是一个对象)。这是因为 JS 不提供返回多个数据却不包装在一个对象中的能力。这并不是严格意义上的一个违反我们对象类似闭包的说明的任务,因为这只是一个暴露/运输具体值的实现,状态追踪本身仍然是基于对象的。使用 ES6+ 数组解构,我们可以声明地忽视这个临时中间对象通过另一种方式:`var [x,y,z] = point()`。从开发者工程学角度,值应该被单独存储并且通过闭包而不是对象来追踪。 124 | 125 | 如果你有一个嵌套对象会怎么样? 126 | 127 | ```js 128 | var person = { 129 | name: "Kyle Simpson", 130 | address: { 131 | street: "123 Easy St", 132 | city: "JS'ville", 133 | state: "ES" 134 | } 135 | }; 136 | ``` 137 | 138 | 我们可以用嵌套闭包来表示相同的状态: 139 | 140 | ```js 141 | function outer() { 142 | var name = "Kyle Simpson"; 143 | return middle(); 144 | 145 | // ******************** 146 | 147 | function middle() { 148 | var street = "123 Easy St"; 149 | var city = "JS'ville"; 150 | var state = "ES"; 151 | 152 | return function inner(){ 153 | return [name,street,city,state]; 154 | }; 155 | } 156 | } 157 | 158 | var person = outer(); 159 | ``` 160 | 161 | 让我们尝试另一个方向,从闭包转为对象: 162 | 163 | ```js 164 | function point(x1,y1) { 165 | return function distFromPoint(x2,y2){ 166 | return Math.sqrt( 167 | Math.pow( x2 - x1, 2 ) + 168 | Math.pow( y2 - y1, 2 ) 169 | ); 170 | }; 171 | } 172 | 173 | var pointDistance = point( 1, 1 ); 174 | 175 | pointDistance( 4, 5 ); // 5 176 | ``` 177 | 178 | `distFromPoint(..)` 封装了 `x1` 和 `y1`,但是我们也可以通过传入一个具体的对象作为替代值: 179 | 180 | ```js 181 | function pointDistance(point,x2,y2) { 182 | return Math.sqrt( 183 | Math.pow( x2 - point.x1, 2 ) + 184 | Math.pow( y2 - point.y1, 2 ) 185 | ); 186 | }; 187 | 188 | pointDistance( 189 | { x1: 1, y1: 1 }, 190 | 4, // x2 191 | 5 // y2 192 | ); 193 | // 5 194 | ``` 195 | 196 | 明确地传入`point` 对象替换了闭包的隐式状态。 197 | 198 | #### 行为,也是一样! 199 | 200 | 对象和闭包不仅是表达状态集合的方式,而且他们也可以包含函数或者方法。将数据和行为捆绑为有一个充满想象力的名字:封装。 201 | 202 | 思考: 203 | 204 | ```js 205 | function person(name,age) { 206 | return happyBirthday(){ 207 | age++; 208 | console.log( 209 | "Happy " + age + "th Birthday, " + name + "!" 210 | ); 211 | } 212 | } 213 | 214 | var birthdayBoy = person( "Kyle", 36 ); 215 | 216 | birthdayBoy(); // Happy 37th Birthday, Kyle! 217 | ``` 218 | 219 | 内部函数 `happyBirthday()` 封闭了 `name` 和 `age` ,所以内部的函数也持有了这个状态。 220 | 221 | 我们也可以通过 this 绑定一个对象来获取同样的能力: 222 | 223 | ```js 224 | var birthdayBoy = { 225 | name: "Kyle", 226 | age: 36, 227 | happyBirthday() { 228 | this.age++; 229 | console.log( 230 | "Happy " + this.age + "th Birthday, " + this.name + "!" 231 | ); 232 | } 233 | }; 234 | 235 | birthdayBoy.happyBirthday(); 236 | // Happy 37th Birthday, Kyle! 237 | ``` 238 | 239 | 我们仍然通过 `happyBrithday()` 函数来表达对状态数据的封装,但是用对象代替了闭包。同时我们没有显式给函数传递一个对象(如同先前的例子);JavaScript 的 `this` 绑定可以创造一个隐式的绑定。 240 | 241 | 从另一方面分析这种关系:闭包将单个函数与一系列状态结合起来,而对象却在保有相同状态的基础上,允许任意数量的函数来操作这些状态。 242 | 243 | 事实上,我们可以在一个作为接口的闭包上将一系列的方法暴露出来。思考一个包含了两个方法的传统对象: 244 | 245 | ```js 246 | var person = { 247 | firstName: "Kyle", 248 | lastName: "Simpson", 249 | first() { 250 | return this.firstName; 251 | }, 252 | last() { 253 | return this.lastName; 254 | } 255 | } 256 | 257 | person.first() + " " + person.last(); 258 | // Kyle Simpson 259 | ``` 260 | 261 | 只用闭包而不用对象,我们可以表达这个程序为: 262 | 263 | ```js 264 | function createPerson(firstName,lastName) { 265 | return API; 266 | 267 | // ******************** 268 | 269 | function API(methodName) { 270 | switch (methodName) { 271 | case "first": 272 | return first(); 273 | break; 274 | case "last": 275 | return last(); 276 | break; 277 | }; 278 | } 279 | 280 | function first() { 281 | return firstName; 282 | } 283 | 284 | function last() { 285 | return lastName; 286 | } 287 | } 288 | 289 | var person = createPerson( "Kyle", "Simpson" ); 290 | 291 | person( "first" ) + " " + person( "last" ); 292 | // Kyle Simpson 293 | ``` 294 | 295 | 尽管这些程序看起来感觉有点反人类,但它们实际上只是相同程序的不同实现。 296 | 297 | ### (不)可变 298 | 299 | 许多人最初都认为闭包和对象行为的差别源于可变性;闭包会阻止来自外部的变化而对象则不然。但是,结果是,这两种形式都有典型的可变行为。 300 | 301 | 正如第 6 章讨论的,这是因为我们关心的是**值**的可变性,值可变是值本身的特性,不在于在哪里或者如何被赋值的。 302 | 303 | ```js 304 | function outer() { 305 | var x = 1; 306 | var y = [2,3]; 307 | 308 | return function inner(){ 309 | return [ x, y[0], y[1] ]; 310 | }; 311 | } 312 | 313 | var xyPublic = { 314 | x: 1, 315 | y: [2,3] 316 | }; 317 | ``` 318 | 319 | 在 `outer()` 中字面变量 `x` 存储的值是不可变的 —— 记住,定义的基本类型如 `2` 是不可变的。但是 `y` 的引用值,一个数组,绝对是可变的。这点对于 `xyPublic` 中的 `x` 和 `y` 属性也是完全相同的。 320 | 321 | 通过指出 `y` 本身是个数组我们可以强调对象和闭包在可变这点上没有关系,因此我们需要将这个例子继续拆解: 322 | 323 | ```js 324 | function outer() { 325 | var x = 1; 326 | return middle(); 327 | 328 | // ******************** 329 | 330 | function middle() { 331 | var y0 = 2; 332 | var y1 = 3; 333 | 334 | return function inner(){ 335 | return [ x, y0, y1 ]; 336 | }; 337 | } 338 | } 339 | 340 | var xyPublic = { 341 | x: 1, 342 | y: { 343 | 0: 2, 344 | 1: 3 345 | } 346 | }; 347 | ``` 348 | 349 | 如果你认为这个如同 “世界是一只驮着一只一直驮下去的乌龟(对象)群”,在最底层,所有的状态数据都是基本类型,而所有基本类型都是不可变值。 350 | 351 | 不论是用嵌套对象还是嵌套闭包代表状态,这些被持有的值都是不可变的。 352 | 353 | ### 同构 354 | 355 | 同构这个概念最近在 JavaScript 圈经常被提出,它通常被用来指代码可以同时被服务端和浏览器端使用/分享。我不久以前写了一篇博文说明这种对同构这个词的使用是错误的,隐藏了它实际上确切和重要的意思。 356 | 357 | 这里我是博文部分的节选: 358 | 359 | > 同构的意思是什么?当然,我们可以用数学词汇,社会学或者生物学讨论它。同构最普遍的概念是你有两个类似但是不相同的结构。 360 | > 361 | > 在这些所有的惯用法中,同构和相等的区别在这里:如果两个值在各方面完全一致那么它们相等,但是如果它们表现不一致却仍有一对一或者双向映射的关系那么它们是同构。 362 | > 363 | > 换而言之,两件事物A和B如果你能够映射(转化)A 到 B 并且能够通过反向映射回到A那么它们就是同构。 364 | 365 | 回想第 2 章的简单数学回顾,我们讨论了函数的数学定义是一个输入和输出之间的映射。我们指出这在学术上称为态射。同构是双映(双向)态射的特殊案例,它需要映射不仅仅必须可以从任意一边完成,而且在任一方式下反应完全一致。 366 | 367 | 不去思考这些关于数字的问题,让我们将同构关联到代码。再一次引用我的博文: 368 | 369 | > 如果 JS 有同构的话是怎么样的?它可能是一集合的 JS 代码转化为了另一集合的 JS 代码,并且(重要的是)如果你原意的话,你可以把转化后的代码转为之前的。 370 | > 371 | 372 | 正如我们之前通过闭包如同对象和对象如同闭包为例声称的一样,它们的表达可以任意替换。就这一点来说,它们互为同构。 373 | 374 | 简而言之,闭包和对象是状态的同构表示(及其相关功能)。 375 | 376 | 下次你听到谁说 “X 与 Y 是同构的”,他们的意思是,“X 和 Y 可以从两者中的任意一方转化到另一方,并且无论怎样都保持了相同的特性。” 377 | 378 | ### 内部结构 379 | 380 | 所以,我们可以从我们写的代码角度想象对象是闭包的一种同构展示。但我们也可以观察到闭包系统可以被实现,并且很可能是用对象实现的! 381 | 382 | 这样想一下:在如下的代码中, 在 `outer()` 已经运行后,JS 如何为了 `inner()` 的引用保持对变量 `x` 的追踪? 383 | 384 | ```js 385 | function outer() { 386 | var x = 1; 387 | 388 | return function inner(){ 389 | return x; 390 | }; 391 | } 392 | ``` 393 | 394 | 我们会想到作用域,`outer()` 作为属性的对象实施设置所有的变量定义。因此,从概念上讲,在内存中的某个地方,是类似这样的。 395 | 396 | ```js 397 | scopeOfOuter = { 398 | x: 1 399 | }; 400 | ``` 401 | 402 | 接下来对于 `inner()` 函数,一旦创建,它获得了一个叫做 `scopeOfInner` 的(空)作用域对象,这个对象被其 `[[Prototype]]` 连接到 `scopeOfOuter` 对象,近似这个: 403 | 404 | ```js 405 | scopeOfInner = {}; 406 | Object.setPrototypeOf( scopeOfInner, scopeOfOuter ); 407 | ``` 408 | 409 | 接着,当内部的 `inner()` 建立词法变量 `x` 的引用时,实际更像这样: 410 | 411 | ```js 412 | return scopeOfInner.x; 413 | ``` 414 | 415 | `scopeOfInner` 并没有一个 `x` 的属性,当他的 `[[Prototype]]` 连接到拥有 `x` 属性的 `scopeOfOuter`时。通过原型委托访问 `scopeOfOuter.x` 返回值是 `1`。 416 | 417 | 这样,我们可以近似认为为什么 `outer()` 的作用域甚至在当它执行完都被保留(通过闭包),这是因为 `scopeOfInner` 对象连接到 `scopeOfOuter` 对象,因此,使这个对象和它的属性完整的被保存下来。 418 | 419 | 现在,这都只是概念。我没有从字面上说 JS 引擎使用对象和原型。但它完全有道理,它**可以**同样地工作。 420 | 421 | 许多语言实际上通过对象实现了闭包。另一些语言用闭包的概念实现了对象。但我们让读者使用他们的想象力思考这是如何工作的。 422 | 423 | 424 | ## 同根异枝 425 | 426 | 所以闭包和对象是等价的,对吗?不完全是,我打赌它们比你在读本章前想的更加相似,但是它们仍有重要的区别点。 427 | 428 | 这些区别点不应当被视作缺点或者不利于使用的论点;这是错误的观点。对于给定的任务,它们应该被视为使一个或另一个更适合(和可读)的特点和优势。 429 | 430 | ### 结构可变性 431 | 432 | 从概念上讲,闭包的结构不是可变的。 433 | 434 | 换而言之,你永远不能从闭包添加或移除状态。闭包是一个表示对象在哪里声明的特性(被固定在编写/编译时间),并且不受任何条件的影响 —— 当然假设你使用严格模式并且/或者没有使用作弊手段例如 `eval(..)`。 435 | 436 | **注意:** JS 引擎可以从技术上过滤一个对象来清除其作用域中不再被使用的变量,但是这是一个对于开发者透明的高级的优化。无论引擎是否实际做了这类优化,我认为对于开发者来说假设闭包是作用域优先而不是变量优先是最安全的。如果你不想保留它,就不要封闭它(在闭包里)! 437 | 438 | 但是,对象默认是完全可变的,你可以自由的添加或者移除(`delete`)一个对象的属性/索引,只要对象没有被冻结(`Object.freeze(..)`) 439 | 440 | 这或许是代码可以根据程序中运行时条件追踪更多(或更少)状态的优势。 441 | 442 | 举个例子,让我们思考追踪游戏中的按键事件。几乎可以肯定,你会考虑使用一个数组来做这件事: 443 | 444 | ```js 445 | function trackEvent(evt,keypresses = []) { 446 | return keypresses.concat( evt ); 447 | } 448 | 449 | var keypresses = trackEvent( newEvent1 ); 450 | 451 | keypresses = trackEvent( newEvent2, keypresses ); 452 | ``` 453 | 454 | **注意**:你能否认出为什么我使用 `concat(..)` 而不是直接对 `keypresses` 数组使用 `push(..)` 操作?因为在函数式编程中,我们通常希望对待数组如同不可变数据结构,可以被创建和添加,但不能直接改变。我们剔除了显式重新赋值带来的邪恶副作用(稍后再作说明)。 455 | 456 | 尽管我们不在改变数组的结构,但当我们希望时我们也可以。稍后详细介绍。 457 | 458 | 数组不是记录这个 evt 对象的增长“列表”的仅有的方式。。我们可以使用闭包: 459 | 460 | ```js 461 | function trackEvent(evt,keypresses = () => []) { 462 | return function newKeypresses() { 463 | return [ ...keypresses(), evt ]; 464 | }; 465 | } 466 | 467 | var keypresses = trackEvent( newEvent1 ); 468 | 469 | keypresses = trackEvent( newEvent2, keypresses ); 470 | ``` 471 | 472 | 你看出这里发生了什么吗? 473 | 474 | 每次我们添加一个新的事件到这个“列表”,我们创建了一个包装了现有 `keypresses()` 方法(闭包)的新闭包,这个新闭包捕获了当前的 `evt` 。当我们调用 `keypresses()` 函数,它将成功地调用所有的内部方法,并创建一个包含所有独立封装的 `evt` 对象的中间数组。再次说明,闭包是一个追踪所有状态的机制;这个你看到的数组只是一个对于需要一个方法来返回函数中多个值的具体实现。 475 | 476 | 所以哪一个更适合我们的任务?毫无意外,数组方法可能更合适一些。闭包的不可变结构意味着我们的唯一选项是封装更多的闭包在里面。对象默认是可扩展的,所以我们需要增长这个数组就足够了。 477 | 478 | 顺便一提,尽管我们表现出结构不可变或可变是一个闭包和对象之间的明显区别,然而我们使用对象作为一个不可变数据的方法实际上使之更相似而非不同。 479 | 480 | 数组每次添加就创造一个新数组(通过 `concat(..)`)就是把数组对待为结构不可变,这个概念上对等于通过适当的设计使闭包结构上不可变。 481 | 482 | ### 私有 483 | 484 | 当对比分析闭包和对象时可能你思考的第一个区分点就是闭包通过词法作用域提供“私有”状态,而对象将一切做为公共属性暴露。这种私有有一个精致的名字:信息隐藏。 485 | 486 | 考虑词法闭包隐藏: 487 | 488 | ```js 489 | function outer() { 490 | var x = 1; 491 | 492 | return function inner(){ 493 | return x; 494 | }; 495 | } 496 | 497 | var xHidden = outer(); 498 | 499 | xHidden(); // 1 500 | ``` 501 | 502 | 现在同样的状态公开: 503 | 504 | ```js 505 | var xPublic = { 506 | x: 1 507 | }; 508 | 509 | xPublic.x; // 1 510 | ``` 511 | 512 | 这里有一些在常规的软件工程原理方面明显的区别 —— 考虑下抽象,这种模块模式有着公有和私有 API 等等。但是让我们试着把我们的讨论局限于函数式编程的观点,毕竟,这是一本关于函数式编程的书! 513 | 514 | #### 可见性 515 | 516 | 似乎隐藏信息的能力是一种理想状态的跟踪特性,但是我认为函数式编程者可能持反对观点。 517 | 518 | 在一个对象中管理状态作为公开属性的一个优点是这使你状态中的所有数据更容易枚举(迭代)。思考下你想访问每一个按键事件(从之前的那个例子)并且存储到一个数据库,使用一个这样的工具: 519 | 520 | ```js 521 | function recordKeypress(keypressEvt) { 522 | // 数据库实用程序 523 | DB.store( "keypress-events", keypressEvt ); 524 | } 525 | ``` 526 | 527 | 如果你已经有一个数组,正好是一个拥有公开的用数字命名属性的对象 —— 非常直接地使用 JS 对象的内建工具 `forEach(..)`: 528 | 529 | ```js 530 | keypresses.forEach( recordKeypress ); 531 | ``` 532 | 533 | 但是,如果按键列表被隐藏在一个闭包里,你不得不在闭包内暴露一个享有特权访问数据的公开 API 工具。 534 | 535 | 举例而说,我可以给我们的闭包 —— keypresses 例子自有的 forEach 方法,如同数组内建的: 536 | 537 | ```js 538 | function trackEvent( 539 | evt, 540 | keypresses = { 541 | list() { return []; }, 542 | forEach() {} 543 | } 544 | ) { 545 | return { 546 | list() { 547 | return [ ...keypresses.list(), evt ]; 548 | }, 549 | forEach(fn) { 550 | keypresses.forEach( fn ); 551 | fn( evt ); 552 | } 553 | }; 554 | } 555 | 556 | // .. 557 | 558 | keypresses.list(); // [ evt, evt, .. ] 559 | 560 | keypresses.forEach( recordKeypress ); 561 | ``` 562 | 563 | 对象状态数据的可见性让我们能更直接地使用它,而闭包遮掩状态让我们更艰难地处理它。 564 | 565 | #### 变更控制 566 | 567 | 如果词法变量被隐藏在一个闭包中,只有闭包内部的代码才能自由的重新赋值,在外部修改 `x` 是不可能的。 568 | 569 | 正如我们在第 6 章看到的,提升代码可读性的唯一真相就是减少表面掩盖,读者必须可以预见到每一个给定变量的行为。 570 | 571 | 词法(作用域)在重新赋值上的局部就近原则是为什么我不认为 const 是一个有帮助的特性的一个重要原因。作用域(例如闭包)通常应该尽可能小,这意味着重新赋值只会影响少许代码。在上面的 `outer() `中,我们可以快速地检查到没有一行代码重设了 x,至此(x 的)所有意图和目的表现地像一个常量。 572 | 573 | 这类保证对于我们对函数纯净的信任是一个强有力的贡献,例如。 574 | 575 | 换而言之,`xPublic.x` 是一个公开属性,程序的任何部分都能引用 `xPublic` ,默认有重设 `xPublic.x` 到别的值的能力。这会让很多行代码需要被考虑。 576 | 577 | 这是为什么在第 6 章, 我们视 `Object.freeze(..)` 为使所有的对象属性只读(writable: false)的一个快速而凌乱的方式,让它们不能被不可预测的重设。 578 | 579 | 不幸的是,`Object.freeze(..)` 是极端且不可逆的。 580 | 581 | 使有了闭包,你就有了一些可以更改代码的权限,而剩余的程序是受限的。当我们冻结一个对象,代码中没有任何部分可以被重设。此外,一旦一个对象被冻结,它不能被解冻,所以所有属性在程序运行期间都保持只读。 582 | 583 | 在我想允许重新赋值但是在表层限制的地方,闭包比起对象更方便和灵活。在我不想重新赋值的地方,一个冻结的对象比起重复 `const` 声明在我所有的函数中更方便一些。 584 | 585 | 许多函数式编程者在重新赋值上采取了一个强硬的立场:它不应该被使用。他们倾向使用 `const` 来使用所有闭包变量只读,并且他们使用 `Ojbect.freeze(..)` 或者完全不可变数据结构来防止属性被重新赋值。此外,他们尽量在每个可能的地方减少显式地声明的/追踪的变量,更倾向于值传递 —— 函数链,作为参数被传递的 `return` 值,等等 —— 替代中间值存储。 586 | 587 | 这本书是关于 JavaScript 中的轻量级函数式编程,这是一个我与核心函数式编程群体有分歧的情况。 588 | 589 | 我认为变量重新赋值当被合理的使用时是相当有用的,它的明确性具有相当有可读性。从经验来看,在插入 `debugger` 或断点或跟踪表表达式时,调试工作要容易得多。 590 | 591 | ### 状态拷贝 592 | 593 | 正如我们在第 6 章学习的,防止副作用侵蚀代码可预测性的最好方法之一是确保我们将所有状态值视为不可变的,无论他们是否真的可变(冻结)与否。 594 | 595 | 如果你没有使用特别定制的库来提供复杂的不可变数据结构,最简单满足要求的方法:在每次变化前复制你的对象或者数组。 596 | 597 | 数组浅拷贝很容易:只要使用 `slice()` 方法: 598 | 599 | ```js 600 | var a = [ 1, 2, 3 ]; 601 | 602 | var b = a.slice(); 603 | b.push( 4 ); 604 | 605 | a; // [1,2,3] 606 | b; // [1,2,3,4] 607 | ``` 608 | 609 | 对象也可以相对容易地实现浅拷贝: 610 | 611 | ```js 612 | var o = { 613 | x: 1, 614 | y: 2 615 | }; 616 | 617 | // 在 ES2017 以后,使用对象的解构: 618 | var p = { ...o }; 619 | p.y = 3; 620 | 621 | // 在 ES2015 以后: 622 | var p = Object.assign( {}, o ); 623 | p.y = 3; 624 | ``` 625 | 626 | 如果对象或数组中的值是非基本类型(对象或数组),使用深拷贝你不得不手动遍历每一层来拷贝每个内嵌对象。否则,你将有这些内部对象的共享引用拷贝,这就像给你的程序逻辑造成了一次大破坏。 627 | 628 | 你是否意识到克隆是可行的只是因为所有的这些状态值是可见的并且可以如此简单地被拷贝?一堆被包装在闭包里的状态会怎么样,你如何拷贝这些状态? 629 | 630 | 那是相当乏味的。基本上,你不得不做一些类似之前我们自定义 `forEach` API 的方法:提供一个闭包内层拥有提取或拷贝隐藏值权限的函数,并在这过程中创建新的等价闭包。 631 | 632 | 尽管这在理论上是可行的,对读者来说也是一种锻炼!这个实现的操作量远远不及你可能进行的任何真实程序的调整。 633 | 634 | 在表示需要拷贝的状态时,对象具有一个更明显的优势。 635 | 636 | ### 性能 637 | 638 | 从实现的角度看,对象有一个比闭包有利的原因,那就是 JavaScript 对象通常在内存和甚至计算角度是更加轻量的。 639 | 640 | 但是需要小心这个普遍的断言:有很多东西可以用来处理对象,这会抹除你从无视闭包转向对象状态追踪获得的任何性能增益。 641 | 642 | 让我们考虑一个情景的两种实现。首先,闭包方式实现: 643 | 644 | ```js 645 | function StudentRecord(name,major,gpa) { 646 | return function printStudent(){ 647 | return `${name}, Major: ${major}, GPA: ${gpa.toFixed(1)}`; 648 | }; 649 | } 650 | 651 | var student = StudentRecord( "Kyle Simpson", "kyle@some.tld", "CS", 4 ); 652 | 653 | // 随后 654 | 655 | student(); 656 | // Kyle Simpson, Major: CS, GPA: 4.0 657 | ``` 658 | 659 | 内部函数 `printStudent()` 封装了三个变量:`name`、`major` 和` gpa`。它维护这个状态无论我们是否传递引用给这个函数,在这个例子我们称它为 `student()`。 660 | 661 | 现在看对象(和 `this`)方式: 662 | 663 | ```js 664 | function StudentRecord(){ 665 | return `${this.name}, Major: ${this.major}, GPA: ${this.gpa.toFixed(1)}`; 666 | } 667 | 668 | var student = StudentRecord.bind( { 669 | name: "Kyle Simpson", 670 | major: "CS", 671 | gpa: 4 672 | } ); 673 | 674 | // 随后 675 | 676 | student(); 677 | // Kyle Simpson, Major: CS, GPA: 4.0 678 | ``` 679 | 680 | `student()` 函数,学术上叫做“边界函数” —— 有一个硬性边界 `this` 来引用我们传入的对象字面量,因此之后任何调用 `student()` 将使用这个对象作为`this`,于是它的封装状态可以被访问。 681 | 682 | 两种实现有相同的输出:一个保存状态的函数,但是关于性能,会有什么不同呢? 683 | 684 | 注意:精准可控地判断 JS 代码片段性能是非常困难的事情。我们在这里不会深入所有的细节,但是我强烈推荐你阅读《你不知道的 JS:异步和性能》这本书,特别是第 6 章“性能测试和调优”,来了解细节。 685 | 686 | 如果你写过一个库来创造持有配对状态的函数,要么在第一个片段中调用 `studentRecord(..)`,要么在第二个片段中调用 `StudentRecord.bind(..)`的方式,你可能更多的关心它们两的性能怎样。检查代码,我们可以看到前者每次都必须创建一个新函数表达式。后者使用 `bind(..)`,没有明显的含义。 687 | 688 | 思考 `bind(..)` 在内部做了什么的一种方式是创建一个闭包来替代函数,像这样: 689 | 690 | ```js 691 | function bind(orinFn,thisObj) { 692 | return function boundFn(...args) { 693 | return origFn.apply( thisObj, args ); 694 | }; 695 | } 696 | 697 | var student = bind( StudentRecord, { name: "Kyle.." } ); 698 | ``` 699 | 700 | 这样,看起来我们的场景的两种实现都是创造一个闭包,所以性能看似也是一致的。 701 | 702 | 但是,内置的 `bind(..)` 工具并不一定要创建闭包来完成任务。它只是简单地创建了一个函数,然后手动设置它的内部 `this` 给一个指定的对象。这可能比起我们使用闭包本身是一个更高效的操作。 703 | 704 | 我们这里讨论的在每次操作上的这种性能优化是不值一提的。但是如果你的库的关键部分被使用了成千上万次甚至更多,那么节省的时间会很快增加。许多库 —— Bluebird 就是这样一个例子,它已经完成移除闭包去使用对象的优化。 705 | 706 | 在库的使用案例之外,持有配对状态的函数通常在应用的关键路径发生的次数相对非常少。相比之下,典型的使用是函数加状态 —— 在任意一个片段调用 `student()`,是更加常见的。 707 | 708 | 如果你的代码中也有这样的场景,你应该更多地考虑(优化)前后的性能对比。 709 | 710 | 历史上的边界函数通常具有一个相当糟糕的性能,但是最近已经被 JS 引擎高度优化。如果你在几年前检测过这些变化,很可能跟你现在用最近的引擎重复测试的结果完全不一致。 711 | 712 | 边界函数现在看起来至少跟同样的封装函数表现的一样好。所以这是另一个支持对象比闭包好的点。 713 | 714 | 我只想重申:性能观察结果不是绝对的,在一个给定场景下决定什么是最好的是非常复杂的。不要随意使用你从别人那里听到的或者是你从之前一些项目中看到的。小心的决定对象还是闭包更适合这个任务。 715 | 716 | ## 总结 717 | 718 | 本章的真理无法被直述。必须阅读本章来寻找它的真理。 719 | -------------------------------------------------------------------------------- /fig13.svg: -------------------------------------------------------------------------------- 1 | 2 |
function foo(x) {
   if (x < 5) return x;
   return foo(x / 2);
}
[Not supported by viewer]
16
[Not supported by viewer]
8
[Not supported by viewer]
function foo(x) {
   if (x < 5) return x;
   return foo(x / 2);
}
[Not supported by viewer]
16
[Not supported by viewer]
8
[Not supported by viewer]
4
[Not supported by viewer]
function foo(x) {
   if (x < 5) return x;
   return foo(x / 2);
}
[Not supported by viewer]
16
[Not supported by viewer]
8
[Not supported by viewer]
4
[Not supported by viewer]
4
[Not supported by viewer]
Step 1
[Not supported by viewer]
Step 2
[Not supported by viewer]
Step 3
[Not supported by viewer]
Step 4
[Not supported by viewer]
function foo(x) {
   if (x < 5) return x;
   return foo(x / 2);
}
[Not supported by viewer]
16
[Not supported by viewer]
--------------------------------------------------------------------------------