├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .vuepress │ └── config.js ├── README.md └── book │ ├── appendix.md │ ├── chapter-fifth.md │ ├── chapter-first.md │ ├── chapter-fourth.md │ ├── chapter-second.md │ ├── chapter-seventh.md │ ├── chapter-sixth.md │ ├── chapter-third.md │ ├── contribution.md │ └── cover-preface.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | test/ 4 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 中的函数式编程 2 | 3 | > 原著由 Dan Mantyla 编写 4 | 5 | 近几年来,随着 Haskell、Scala、Clojure 等学院派原生支持函数式编程的偏门语言越来越受到关注,同时主流的 Java、JavaScript、Python 甚至 C++都陆续支持函数式编程。特别值得一提的是,在 nodejs 出现后,JavaScript 成为第一种从前端到后台的全栈语言,而且 JavaScript 支持多范式编程。应用函数式编程的最大挑战就是思维模式的改变———从传统面向对象的范式变为函数式编程范式。 6 | 7 | 《JavaScript 中的函数式编程》(Functional Programming in JavaScript)是 JavaScript 函数式编程极具代表性的原著书籍,至今未获中文翻译和发售,于是本人尝试翻译,以期在翻译过程中有所收获,也欢迎朋友们加入一起翻译。 8 | 9 | 本书利用业余时间翻译,如有理解和用词错误,还请不吝赐教。 10 | 11 | ![Front End Handbook 2018 Cover](https://blog.ahthw.com/wp-content/uploads/2019/12/Functional_Programming_in_JavaScript.jpg) 12 | 13 | [主站](https://github.ahthw.com/natpagle/) · [下载电子版](https://blog.ahthw.com/wp-content/uploads/2019/12/Dan_Mantyla_Functional_Programming_in_JavaScript.pdf) 14 | 15 | ## 目录和章节 16 | 17 | - [目录:全书章节内容简介](https://github.ahthw.com/natpagle/book/cover-preface.html) 18 | - [第一章:通过一个案例了解 JavaScript 语言能力](https://github.ahthw.com/natpagle/book/chapter-first.html) 19 | - [第二章:函数式编程基础](https://github.ahthw.com/natpagle/book/chapter-second.html) 20 | - [第三章:搭建函数式编程环境](https://github.ahthw.com/natpagle/book/chapter-third.html) 21 | - [第四章:JavaScript 中的函数式编程实现](https://github.ahthw.com/natpagle/book/chapter-fourth.html) 22 | - [第五章:理论范畴](https://github.ahthw.com/natpagle/book/chapter-fifth.html) 23 | - [第六章:JavaScript 中的高级函数和陷阱话题](https://github.ahthw.com/natpagle/book/chapter-sixth.html) 24 | - [第七章:JavaScript 中的函数式和面向对象编程](https://github.ahthw.com/natpagle/book/chapter-seventh.html) 25 | - [附录:JavaScript 中常用函数的函数式方法](https://github.ahthw.com/natpagle/book/appendix.html) 26 | 27 | ## 贡献内容 28 | 29 | 如果你想参与这本书的共同创作、修改或添加内容,可以先 [Fork](https://github.com/hex-translate/natpagle) 这本书的仓库,然后将修改的内容提交 [Pull requests](https://github.com/hex-translate/natpagle/pulls) ;或者创建 [Issues](https://github.com/hex-translate/natpagle/issues)。 30 | 31 | Fork 后的仓库如何同步本仓库? 32 | 33 | ```bash 34 | # 添加 upstream 源,只需执行一次 35 | git remote add upstream git@github.com:hex-translate/natpagle.git 36 | 37 | # 拉取远程代码 38 | git pull upstream master 39 | 40 | # 提交修改 41 | git add . 42 | git commit 43 | 44 | # 更新 fork 仓库 45 | git push origin master 46 | ``` 47 | 48 | 更多参考: [Syncing a fork](https://help.github.com/articles/syncing-a-fork/) 49 | 50 | 注意,本书内容在 `/docs` 目录中, `/dist`是通过脚本自动生成的网站文件。 51 | 52 | ## 生成电子书 53 | 54 | 这本书使用 [Vuepress](https://vuepress.vuejs.org/zh/) 撰写并生成[网站](https://github.com/hex-translate/natpagle.git),请查看 `package.json` 中的 `scripts` 配置和 `/scripts` 目录中的脚本来了解这本书的构建和发布过程。 55 | 56 | ```bash 57 | # 初始化 nodejs 依赖 58 | npm install 59 | 60 | # 安装 vuepress 插件 61 | npm install -g vuepress 62 | 63 | # 进入图书目录 64 | cd docs 65 | 66 | # 开始写作 67 | vuepress dev . 68 | 69 | # 构建静态文件 70 | vuepress build . 71 | 72 | # 查看写作内容 73 | # visit http://localhost:8080 74 | 75 | ``` 76 | 77 | ## 更新日志 78 | 79 | [https://github.com/hex-translate/natpagle/tree/master](https://github.com/hex-translate/natpagle/tree/master) 80 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: "JavaScript中的函数式编程", 3 | description: "JavaScript 中的函数式编程", 4 | dest: "./dist", 5 | base: "/natpagle/", 6 | sidebarDepth: 2, 7 | themeConfig: { 8 | repo: "halldwang/natpagle", 9 | docsBranch: "master", 10 | docsDir: "docs", 11 | editLinkText: "在 GitHub 上编辑此页", 12 | lastUpdated: "上次更新", 13 | editLinks: true, 14 | smoothScroll: true, 15 | nav: [ 16 | { text: "首页", link: "/" }, 17 | { text: "章节", link: "/book/cover-preface" }, 18 | { text: "贡献内容", link: "/book/contribution" } 19 | ], 20 | sidebar: [ 21 | ["./book/cover-preface", "章节和目录"], 22 | ["./book/chapter-first", "第一章:通过一个案例了解JavaScript语言能力"], 23 | ["./book/chapter-second", "第二章:函数式编程基础"], 24 | ["./book/chapter-third", "第三章:搭建函数式编程环境"], 25 | ["./book/chapter-fourth", "第四章:JavaScript中的函数式编程实现"], 26 | ["./book/chapter-fifth", "第五章:理论范畴"], 27 | ["./book/chapter-sixth", "第六章:JavaScript中的高级函数和陷阱话题"], 28 | ["./book/chapter-seventh", "第七章:JavaScript中的函数式和面向对象编程"], 29 | ["./book/appendix", "附录:JavaScript中常用函数的函数式方法"], 30 | ["./book/contribution", "贡献内容"] 31 | ] 32 | } 33 | }; -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 中的函数式编程 2 | 3 | > 原著由 Dan Mantyla 编写 4 | 5 | 近几年来,随着 Haskell、Scala、Clojure 等学院派原生支持函数式编程的偏门语言越来越受到关注,同时主流的 Java、JavaScript、Python 甚至 C++都陆续支持函数式编程。特别值得一提的是,在 nodejs 出现后,JavaScript 成为第一种从前端到后台的全栈语言,而且 JavaScript 支持多范式编程。应用函数式编程的最大挑战就是思维模式的改变———从传统面向对象的范式变为函数式编程范式。 6 | 7 | 《JavaScript 中的函数式编程》(Functional Programming in JavaScript)是 JavaScript 函数式编程极具代表性的原著书籍,至今未获中文翻译和发售,于是本人尝试翻译,以期在翻译过程中有所收获,也欢迎朋友们加入一起翻译。 8 | 9 | 本书利用业余时间翻译,如有理解和用词错误,还请不吝赐教。 10 | 11 | ![Front End Handbook 2018 Cover](https://blog.ahthw.com/wp-content/uploads/2019/12/Functional_Programming_in_JavaScript.jpg) 12 | 13 | [主站](https://github.ahthw.com/natpagle/) · [下载电子版](https://blog.ahthw.com/wp-content/uploads/2019/12/Dan_Mantyla_Functional_Programming_in_JavaScript.pdf) 14 | 15 | ## 目录和章节 16 | 17 | - [目录:全书章节内容简介](https://github.ahthw.com/natpagle/book/cover-preface.html) 18 | - [第一章:通过一个案例了解 JavaScript 语言能力](https://github.ahthw.com/natpagle/book/chapter-first.html) 19 | - [第二章:函数式编程基础](https://github.ahthw.com/natpagle/book/chapter-second.html) 20 | - [第三章:搭建函数式编程环境](https://github.ahthw.com/natpagle/book/chapter-third.html) 21 | - [第四章:JavaScript 中的函数式编程实现](https://github.ahthw.com/natpagle/book/chapter-fourth.html) 22 | - [第五章:理论范畴](https://github.ahthw.com/natpagle/book/chapter-fifth.html) 23 | - [第六章:JavaScript 中的高级函数和陷阱话题](https://github.ahthw.com/natpagle/book/chapter-sixth.html) 24 | - [第七章:JavaScript 中的函数式和面向对象编程](https://github.ahthw.com/natpagle/book/chapter-seventh.html) 25 | - [附录:JavaScript 中常用函数的函数式方法](https://github.ahthw.com/natpagle/book/appendix.html) 26 | 27 | ## 更新日志 28 | 29 | [https://github.com/hex-translate/natpagle/tree/master](https://github.com/hex-translate/natpagle/tree/master) 30 | -------------------------------------------------------------------------------- /docs/book/appendix.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 附录介绍了使用JavaScript进行函数式编程的常用功能: 4 | 5 | - Array Functions: 6 | 7 | ```js 8 | var flatten = function(arrays) { 9 | return arrays.reduce(function(p, n) { 10 | return p.concat(n); 11 | }); 12 | }; 13 | var invert = function(arr) { 14 | return arr.map(function(x, i, a) { 15 | return a[a.length - (i + 1)]; 16 | }); 17 | }; 18 | ``` 19 | 20 | - Binding Functions: 21 | 22 | ```js 23 | var bind = Function.prototype.call.bind(Function.prototype.bind); 24 | var call = bind(Function.prototype.call, Function.prototype.call); 25 | var apply = bind(Function.prototype.call, Function.prototype.apply); 26 | 27 | ``` 28 | 29 | - Category Theory: 30 | 31 | ```js 32 | var checkTypes = function(typeSafeties) { 33 | arrayOf(func)(arr(typeSafeties)); 34 | var argLength = typeSafeties.length; 35 | return function(args) { 36 | arr(args); 37 | if (args.length != argLength) { 38 | throw new TypeError("Expected " + argLength + "arguments"); 39 | } 40 | var results = []; 41 | for (var i = 0; i < argLength; i++) { 42 | results[i] = typeSafeties[i](args[i]); 43 | } 44 | return results; 45 | }; 46 | }; 47 | var homoMorph = function(/* arg1, arg2, ..., argN, output */) { 48 | var before = checkTypes( 49 | arrayOf(func)( 50 | Array.prototype.slice.call(arguments, 0, arguments.length - 1) 51 | ) 52 | ); 53 | var after = func(arguments[arguments.length - 1]); 54 | return function(middle) { 55 | return function(args) { 56 | return after(middle.apply(this, before([].slice.apply(arguments)))); 57 | }; 58 | }; 59 | }; 60 | ``` 61 | 62 | - Composition: 63 | 64 | ```js 65 | Function.prototype.compose = function(prevFunc) { 66 | var nextFunc = this; 67 | return function() { 68 | return; 69 | nextFunc.call(this, prevFunc.apply(this, arguments)); 70 | }; 71 | }; 72 | Function.prototype.sequence = function(prevFunc) { 73 | var nextFunc = this; 74 | return function() { 75 | return; 76 | prevFunc.call(this, nextFunc.apply(this, arguments)); 77 | }; 78 | }; 79 | ``` 80 | 81 | - Currying: 82 | 83 | ```js 84 | Function.prototype.curry = function(numArgs) { 85 | var func = this; 86 | numArgs = numArgs || func.length; 87 | // recursively acquire the arguments 88 | function subCurry(prev) { 89 | return function(arg) { 90 | var args = prev.concat(arg); 91 | if (args.length < numArgs) { 92 | // recursive case: we still need more args 93 | return subCurry(args); 94 | } else { 95 | // base case: apply the function 96 | return func.apply(this, args); 97 | } 98 | }; 99 | } 100 | return subCurry([]); 101 | }; 102 | ``` 103 | 104 | - Functors: 105 | 106 | ```js 107 | // map :: (a -> b) -> [a] -> [b] 108 | var map = function(f, a) { 109 | return arr(a).map(func(f)); 110 | }; 111 | // strmap :: (str -> str) -> str -> str 112 | var strmap = function(f, s) { 113 | return str(s) 114 | .split("") 115 | .map(func(f)) 116 | .join(""); 117 | }; 118 | // fcompose :: (a -> b)* -> (a -> b) 119 | var fcompose = function() { 120 | var funcs = arrayOf(func)(arguments); 121 | return function() { 122 | var argsOfFuncs = arguments; 123 | for (var i = funcs.length; i > 0; i -= 1) { 124 | argsOfFuncs = [funcs[i].apply(this, args)]; 125 | } 126 | return args[0]; 127 | }; 128 | }; 129 | ``` 130 | 131 | - Lenses: 132 | 133 | ```js 134 | var lens = function(get, set) { 135 | var f = function(a) { 136 | return get(a); 137 | }; 138 | f.get = function(a) { 139 | return get(a); 140 | }; 141 | f.set = set; 142 | f.mod = function(f, a) { 143 | return set(a, f(get(a))); 144 | }; 145 | return f; 146 | }; 147 | // usage: 148 | var first = lens( 149 | function(a) { 150 | return arr(a)[0]; 151 | }, // get 152 | function(a, b) { 153 | return [b].concat(arr(a).slice(1)); 154 | } // set 155 | ); 156 | ``` 157 | 158 | - Maybes: 159 | 160 | ```js 161 | var Maybe = function() {}; 162 | Maybe.prototype.orElse = function(y) { 163 | if (this instanceof Just) { 164 | return this.x; 165 | } else { 166 | return y; 167 | } 168 | }; 169 | var None = function() {}; 170 | None.prototype = Object.create(Maybe.prototype); 171 | None.prototype.toString = function() { 172 | return "None"; 173 | }; 174 | var none = function() { 175 | return new None(); 176 | }; 177 | // and the Just instance, a wrapper for an object with a value; 178 | var Just = function(x) { 179 | return (this.x = x); 180 | }; 181 | Just.prototype = Object.create(Maybe.prototype); 182 | Just.prototype.toString = function() { 183 | return "Just " + this.x; 184 | }; 185 | var just = function(x) { 186 | return new Just(x); 187 | }; 188 | var maybe = function(m) { 189 | if (m instanceof None) { 190 | return m; 191 | } else if (m instanceof Just) { 192 | return just(m.x); 193 | } else { 194 | throw new TypeError( 195 | "Error: Just or None expected, " + m.toString() + " given." 196 | ); 197 | } 198 | }; 199 | var maybeOf = function(f) { 200 | return function(m) { 201 | if (m instanceof None) { 202 | return m; 203 | } else if (m instanceof Just) { 204 | return just(f(m.x)); 205 | } else { 206 | throw new TypeError( 207 | "Error: Just or None expected, " + m.toString() + " given." 208 | ); 209 | } 210 | }; 211 | }; 212 | ``` 213 | 214 | - Mixins: 215 | 216 | ```js 217 | Object.prototype.plusMixin = function(mixin) { 218 | var newObj = this; 219 | newObj.prototype = Object.create(this.prototype); 220 | newObj.prototype.constructor = newObj; 221 | for (var prop in mixin) { 222 | if (mixin.hasOwnProperty(prop)) { 223 | newObj.prototype[prop] = mixin[prop]; 224 | } 225 | } 226 | return newObj; 227 | }; 228 | ``` 229 | 230 | - Partial Application: 231 | 232 | ```js 233 | function bindFirstArg(func, a) { 234 | return function(b) { 235 | return func(a, b); 236 | }; 237 | } 238 | Function.prototype.partialApply = function() { 239 | var func = this; 240 | args = Array.prototype.slice.call(arguments); 241 | return function() { 242 | return func.apply(this, args.concat(Array.prototype.slice.call(arguments))); 243 | }; 244 | }; 245 | Function.prototype.partialApplyRight = function() { 246 | var func = this; 247 | args = Array.prototype.slice.call(arguments); 248 | return function() { 249 | return func.apply( 250 | this, 251 | Array.protype.slice.call(arguments, 0).concat(args) 252 | ); 253 | }; 254 | }; 255 | ``` 256 | 257 | - Trampolining: 258 | 259 | ```js 260 | var trampoline = function(f) { 261 | while (f && f instanceof Function) { 262 | f = f.apply(f.context, f.args); 263 | } 264 | return f; 265 | }; 266 | var thunk = function(fn) { 267 | return function() { 268 | var args = Array.prototype.slice.apply(arguments); 269 | return function() { 270 | return fn.apply(this, args); 271 | }; 272 | }; 273 | }; 274 | ``` 275 | 276 | - Type Safeties: 277 | 278 | ```js 279 | var typeOf = function(type) { 280 | return function(x) { 281 | if (typeof x === type) { 282 | return x; 283 | } else { 284 | throw new TypeError( 285 | "Error: " + type + " expected, " + typeof x + " given." 286 | ); 287 | } 288 | }; 289 | }; 290 | var str = typeOf("string"), 291 | num = typeOf("number"), 292 | func = typeOf("function"), 293 | bool = typeOf("boolean"); 294 | 295 | var objectTypeOf = function(name) { 296 | return function(o) { 297 | if (Object.prototype.toString.call(o) === "[object " + name + "]") { 298 | return o; 299 | } else { 300 | throw new TypeError("Error: '+name+' expected, something else given."); 301 | } 302 | }; 303 | }; 304 | 305 | var obj = objectTypeOf("Object"); 306 | var arr = objectTypeOf("Array"); 307 | var date = objectTypeOf("Date"); 308 | var div = objectTypeOf("HTMLDivElement"); 309 | // arrayOf :: (a -> b) -> ([a] -> [b]) 310 | var arrayOf = function(f) { 311 | return function(a) { 312 | return map(func(f), arr(a)); 313 | }; 314 | }; 315 | ``` 316 | 317 | - Y-combinator: 318 | 319 | ```js 320 | var Y = function(F) { 321 | return (function(f) { 322 | return f(f); 323 | })(function(f) { 324 | return F(function(x) { 325 | return f(f)(x); 326 | }); 327 | }); 328 | }; 329 | // Memoizing Y-Combinator: 330 | var Ymem = function(F, cache) { 331 | if (!cache) { 332 | cache = {}; // Create a new cache. 333 | } 334 | return function(arg) { 335 | if (cache[arg]) { 336 | // Answer in cache 337 | return cache[arg]; 338 | } 339 | // else compute the answer 340 | var answer = F(function(n) { 341 | return Ymem(F, cache)(n); 342 | })(arg); // Compute the answer. 343 | cache[arg] = answer; // Cache the answer. 344 | return answer; 345 | }; 346 | }; 347 | ``` 348 | -------------------------------------------------------------------------------- /docs/book/chapter-fifth.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 托马斯·沃森(Thomas Watson)曾有一句名言:“我认为世界上可能有五台电脑的市场。”。那是在1948年。那时,每个人都知道计算机只能用于两件事:数学和工程学。即使是科技界最有头脑的人也无法预测,有一天,计算机将能够把西班牙语翻译成英语,或者模拟整个天气系统。当时,最快的机器是IBM的SSEC,每秒50次乘法,多个用户终端共享一个处理器。晶体管改变了一切,但科技公司的远见者仍然没有达到目标。肯·奥尔森(Ken Olson)在1977年做出了另一个著名的愚蠢预测,当时他说“没有理由任何人想要在家里有一台电脑”。 4 | 5 | 当今,计算机不仅仅是为科学家和工程师服务的,70年前,认为机器不仅仅能做数学的想法根本不是直觉。沃森没有意识到计算机是如何改变一个社会的,他也没有意识到数学的变革和进化的力量。 6 | 7 | 但并不是每个人都忽视了计算机和数学的潜力。John McCarthy在1958年发明了Lisp,这是一种革命性的基于算法的语言,开创了计算的新纪元。从一开始,Lisp就在使用抽象层-编译器、解释器、虚拟化-来推动计算机从核心数学机器发展到今天的发展方面发挥了重要作用。 8 | 9 | Lisp衍生的Scheme是JavaScript的语言祖先。现在我们又回到了原点。如果计算机的核心是只做数学运算的机器,那么基于数学的编程范例理所当然会出类拔萃。 10 | 11 | 这里使用的术语“数学”不是用来描述计算机显然能做的“数字运算”,而是用来描述离散数学:研究离散的数学结构,如逻辑中的语句或计算机语言的指令。通过将代码视为一种离散的数学结构,我们可以将数学中的概念和思想应用到代码中。这就是函数式编程在人工智能、图形搜索、模式识别和计算机科学中的其他重大挑战中发挥如此重要作用的原因。 12 | 13 | 在本章中,我们将试验其中一些概念及其在日常编程挑战中的应用。它们将包括: 14 | 15 | - Category theory 范畴论 16 | - Morphisms 态射 17 | - Functors 函子 18 | - Maybes 19 | - Promises 20 | - lenses 状态管理(依赖)[原理](https://blog.csdn.net/weixin_34032792/article/details/87963097) 21 | - Function composition 函数组合 22 | 23 | 有了这些概念,才能够非常容易和安全地编写函数库和api。我们将从解释范畴理论到用JavaScript正式实现它。 24 | 25 | ## 范畴论 26 | 27 | 范畴理论是赋予功能构成权力的理论概念。(此处有删减,举例:[让你烤个面包](https://blog.csdn.net/weixin_43801661/article/details/84670359)。) 28 | 29 | ### 范畴论简述 30 | 31 | 范畴论不是一个很难的概念。 它在数学上的位置足以填满整个研究生水平的大学课程,但是在计算机编程中的位置可以很容易地总结出来。 32 | 33 | 爱因斯坦曾经说过,“如果你不能向一个6岁的孩子解释它,那么你自己也不清楚”。因此,本着向6岁儿童解释的精神,范畴理论只是把这些点联系起来。尽管它可能过于简化范畴理论,但它确实很好地以一种直截了当的方式解释了我们需要知道的东西。 34 | 35 | 首先你需要知道一些术语。类别只是具有相同类型的集合。在JavaScript中,它们是数组或对象,包含显式声明为数字、字符串、布尔值、日期、节点等的变量。态射是纯函数,当给定一组特定的输入时,总是返回相同的输出。同态操作仅限于一个类别,而多态操作可以操作多个类别。例如,同态函数乘法适用于数字类型,但多态函数加法也可以对字符串起作用。 36 | 37 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/f8b.png) 38 | 39 | 下图显示了三个类别-A、B和C-以及两个态射-ƒ和ɡ。 40 | 范畴理论告诉我们,当我们有两个态射,其中第一个态射的范畴是另一个态射的期望输入时,它们可以组成如下: 41 | 42 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/1577544451533.jpg) 43 | 44 | ƒog 符号是同态ƒ和g的组合。现在我们只需要把点连接起来。 45 | 46 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/1577545148359.jpg) 47 | 48 | 只是连接点而已,仅此而已。 49 | 50 | ## 类型安全 51 | 52 | 让我们把这些点连起来。包含两个内容: 53 | 54 | 1. Objects对象(在JavaScript中,types)。 55 | 2. Morphisms态射(在JavaScript中,仅适用于纯函数的类型)。 56 | 57 | 这些是数学家给范畴理论的术语,因此我们的JavaScript术语中有一些不幸的术语重载。范畴理论中的对象更像是具有显式数据类型的变量,而不是JavaScript定义的对象属性和值集合。形态只是使用这些类型的纯函数。 58 | 59 | 因此,将范畴理论的思想应用于JavaScript很容易。在JavaScript中使用范畴理论意味着在每个范畴中使用一种特定的数据类型。数据类型包括数字、字符串、数组、日期、对象、布尔值等。但是,由于JavaScript中没有严格的类型系统,可能会出错。所以我们必须自己实现方法来确保数据正确。 60 | 61 | JavaScript中有四种基本数据类型:数字、字符串、布尔值和函数。我们可以创建返回变量或抛出错误的类型安全函数。 62 | 63 | ```js 64 | var str = function(s) { 65 | if (typeof s === "string") { 66 | return s; 67 | } else { 68 | throw new TypeError("Error: String expected, " + typeof s + "given."); 69 | } 70 | }; 71 | var num = function(n) { 72 | if (typeof n === "number") { 73 | return n; 74 | } else { 75 | throw new TypeError("Error: Number expected, " + typeof n + "given."); 76 | } 77 | }; 78 | var bool = function(b) { 79 | if (typeof b === "boolean") { 80 | return b; 81 | } else { 82 | throw new TypeError("Error: Boolean expected, " + typeof b + "given."); 83 | } 84 | }; 85 | var func = function(f) { 86 | if (typeof f === "function") { 87 | return f; 88 | } else { 89 | throw new TypeError("Error: Function expected, " + typeof f + " given."); 90 | } 91 | }; 92 | ``` 93 | 94 | 虽然以上有很多重复的代码,但它们的功满足需要。 相反,我们可以创建一个函数,该函数返回另一个函数,即类型安全函数。 95 | 96 | ```js 97 | var typeOf = function(type) { 98 | return function(x) { 99 | if (typeof x === type) { 100 | return x; 101 | } else { 102 | throw new TypeError( 103 | "Error: " + type + " expected, " + typeof x + "given." 104 | ); 105 | } 106 | }; 107 | }; 108 | var str = typeOf("string"), 109 | num = typeOf("number"), 110 | func = typeOf("function"), 111 | bool = typeOf("boolean"); 112 | ``` 113 | 114 | 我们使用它们来确保我们的函数按预期运行。 115 | 116 | ```js 117 | // unprotected method: 118 | var x = "24"; 119 | x + 1; // will return '241', not 25 120 | // protected method 121 | // plusplus :: Int -> Int 122 | function plusplus(n) { 123 | return num(n) + 1; 124 | } 125 | plusplus(x); // throws error, preferred over unexpected output 126 | ``` 127 | 128 | 让我们看一个更重要的例子。 如果要检查由JavaScript函数Date.parse()返回的Unix时间戳的长度,不是字符串,而是数字,则必须使用str()函数。 129 | 130 | ```js 131 | // timestampLength :: String -> Int 132 | function timestampLength(t) { 133 | return num(str(t).length); 134 | } 135 | timestampLength(Date.parse("12/31/1999")); // throws error 136 | timestampLength(Date.parse("12/31/1999").toString()); // returns 12 137 | ``` 138 | 139 | 像这样显式地将一种类型转换为另一种类型(或同一类型)的函数称为态射。这满足了范畴论的态射公理。这些通过类型安全性函数的强制类型声明以及使用它们的态射是我们在JavaScript中表示类别概念所需要的一切。 140 | 141 | ## 对象标识 142 | 143 | 还有一种重要的数据类型:对象。 144 | 145 | ```js 146 | var obj = typeOf("object"); 147 | obj(123); // throws error 148 | obj({ x: "a" }); // returns {x:'a'} 149 | ``` 150 | 151 | 但是,对象是不同的。它们可以继承。并非原语的所有内容(数字,字符串,布尔值和函数)都是一个对象,包括数组,日期,元素等。 152 | 153 | 我们无法从`typeof`关键字中知道某个对象是什么类型的,比如从typeof关键字中知道JavaScript“object”是什么子类型,所以我们必须想其它办法。对象有一个toString()函数,我们可以的劫持它。 154 | 155 | ```js 156 | var obj = function(o) { 157 | if (Object.prototype.toString.call(o) === "[object Object]") { 158 | return o; 159 | } else { 160 | throw new TypeError("Error: Object expected, something else given."); 161 | } 162 | }; 163 | ``` 164 | 165 | 在所有对象都存在的情况下,我们应该实现一些代码复用。 166 | 167 | ```js 168 | var objectTypeOf = function(name) { 169 | return function(o) { 170 | if (Object.prototype.toString.call(o) === "[object " + name + "]") { 171 | return o; 172 | } else { 173 | throw new TypeError("Error: '+name+' expected, something else given."); 174 | } 175 | }; 176 | }; 177 | var obj = objectTypeOf("Object"); 178 | var arr = objectTypeOf("Array"); 179 | var date = objectTypeOf("Date"); 180 | var div = objectTypeOf("HTMLDivElement"); 181 | ``` 182 | 183 | 以上对于我们的下一个主题:函子将非常有用。 184 | 185 | ## 函子 186 | 187 | 态射是类型之间的映射,函子是类别之间的映射。 可以将它们视为将值从容器中取出,变形然后再将其放入新容器中的函数。 第一个输入是该类型的词素,第二个输入是容器。 188 | 189 | > 函子的类型签名如:// myFunctor ::(a-> b)-> f a-> f b 这就是说,“给我一个接受a并返回b和包含a(s)的box的函数,然后我将返回包含b(s)的box。 190 | 191 | ### 创建函子 192 | 193 | 事实证明,我们已经有一个函子:map()。 它获取容器,数组中的值,并对其应用函数。 194 | 195 | ```js 196 | [1, 4, 9].map(Math.sqrt); // Returns: [1, 2, 3] 197 | ``` 198 | 199 | 但是,我们需要将其编写为全局函数,而不是数组对象的方法。 这将使我们以后可以编写更干净,更安全的代码。 200 | 201 | ```js 202 | // map :: (a -> b) -> [a] -> [b] 203 | var map = function(f, a) { 204 | return arr(a).map(func(f)); 205 | }; 206 | ``` 207 | 208 | 这个例子看起来像是一个设计的包装器,因为我们只是搭载在map()函数上。但这是有目的的。它为其他类型的映射提供模板。 209 | 210 | ```js 211 | // strmap :: (str -> str) -> str -> str 212 | var strmap = function(f, s) { 213 | return str(s).split('').map(func(f)).join(''); 214 | } 215 | // MyObject#map :: (myValue -> a) -> a 216 | MyObject.prototype.map(f{ 217 | return func(f)(this.myValue); 218 | } 219 | ``` 220 | 221 | ### 数组和函子 222 | 223 | 数组是在函数式JavaScript中处理数据的首选方法。 224 | 225 | 有没有一种更简单的方法来创建已经分配给态射的函子?是的,它叫arrayOf。当你传入一个期望整数的态射并返回一个数组时,你得到一个期望整数数组的态射并返回一个数组数组。 226 | 227 | 它本身不是函子,但它使我们能够根据态射来创建函子。 228 | 229 | ```js 230 | // arrayOf :: (a -> b) -> ([a] -> [b]) 231 | var arrayOf = function(f) { 232 | return function(a) { 233 | return map(func(f), arr(a)); 234 | }; 235 | }; 236 | ``` 237 | 238 | 这是通过使用态射来创建函子的方法: 239 | 240 | ```js 241 | var plusplusall = arrayOf(plusplus); // plusplus is our morphism 242 | console.log(plusplusall([1, 2, 3])); // returns [2,3,4] 243 | console.log(plusplusall([1, "2", 3])); // error is thrown 244 | ``` 245 | 246 | arrayOf函数的有趣之处在于它也可以处理类型安全性。 当为字符串传递类型安全函数时,将返回字符串数组的类型安全函数。 类型安全性像身份函数态射一样对待。 这为数组包含所有正确的类型提供了方式。 247 | 248 | ## 回顾函数组合 249 | 250 | 函数是我们可以为其创建函子的另一种原语。 该函子称为`fcompose`。我们将函子定义为从容器中获取值并对其应用函数的对象。 当该容器是一个函数时,我们只需调用它即可获取其内部值。 251 | 252 | 我们已经知道什么是函数组合,但是让我们看看它们在范畴理论驱动的环境中可以做什么。 253 | 254 | 函数组合是关联的。如果你的高中代数老师和我一样,教你什么是属性,而不是它能做什么。实际上,组合是关联的属性所能做的事情。 255 | 256 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/WX20191229-230353@2x.png) 257 | 258 | 我们可以进行任何内部组合,无论如何组合。 请勿将此与可交换属性混淆。ƒ o g并不总是等于g o ƒ。换言之,字符串的第一个单词的反义词与字符串的第一个单词的反义词不同。 259 | 260 | 这一切意味着,只要每个函数的输入来自前一个函数的输出,那么应用哪个函数和以什么顺序无关紧要。但是,如果右边的函数依赖左边的函数,那么就不能只有一个求值顺序吗?从左到右?但是如果将其封装起来,那么我们可以把控它,不管我们感觉如何。这就是在JavaScript中惰性计算的原因。 261 | 262 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/8027-86964e9a39d1.png) 263 | 264 | 让我们重写函数组成,而不是作为函数原型的扩展,而是作为一个独立的函数,它将使我们受益匪浅。 基本形式如下: 265 | 266 | ```js 267 | var fcompose = function(f, g) { 268 | return function() { 269 | return f.call(this, g.apply(this, arguments)); 270 | }; 271 | }; 272 | ``` 273 | 274 | 但我们需要它来处理任意数量的实参。 275 | 276 | ```js 277 | var fcompose = function() { 278 | // first make sure all arguments are functions 279 | var funcs = arrayOf(func)(arguments); 280 | // return a function that applies all the functions 281 | return function() { 282 | var argsOfFuncs = arguments; 283 | for (var i = funcs.length; i > 0; i -= 1) { 284 | argsOfFuncs = [funcs[i].apply(this, args)]; 285 | } 286 | return args[0]; 287 | }; 288 | }; 289 | // example: 290 | var f = fcompose(negate, square, mult2, add1); 291 | f(2); // Returns: -36 292 | ``` 293 | 294 | 既然我们已经封装了这些函数,我们就可以控制它们了。我们可以重写compose函数,以便每个函数接受另一个函数作为输入,存储它,并返回一个执行相同操作的对象。我们可以接受源中每个元素的一个数组,执行组合的所有操作(每个map()、filter()等等,组合在一起),最后将结果存储到一个新数组中,而不是接受一个数组作为输入,对它执行一些操作,然后为每个操作返回一个新数组。这是通过函数组合的惰性求值。没有理由在这里重新发明轮子。许多库都很好地实现了这个概念,包括Lazy.js、Bacon.js和wu.js库。 295 | 296 | 由于采用了这种不同的模型,我们可以做更多的事情:异步迭代,异步事件处理,惰性求值,甚至自动并行化。 297 | 298 | `自动并行化?在计算机科学界有一个说法:不可能。但这真的不可能吗?摩尔定律的下一个进化飞跃可能是一个编译器,它可以为我们并行化代码,函数组合也可以吗?不,这样不太管用。JavaScript引擎实际上是在进行并行化,不是自动的,而是经过深思熟虑的代码。Compose只是让引擎有机会将其拆分为并行进程。但这本身就很酷。` 299 | 300 | ## Monads 301 | 302 | Monad是可帮助您编写功能的工具。 303 | 304 | 与基本类型一样,monad是可以用作函子“触及”的容器的结构。函子抓取数据,对其进行处理,将其放入一个新的monad中,然后返回。 305 | 306 | 我们将关注三个monad: 307 | 308 | - Maybes 309 | - Promises 310 | - Lenses 311 | 312 | 所以除了数组(map)和函数(compose),我们还有五个函子(map,compose,may,promise和lens)。这些只是许多其他函子和单子中的一部分。 313 | 314 | ### Maybes 315 | 316 | Maybes允许我们优雅地处理可能为空并具有默认值的数据。maybe是一个变量,它要么有一些值要么没有,对调用者来说无关紧要。 317 | 318 | 就其本身而言,这似乎没什么大不了的。大家都知道,使用if-else语句很容易完成空检查: 319 | 320 | ```js 321 | if (getUsername() == null) { 322 | username = "Anonymous"; 323 | } else { 324 | username = getUsername(); 325 | } 326 | ``` 327 | 328 | 但是在函数式编程中,我们脱离了逐行处理方式,而是使用函数和数据的管道。如果我们必须在中间断开链来检查值是否存在,我们就必须创建临时变量并编写更多代码。Maybes是帮助我们保持逻辑在管道中流动的工具。 329 | 330 | 要实现Maybes,我们首先需要创建一些构造函数。 331 | 332 | ```js 333 | // the Maybe monad constructor, empty for now 334 | var Maybe = function() {}; 335 | // the None instance, a wrapper for an object with no value 336 | var None = function() {}; 337 | None.prototype = Object.create(Maybe.prototype); 338 | None.prototype.toString = function() { 339 | return "None"; 340 | }; 341 | // now we can write the `none` function 342 | // saves us from having to write `new None()` all the time 343 | var none = function() { 344 | return new None(); 345 | }; 346 | // and the Just instance, a wrapper for an object with a value 347 | var Just = function(x) { 348 | return (this.x = x); 349 | }; 350 | Just.prototype = Object.create(Maybe.prototype); 351 | Just.prototype.toString = function() { 352 | return "Just " + this.x; 353 | }; 354 | var just = function(x) { 355 | return new Just(x); 356 | }; 357 | ``` 358 | 359 | 我们可以编写maybe函数。它返回一个新函数,该函数要么不返回任何内容,要么返回maybe。 360 | 361 | 它是一个函子: 362 | 363 | ```js 364 | var maybe = function(m) { 365 | if (m instanceof None) { 366 | return m; 367 | } else if (m instanceof Just) { 368 | return just(m.x); 369 | } else { 370 | throw new TypeError( 371 | "Error: Just or None expected, " + m.toString() + " given." 372 | ); 373 | } 374 | }; 375 | ``` 376 | 377 | 我们也可以像创建数组一样创建函子生成器: 378 | 379 | ```js 380 | var maybeOf = function(f) { 381 | return function(m) { 382 | if (m instanceof None) { 383 | return m; 384 | } else if (m instanceof Just) { 385 | return just(f(m.x)); 386 | } else { 387 | throw new TypeError( 388 | "Error: Just or None expected, " + m.toString() + " given." 389 | ); 390 | } 391 | }; 392 | }; 393 | ``` 394 | 395 | 所以Maybe是monad,maybe是函子,maybeOf返回已经分配给态射的函子。我们需要向Maybe monad对象添加一个方法,帮助我们更直观地使用它。 396 | 397 | ```js 398 | Maybe.prototype.orElse = function(y) { 399 | if (this instanceof Just) { 400 | return this.x; 401 | } else { 402 | return y; 403 | } 404 | }; 405 | ``` 406 | 407 | 在其原型中,maybes可以直接使用。 408 | 409 | ```js 410 | maybe(just(123)).x; // Returns 123 411 | maybeOf(plusplus)(just(123)).x; // Returns 124 412 | maybe(plusplus)(none()).orElse("none"); // returns 'none' 413 | ``` 414 | 415 | 任何返回一个随后被执行的方法的操作都非常复杂。所以我们可以通过调用curry()函数使它更简洁一些。 416 | 417 | ```js 418 | maybePlusPlus = maybeOf.curry()(plusplus); 419 | maybePlusPlus(just(123)).x; // returns 123 420 | maybePlusPlus(none()).orElse("none"); // returns none 421 | ``` 422 | 423 | 当抽象出直接调用none()和just()函数时,maybes的真正强大就会显露。我们将使用一个示例对象User来实现这一点,该对象使用mays作为用户名。 424 | 425 | ```js 426 | var User = function() { 427 | this.username = none(); // initially set to `none` 428 | }; 429 | User.prototype.setUsername = function(name) { 430 | this.username = just(str(name)); // it's now a `just 431 | }; 432 | User.prototype.getUsernameMaybe = function() { 433 | var usernameMaybe = maybeOf.curry()(str); 434 | return usernameMaybe(this.username).orElse("anonymous"); 435 | }; 436 | var user = new User(); 437 | user.getUsernameMaybe(); // Returns 'anonymous' 438 | user.setUsername("Laura"); 439 | user.getUsernameMaybe(); // Returns 'Laura' 440 | ``` 441 | 442 | 我们有了一个强大而安全的方法来定义默认值。记住这个User对象,因为我们将在本章后面使用它。 443 | 444 | ## Promises 445 | 446 | `承诺的本质是它们不受环境变化的影响。-Frank Underwood,纸牌屋` 447 | 448 | 在函数式编程中,我们经常使用管道和数据流:函数链,其中每个函数产生的数据类型将被下一个消耗。但是,其中许多函数是异步的:readFile、events、AJAX等。与其使用连续传递样式和深度嵌套的回调,不如如何修改这些函数的返回类型以指示结果?通过将它们包装在promises中。 449 | 450 | Promises就像是回调的功能等价物。显然,回调不具有全部功能,因为如果有多个函数正在改变相同的数据,则可能存在争用条件和错误。Promises solve能解决这个问题。 451 | 452 | 你应该用Promises来解决它: 453 | 454 | ```js 455 | fs.readFile("file.json", function(err, val) { 456 | if (err) { 457 | console.error("unable to read file"); 458 | } else { 459 | try { 460 | val = JSON.parse(val); 461 | console.log(val.success); 462 | } catch (e) { 463 | console.error("invalid json in file"); 464 | } 465 | } 466 | }); 467 | ``` 468 | 469 | 使用以下代码: 470 | 471 | ```js 472 | fs.readFileAsync("file.json") 473 | .then(JSON.parse) 474 | .then(function(val) { 475 | console.log(val.success); 476 | }) 477 | .catch(SyntaxError, function(e) { 478 | console.error("invalid json in file"); 479 | }) 480 | .catch(function(e) { 481 | console.error("unable to read file"); 482 | }); 483 | ``` 484 | 485 | 上面的代码来自[bluebird](https://github.com/petkaantonov/bluebird)的README:一个功能齐全的Promises/a+实现,性能非常好。Promises/A+是用JavaScript实现Promises的规范。(当时考虑到JavaScript社区目前的争论,我们将把实现留给Promises/A+团队,因为它比maybes复杂得多。) 486 | 487 | 但这里有一个实现的片段: 488 | 489 | ```js 490 | // the Promise monad 491 | var Promise = require("bluebird"); 492 | // the promise functor 493 | var promise = function(fn, receiver) { 494 | return function() { 495 | var slice = Array.prototype.slice, 496 | args = slice.call(arguments, 0, fn.length - 1), 497 | promise = new Promise(); 498 | args.push(function() { 499 | var results = slice.call(arguments), 500 | error = results.shift(); 501 | if (error) promise.reject(error); 502 | else promise.resolve.apply(promise, results); 503 | }); 504 | fn.apply(receiver, args); 505 | return promise; 506 | }; 507 | }; 508 | ``` 509 | 510 | 现在,我们可以使用promise()函数将回调函数转换为返回promises的函数。 511 | 512 | ```js 513 | var files = ["a.json", "b.json", "c.json"]; 514 | readFileAsync = promise(fs.readFile); 515 | var data = files 516 | .map(function(f) { 517 | readFileAsync(f).then(JSON.parse); 518 | }) 519 | .reduce(function(a, b) { 520 | return $.extend({}, a, b); 521 | }); 522 | ``` 523 | 524 | ## Lenses 525 | 526 | 程序员真正喜欢monad的另一个原因是它们使编写库变得非常容易。为了探索这一点,让我们使用更多的函数来扩展User对象以getting和getting值,但是,我们将使用lenses而不是使用getter和setter。 527 | 528 | lenses是一流的getter和setter。它们不仅允许我们获取和设置变量,还允许我们在变量上运行函数。但是,它不是对原始数据进行改变,而是克隆并返回由函数修改的新数据。它们迫使数据是不可变的,这对于安全性和一致性以及库都非常有用。无论应用程序是什么,它们都非常适合优雅的代码,引入额外的数组副本对性能的影响不是很大。 529 | 530 | 在编写lens()函数之前,让我们看看它是如何工作的。 531 | 532 | ```js 533 | var first = lens( 534 | function(a) { 535 | return arr(a)[0]; 536 | }, // get 537 | function(a, b) { 538 | return [b].concat(arr(a).slice(1)); 539 | } // set 540 | ); 541 | first([1, 2, 3]); // outputs 1 542 | first.set([1, 2, 3], 5); // outputs [5, 2, 3] 543 | function tenTimes(x) { 544 | return x * 10; 545 | } 546 | first.modify(tenTimes, [1, 2, 3]); // outputs [10,2,3] 547 | ``` 548 | 549 | 这是lens()函数的工作方式。 它返回定义了get,set和mod的defined.函数。 lens()函数本身就是一个函子。 550 | 551 | ```js 552 | var lens = fuction(get, set) { 553 | var f = function (a) { 554 | return get(a) 555 | }; 556 | 557 | f.get = function (a) {return get(a)}; 558 | f.set = set; 559 | f.mod = function (f, a) {return set(a, f(get(a)))}; 560 | 561 | return f; 562 | }; 563 | ``` 564 | 565 | 我们来举个例子。我们将在前面的示例中扩展User对象。 566 | 567 | ```js 568 | // userName :: User -> str 569 | var userName = lens( 570 | function(u) { 571 | return u.getUsernameMaybe(); 572 | }, // get 573 | function(u, v) { 574 | // set 575 | u.setUsername(v); 576 | return u.getUsernameMaybe(); 577 | } 578 | ); 579 | var bob = new User(); 580 | bob.setUsername("Bob"); 581 | userName.get(bob); // returns 'Bob' 582 | userName.set(bob, "Bobby"); //return 'Bobby' 583 | userName.get(bob); // returns 'Bobby' 584 | userName.mod(strToUpper, bob); // returns 'BOBBY' 585 | strToUpper.compose(userName.set)(bob, "robert"); // returns "ROBERT"; 586 | userName.get(bob); // returns 'robert' 587 | ``` 588 | 589 | ## jQuery是一个monad 590 | 591 | 如果你认为所有这些抽象的关于范畴、函子和单子都没有实际的应用,请考虑一下。 jQuery是流行的JavaScript库,它为使用HTML提供了一个增强的接口,它提供了用于处理HTML的增强接口,它是一个独立的库。 592 | 593 | jQuery对象是monad,其方法是函子实际上,它们是一种称为endofunctors的特殊函子。 Endofunctors是返回与输入相同类别的函子,即F :: X->X。每个jQuery方法都接受一个jQuery对象并返回一个jQuery对象,该对象允许方法被链接,并且它们将具有类型签名。jFunc::jQuery obj->jQuery obj。 594 | 595 | 这也是jQuery插件框架的功能所在。 如果插件将jQuery对象作为输入并返回一个作为输出,那么可以将其插入链中。 596 | 597 | monad是函子“接触”以获取数据的容器。 这样,数据可以由库保护和控制。 jQuery通过其许多方法提供对基础数据(包装的HTML元素集)的访问。 598 | 599 | jQuery对象本身是匿名函数调用的结果。 600 | 601 | ```js 602 | var jQuery = (function () { 603 | var j = function (selector, context) { 604 | var jq-obj = new j.fn.init(selector, context); 605 | return jq-obj; 606 | }; 607 | 608 | j.fn = j.prototype = { 609 | init: function (selector, context) { 610 | if (!selector) { 611 | return this; 612 | } 613 | } 614 | }; 615 | 616 | j.fn.init.prototype = j.fn; 617 | return j; 618 | })(); 619 | ``` 620 | 621 | 在此高度简化的jQuery版本中,它返回定义j对象的函数,该对象实际上只是增强的init构造函数。 622 | 623 | ```js 624 | var $ = jQuery(); // the function is returned and assigned to `$` 625 | var x = $("#select-me"); // jQuery object is returned 626 | ``` 627 | 628 | 就像函子将值从容器中取出一样,jQuery包装HTML元素并提供对它们的访问,而不是直接修改HTML元素。 629 | 630 | jQuery并不经常公布这一点,但它有自己的map()方法来将HTML元素对象从包装器中取出。 就像fmap()方法一样,元素被提升,对它们进行某些处理,然后将它们放回容器中。 这就是jQuery运行中的许多后台命令工作原理。 631 | 632 | ```js 633 | $("li").map(function(index, element) { 634 | // do something to the element 635 | return element; 636 | }); 637 | 638 | ``` 639 | 640 | 另一个用于处理HTML元素的库Prototype.js不能像这样工作。Prototype直接通过helpers修改HTML元素。因此,它在JavaScript社区中并没有得到推崇。 641 | 642 | ## 功能实现 643 | 644 | 现在是我们正式将范畴理论定义为JavaScript对象的时候了。 类别是对象(类型)和态射(仅对这些类型起作用的函数)。 这是一种非常高级的,完全声明式的编程方式,它可以确保代码极其安全可靠,对于担心并发性和类型安全性的API和库来说,这是完美的。 645 | 646 | 首先,我们需要一个函数来帮助我们创建态射。 我们将其称为homoMorph(),因为它们将是同态的。 它将返回一个函数,它将返回一个期望传递函数的函数,并根据输入生成该函数的组合。输入是态射接受作为输入并给予作为输出的类型。 就像我们的类型签名一样,即`// morph :: num-> num-> [num]`,只有最后一个是输出。 647 | 648 | ```js 649 | var homoMorph = function() /* input1, input2,..., inputN, output */ 650 | { 651 | var before = checkTypes( 652 | arrayOf(func)( 653 | Array.prototype.slice.call(arguments, 0, arguments.length - 1) 654 | ) 655 | ); 656 | var after = func(arguments[arguments.length - 1]); 657 | return function(middle) { 658 | return function(args) { 659 | return after(middle.apply(this, before([].slice.apply(arguments)))); 660 | }; 661 | }; 662 | }; 663 | // now we don't need to add type signature comments 664 | // because now they're built right into the function declaration 665 | add = homoMorph( 666 | num, 667 | num, 668 | num 669 | )(function(a, b) { 670 | return a + b; 671 | }); 672 | add(12, 24); // returns 36 673 | add("a", "b"); // throws error 674 | homoMorph( 675 | num, 676 | num, 677 | num 678 | )(function(a, b) { 679 | return a + b; 680 | })(18, 24); // returns 42 681 | ``` 682 | 683 | homoMorph()函数非常复杂。 它使用闭包(请参见第2章,函数编程基础)返回一个接受函数并检查其输入和输出值以确保类型安全的函数。 为此,它依赖于一个辅助函数:checkTypes,其定义如下: 684 | 685 | ```js 686 | var checkTypes = function(typeSafeties) { 687 | arrayOf(func)(arr(typeSafeties)); 688 | var argLength = typeSafeties.length; 689 | return function(args) { 690 | arr(args); 691 | if (args.length != argLength) { 692 | throw new TypeError("Expected " + argLength + " arguments"); 693 | } 694 | var results = []; 695 | for (var i = 0; i < argLength; i++) { 696 | results[i] = typeSafeties[i](args[i]); 697 | } 698 | return results; 699 | }; 700 | }; 701 | ``` 702 | 703 | 现在让我们正式定义一些同态(homomorphisms)。 704 | 705 | ```js 706 | var lensHM = homoMorph(func, func, func)(lens); 707 | 708 | var userNameHM = lensHM( 709 | function(u) { 710 | return u.getUsernameMaybe(); 711 | }, // get 712 | function(u, v) { 713 | // setu.setUsername(v); 714 | return u.getUsernameMaybe(); 715 | } 716 | ); 717 | 718 | var strToUpperCase = homoMorph( 719 | str, 720 | str 721 | )(function(s) { 722 | return s.toUpperCase(); 723 | }); 724 | 725 | var morphFirstLetter = homoMorph( 726 | func, 727 | str, 728 | str 729 | )(function(f, s) { 730 | return f(s[0]).concat(s.slice(1)); 731 | }); 732 | 733 | var capFirstLetter = homoMorph( 734 | str, 735 | str 736 | )(function(s) { 737 | return morphFirstLetter(strToUpperCase, s); 738 | }); 739 | ``` 740 | 741 | 最后,我们可以综合下。 以下示例包括函数组和,lens,同态性(homomorphisms)等等。 742 | 743 | ```js 744 | // homomorphic lenses 745 | var bill = new User(); 746 | userNameHM.set(bill, "William"); // Returns: 'William' 747 | userNameHM.get(bill); // Returns: 'William' 748 | 749 | // compose 750 | var capatolizedUsername = fcompose(capFirstLetter, userNameHM.get); 751 | capatolizedUsername(bill, "bill"); // Returns: 'Bill' 752 | 753 | // it's a good idea to use homoMorph on .set and .get too 754 | var getUserName = homoMorph(obj, str)(userNameHM.get); 755 | var setUserName = homoMorph(obj, str, str)(userNameHM.set); 756 | getUserName(bill); // Returns: 'Bill' 757 | setUserName(bill, "Billy"); // Returns: 'Billy' 758 | 759 | // now we can rewrite capatolizeUsername with the new setter 760 | capatolizedUsername = fcompose(capFirstLetter, setUserName); 761 | capatolizedUsername(bill, "will"); // Returns: 'Will' 762 | getUserName(bill); // Returns: 'will' 763 | ``` 764 | 765 | 以上的代码是非常声明式的,安全的,可靠的。 766 | 767 | `代码是声明式的意味着什么? 在“命令式”编程中,我们编写一系列指令,以告诉机器如何执行所需的操作。 在函数式编程中,我们描述值之间的关系,这些值告诉机器我们要计算的内容,并且机器找出指令序列来实现它。 函数式编程是声明性的。` 768 | 769 | 整个库和api都可以这样构造,这样程序员就可以自由地编写代码,而不必担心并发性和类型安全,因为这些担心是在后台处理的。 770 | 771 | ## 小结 772 | 773 | 大约每2000人中就有一人患有[通感症](https://baike.baidu.com/item/%E9%80%9A%E6%84%9F%E7%97%87/1079189),比如说他们在吃好吃的东西的时候,会听到美妙的音乐,听到美妙的音乐会闻到花的香气,绿色可能在他们感觉里是可爱的小熊,难过的情绪对他们来说可能是白色的绵羊。然而,还有一种更为罕见的形式,即句子和段落与品味和感受相关联。 774 | 775 | 对这些人来说,他们不会逐字逐句地解释。他们查看整个页面/文档/程序,了解它的味道不是在嘴里而是在头脑中。然后他们把文本的各个部分像拼图一样拼在一起。 776 | 777 | 这就是编写完全声明性代码的方式:描述值之间关系的代码,这些值告诉机器我们希望它计算什么。 程序的各个部分不是按行顺序排列的指令。 通感学也许能够自然地做到这一点,但是只要稍加练习,任何人都可以学习如何将关系性拼图拼凑在一起。 778 | 779 | 在本章中,我们研究了适用于函数式编程的几个数学概念,以及它们如何使我们在数据之间建立关系。 接下来,我们将探讨JavaScript中的递归和其他高级主题。 -------------------------------------------------------------------------------- /docs/book/chapter-first.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 几十年来,函数式编程一直是计算机科学爱好者的推崇,因其数学上的纯洁性和令人费解等特性而倍受推崇,正因为这些特性使函数式编程一直隐藏在数据科学家和博士生计算机实验室中。但现在,它正在经历一次复苏,感谢现代语言,如Python、Julia、Ruby、Clojure和JavaScipt。 4 | 5 | > 您说的是JavaScript吗? web脚本语言?(答案是)是! 6 | 7 | 事实证明,JavaScript是一项重要的编程技术,并且很长时间内不会消失。 这在很大程度上是由于它具有旺盛的扩展新框架和库的能力,例如backbone.js,jQuery,Dojo,underscore.js等。 这与JavaScript作为功能性编程语言的特性相关。 往长期去看,对于各种语言技能的程序员来说,使用JavaScript进行函数式编程对提高编程能力是非常有好处的。 8 | 9 | 为什么? 函数式编程功能强大,健壮且优雅。 它在大型数据结构上非常有用且高效。 使用JavaScript作为客户端语言,去操作DOM,对API响应进行排序或在复杂的网站上执行其他任务或实现功能,实现起来容易。 10 | 11 | 在本书中,您将学到使用JavaScript编程的相关知识: 12 | 13 | - 如何使用函数式编程,如何解锁JavaScript的编程能力。 14 | - 如何编写更好的代码,功能健壮且易于维护,下载速度更快,内存开销少。 15 | - 学习函数式编程的核心概念,如何将特性应用于JavaScript。 16 | - 如何避开使用JavaScript作为函数语言时可能出现的警告和问题。 17 | - 如何在JavaScript开发中使用函数式编程和面向对象编程思想。 18 | 19 | 但在开始之前,我们先做个实验。 20 | 21 | ## 案例 22 | 23 | 案例可能是介绍特性的最佳方法,基于JavaScript编程,比较传统的编程方法和使用函数式编程实现相同功能。 24 | 25 | ## 应用 26 | 27 | 应用是一个通过运算咖啡种类和大小(杯)不同,产生不同价格的示例。 28 | 29 | ## 实现方法 30 | 31 | 首先,我们看下整个过程。 32 | 33 | ```js 34 | // 创建对象存储数据. 35 | var columbian = { 36 | name: "columbian", 37 | basePrice: 5 38 | }; 39 | var frenchRoast = { 40 | name: "french roast", 41 | basePrice: 8 42 | }; 43 | var decaf = { 44 | name: "decaf", 45 | basePrice: 6 46 | }; 47 | // 实现一个计算器方法计算成本 48 | // 根据大小显示在页面list上 49 | function printPrice(coffee, size) { 50 | if (size == "small") { 51 | var price = coffee.basePrice + 2; 52 | } else if (size == "medium") { 53 | var price = coffee.basePrice + 4; 54 | } else { 55 | var price = coffee.basePrice + 6; 56 | } 57 | // create the new html list item 58 | var node = document.createElement("li"); 59 | var label = coffee.name + " " + size; 60 | var textnode = document.createTextNode(label + " price: $" + price); 61 | node.appendChild(textnode); 62 | document.getElementById("products").appendChild(node); 63 | } 64 | // now all we need to do is call the printPrice function 65 | // for every single combination of coffee type and size 66 | printPrice(columbian, "small"); 67 | printPrice(columbian, "medium"); 68 | printPrice(columbian, "large"); 69 | printPrice(frenchRoast, "small"); 70 | printPrice(frenchRoast, "medium"); 71 | printPrice(frenchRoast, "large"); 72 | printPrice(decaf, "small"); 73 | printPrice(decaf, "medium"); 74 | printPrice(decaf, "large"); 75 | 76 | // 输出 => 77 | columbian small price: $7 78 | columbian medium price: $9 79 | columbian large price: $11 80 | french roast small price: $10 81 | french roast medium price: $12 82 | french roast large price: $14 83 | decaf small price: $8 84 | decaf medium price: $10 85 | decaf large price: $12 86 | ``` 87 | 88 | 如上所见,此代码非常基础。 如果有比我们这里的三种咖啡更多的咖啡种类,该怎么办? 如果有20、50个怎么办, 如果除了大小(杯)之外,加上有机和非有区别,那该怎么办? 代码按照这样写下去,代码行会越来越多!使用以上方法,我们告诉咖啡机每种咖啡类型和尺寸的打印内容。 从根本上讲,这需要函数式编程解决问题所在。 89 | 90 | ## 函数式编程 91 | 92 | 功能编程(命令式代码)一步一步地告诉机器它需要做什么来解决问题,而函数式编程则试图用数学方法来描述问题,以便机器可以完成其余的工作。 93 | 94 | 使用更实用的方法,可以按如下方式编写相同的方法: 95 | 96 | ```js 97 | // separate the data and logic from the interface 98 | var printPrice = function(price, label) { 99 | var node = document.createElement("li"); 100 | var textnode = document.createTextNode(label + " price: $" + price); 101 | node.appendChild(textnode); 102 | document.getElementById("products 2").appendChild(node); 103 | }; 104 | // create function objects for each type of coffee 105 | var columbian = function() { 106 | this.name = "columbian"; 107 | this.basePrice = 5; 108 | }; 109 | var frenchRoast = function() { 110 | this.name = "french roast"; 111 | this.basePrice = 8; 112 | }; 113 | var decaf = function() { 114 | this.name = "decaf"; 115 | this.basePrice = 6; 116 | }; 117 | // create object literals for the different sizes 118 | var small = { 119 | getPrice: function() { 120 | return this.basePrice + 2; 121 | }, 122 | getLabel: function() { 123 | return this.name + " small"; 124 | } 125 | }; 126 | var medium = { 127 | getPrice: function() { 128 | return this.basePrice + 4; 129 | }, 130 | getLabel: function() { 131 | return this.name + " medium"; 132 | } 133 | }; 134 | var large = { 135 | getPrice: function() { 136 | return this.basePrice + 6; 137 | }, 138 | getLabel: function() { 139 | return this.name + " large"; 140 | } 141 | }; 142 | // put all the coffee types and sizes into arrays 143 | var coffeeTypes = [columbian, frenchRoast, decaf]; 144 | var coffeeSizes = [small, medium, large]; 145 | // build new objects that are combinations of the above 146 | // and put them into a new array 147 | var coffees = coffeeTypes.reduce(function(previous, current) { 148 | var newCoffee = coffeeSizes.map(function(mixin) { 149 | // `plusmix` function for functional mixins, see Ch.7 150 | var newCoffeeObj = plusMixin(current, mixin); 151 | return new newCoffeeObj(); 152 | }); 153 | return previous.concat(newCoffee); 154 | }, []); 155 | // we've now defined how to get the price and label for each 156 | // coffee type and size combination, now we can just print them 157 | coffees.forEach(function(coffee) { 158 | printPrice(coffee.getPrice(), coffee.getLabel()); 159 | }); 160 | ``` 161 | 162 | 首先应该清楚的是,它更模块化。这使得添加新大小(杯)或新咖啡类型变得简单,如下面的代码片段所示: 163 | 164 | ```js 165 | var peruvian = function() { 166 | this.name = "peruvian"; 167 | this.basePrice = 11; 168 | }; 169 | var extraLarge = { 170 | getPrice: function() { 171 | return this.basePrice + 10; 172 | }, 173 | getLabel: function() { 174 | return this.name + " extra large"; 175 | } 176 | }; 177 | coffeeTypes.push(Peruvian); 178 | coffeeSizes.push(extraLarge); 179 | ``` 180 | 181 | 咖啡对象数组和大小(杯)对象数组通过一个名为plusMixin的自定义函数“mixed”在一起,即它们的方法和成员变量组合在一起(请参见第7章,JavaScript中的面向函数和面向对象的编程)。 182 | 183 | 咖啡类型类包含成员变量,尺寸包含用于计算名称和价格的方法。 “mixing”发生在映射操作中,该操作将纯函数应用于数组中的每个元素,并在reduce()操作内部返回一个新函数,这是另一个类似于映射函数的高阶函数,只是其中的所有元素,数组合并为一个。 184 | 185 | 最后,使用forEach()方法迭代所有类型和大小的所有可能组合的新数组。forEach()方法是另一个高阶函数,它将回调函数应用于数组中的每个对象。 在上面示例中,我们将其作为匿名函数,该函数实例化对象并使用对象的getPrice()和getLabel()方法作为参数来调用printPrice()函数。 186 | 187 | 另外,通过删除coffee变量并将函数连接在一起,我们可以使这个示例更加实用,这是函数式编程中的另一个小技巧。 188 | 189 | ```js 190 | coffeeTypes 191 | .reduce(function(previous, current) { 192 | var newCoffee = coffeeSizes.map(function(mixin) { 193 | // `plusMixin` function for functional mixins, see Ch.7 194 | var newCoffeeObj = plusMixin(current, mixin); 195 | return new newCoffeeObj(); 196 | }); 197 | return previous.concat(newCoffee); 198 | }, []) 199 | .forEach(function(coffee) { 200 | printPrice(coffee.getPrice(), coffee.getLabel()); 201 | }); 202 | ``` 203 | 204 | 另外,函数式编程控制流并不像命令式代码那样自上而下。在函数式编程中,map()函数和其他高阶函数代替for和while循环,对执行顺序的关注程度很低。这使得新进入范例的人阅读代码变得有点棘手,但是,一旦你掌握了它的窍门,就不难理解了,你会发现它要好得多。 205 | 206 | 这个案例几乎没有涉及到函数式编程在JavaScript中能做什么,在这本书中,你将看到更强大的函数式编程方法的案例。 207 | 208 | ## 小结 209 | 210 | 首先,函数式编程的好处显而易见(函数一等公民)。 211 | 212 | 其次,不要惧怕函数式编程。 虽然它通常被视为人们通常认为纯逻辑的计算机语言形式,但是我们不需要了解λ演算(Lambda calculus)就可以将它应用于日常工作中。 实际上它的实现过程是我们将这些逻辑拆分为较小的部分,这样更易于理解,更易于维护且更可靠。 map()和reduce()函数是JavaScript中的内置函数,本书将进行详解。 213 | 214 | JavaScript是一种脚本语言,具有交互性且易于使用,并且无需编译。 我们甚至不需要下载任何开发软件,浏览器就可以充当解释器和开发环境。 215 | 216 | 大家有兴趣吗,让我们开始学习吧! 217 | -------------------------------------------------------------------------------- /docs/book/chapter-fourth.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 大家现在要集中精力了,因为现在真的要进入实用的编程思维模式了。 4 | 5 | 在本章中,我们将执行以下操作: 6 | 7 | - 把所有的核心概念整合成一个连贯的范例 8 | - 当我们完全致力于这种风格时,探索函数式编程所能提供的好处 9 | - 在函数式编程模式相互构建的过程中逐步编写它们的逻辑过程 10 | - 我们将构建一个简单的程序,完成一些非常酷的功能 11 | 12 | 上一章中提出了一些函数式编程概念,但在第 2 章“函数编程的基本原理”中没有提到。那是有原因的!Compositions, currying, partial application,等等。让我们来学习这些库为什么以及如何实现这些概念。 13 | 14 | 函数式编程可以有多种风格和模式。本章将介绍许多不同风格的函数式编程: 15 | 16 | - 数据通用编程 17 | - 一般函数式编程 18 | - 响应式函数式编程 19 | 20 | 这一章将尽可能的保持编程风格平和。在不过分依赖一种风格的函数式编程的情况下,总目标是寻找有更好的方法来编写代码,而不是采用唯一的方法。如果你以多种方式来编写代码的先入为主的观念解放编程思想,你就可以做任何你想做的事情。反之,如果你不遵循规律,随意写代码,可能会产生很多问题。 21 | 22 | ## 函数局部应用和柯里化 23 | 24 | 许多语言支持可选参数,但在 JavaScript 中不支持。JavaScript 使用完全不同的模式,允许向函数传递任意数量的参数。这就为一些非常有趣和不同寻常的设计模式打开了大门。函数可以部分或全部应用。 25 | 26 | JavaScript 中的部分应用程序是将值绑定到函数的一个或多个参数的过程,该函数返回另一个接受其余未绑定参数的函数。类似地,curring 是将具有多个参数的函数转换为具有一个参数的函数的过程,该参数返回另一个根据需要接受更多参数的函数。 27 | 28 | 两者之间的区别现在可能还不清楚,但最终会差异很大。 29 | 30 | ## 函数操作 31 | 32 | 实际上,在我们进一步解释如何实现偏函数应用和柯里化之前,我们需要做一个系统回顾。我们要探索 JavaScriptC 语言,暴露它的功能底层,我们需要了解 JavaScript 中的原函数、函数和原型是如何工作的;如果我们只是想设置一些 cookie 或验证一些表单字段,我们就不需要考虑这些。 33 | 34 | ### apply、call 和 this 关键字 35 | 36 | 在纯函数式语言中,函数不被调用;而是被应用。JavaScript 也以同样的方式工作,甚至提供了手动调用和应用函数的实用程序。这都是关于`this`关键字的,当然也是函数所属的对象。 37 | 38 | 使用 call()函数可以将 this 关键字定义为第一个参数。 其工作方式如下: 39 | 40 | ```js 41 | console.log(["Hello", "world"].join(" ")); // normal way 42 | console.log(Array.prototype.join.call(["Hello", "world"], " ")); // using call 43 | ``` 44 | 45 | 可以使用 call()函数来调用匿名函数: 46 | 47 | ```js 48 | console.log( 49 | function() { 50 | console.log(this.length); 51 | }.call([1, 2, 3]) 52 | ); 53 | ``` 54 | 55 | apply()函数与 call()函数非常相似,但更有用: 56 | 57 | ```js 58 | console.log(Math.max(1, 2, 3)); // returns 3 59 | console.log(Math.max([1, 2, 3])); // won't work for arrays though 60 | console.log(Math.max.apply(null, [1, 2, 3])); // but this will work 61 | ``` 62 | 63 | 根本的区别是,尽管 call()函数接受参数列表,而 apply()函数接受参数数组。 64 | 65 | call()和 apply()函数允许你编写一次函数,然后在其他对象中继承它,而无需再次编写函数。它们都是 Function 参数的成员。 66 | 67 | > 当你自己使用 call()函数时,可能会发生一些非常有意思的事情: 68 | 69 | ```js 70 | // 两者相等 71 | func.call(thisValue); 72 | Function.prototype.call.call(func,thisValue); 73 | ``` 74 | 75 | ## 绑定参数 76 | 77 | bind()函数的作用是:将一个方法应用于一个对象,并将这个 this 关键字分配给另一个对象。在内部,它与 call()函数相同,但它链接到该方法并返回一个新的有界函数。 78 | 79 | 它对于回调特别有用,如下代码片段所示: 80 | 81 | ```js 82 | function Drum() { 83 | this.noise = "boom"; 84 | this.duration = 1000; 85 | this.goBoom = function() { 86 | console.log(this.noise); 87 | }; 88 | } 89 | var drum = new Drum(); 90 | setInterval(drum.goBoom.bind(drum), drum.duration); 91 | ``` 92 | 93 | 这解决了诸如 Dojo.js 之类的面向对象框架中的许多问题,特别是在使用定义自己的处理函数的类时维护状态的问题。 但是我们也可以将 bind()函数用于函数式编程。 94 | 95 | ## 函数工厂 96 | 97 | 还记得第二章“函数式编程基础知识”中有关闭包的部分吗? 闭包是使可以创建有用的 JavaScript 编程模式(称为函数工厂)的构造。 它们允许我们手动将参数绑定到函数。 98 | 99 | 首先,我们需要一个将参数绑定到另一个函数的函数: 100 | 101 | ```js 102 | function bindFirstArg(func, a) { 103 | return function(b) { 104 | return func(a, b); 105 | }; 106 | } 107 | ``` 108 | 109 | 然后,我们可以使用它来创建更多通用函数: 110 | 111 | ```js 112 | var powersOfTwo = bindFirstArg(Math.pow, 2); 113 | console.log(powersOfTwo(3)); // 8 114 | console.log(powersOfTwo(5)); // 32 115 | ``` 116 | 117 | 它也可以处理其他参数: 118 | 119 | ```js 120 | function bindSecondArg(func, b) { 121 | return function(a) { 122 | return func(a, b); 123 | }; 124 | } 125 | var squareOf = bindSecondArg(Math.pow, 2); 126 | var cubeOf = bindSecondArg(Math.pow, 3); 127 | console.log(squareOf(3)); // 9 128 | console.log(squareOf(4)); // 16 129 | console.log(cubeOf(3)); // 27 130 | console.log(cubeOf(4)); // 64 131 | ``` 132 | 133 | 创建泛型函数的功能在函数编程中非常重要。 但是,有一个巧妙的技巧可以使此过程更加通用。 bindFirstArg()函数本身带有两个参数,第一个是函数。 如果我们将 bindFirstArg 函数作为函数传递给自身,则可以创建可绑定函数。 可以通过以下示例对此进行最好的描述: 134 | 135 | ```js 136 | var makePowersOf = bindFirstArg(bindFirstArg, Math.pow); 137 | var powersOfThree = makePowersOf(3); 138 | console.log(powersOfThree(2)); // 9 139 | console.log(powersOfThree(3)); // 27 140 | ``` 141 | 142 | 这就是为什么它们被称为函数工厂的原因。 143 | 144 | ## 局部应用 145 | 146 | 需要注意的是,我们的函数工厂示例的 bindFirstArg()和 bindSecondArg()函数只适用于只有两个参数的函数。我们可以为不同数量的论据编写新的论据,但这会偏离我们的概括。我们需要的是局部应用。 147 | 148 | 在哪里需要局部应用? 149 | 150 | 与 bind()函数和 Function 对象的其他内置方法不同,我们必须为局部应用和 currying 创建自己的函数。 有两种不同的方法可以做到这一点。 151 | 152 | ```js 153 | var partial = function(func){... // As a stand-alone function 154 | Function.prototype.partial = function(){... // As a polyfill 155 | ``` 156 | 157 | `Polyfill`用于增加具有新功能的原型,并允许我们将新功能称为要部分应用的功能的方法。 就像这样:`myfunction.partial(arg1,arg2,…);` 158 | 159 | ### 从左侧局部应用 160 | 161 | 这是 JavaScript 的 apply()和 call()实用程序对我们有用的地方。 让我们看一下 Function 对象的可能的 polyfill: 162 | 163 | ```js 164 | Function.prototype.partialApply = function() { 165 | var func = this; 166 | args = Array.prototype.slice.call(arguments); 167 | return function() { 168 | return func.apply(this, args.concat(Array.prototype.slice.call(arguments))); 169 | }; 170 | }; 171 | ``` 172 | 173 | 如您所见,它通过对参数特殊变量进行切片而起作用。 174 | 175 | 让我们看看在示例中使用它时会发生什么。 这次,让我们远离数学,去做一些有用的事情。 我们将创建一个小的应用程序,将数字转换为十六进制值。 176 | 177 | ```js 178 | function nums2hex() { 179 | function componentToHex(component) { 180 | var hex = component.toString(16); 181 | // make sure the return value is 2 digits, i.e. 0c or 12 182 | if (hex.length == 1) { 183 | return "0" + hex; 184 | } else { 185 | return hex; 186 | } 187 | } 188 | return Array.prototype.map.call(arguments, componentToHex).join(""); 189 | } 190 | // the function works on any number of inputs 191 | console.log(nums2hex()); // '' 192 | console.log(nums2hex(100, 200)); // '64c8' 193 | 194 | console.log(nums2hex(100, 200, 255, 0, 123)); // '64c8ff007b' 195 | // but we can use the partial function to partially apply 196 | // arguments, such as the OUI of a mac address 197 | var myOUI = 123; 198 | var getMacAddress = nums2hex.partialApply(myOUI); 199 | console.log(getMacAddress()); // '7b' 200 | console.log(getMacAddress(100, 200, 2, 123, 66, 0, 1)); 201 | // '7b64c8027b420001' 202 | // or we can convert rgb values of red only to hexadecimal 203 | var shadesOfRed = nums2hex.partialApply(255); 204 | console.log(shadesOfRed(123, 0)); // 'ff7b00' 205 | console.log(shadesOfRed(100, 200)); // 'ff64c8' 206 | ``` 207 | 208 | 这个例子表明,我们可以部分地将参数应用于泛型函数,并得到一个新的函数。第一个例子是从左到右,这意味着我们只能部分地应用第一个参数(最左边的参数)。 209 | 210 | ### 从右侧局部应用 211 | 212 | 为了从右边应用参数,我们可以定义另一个 polyfill。 213 | 214 | ```js 215 | Function.prototype.partialApplyRight = function() { 216 | var func = this; 217 | args = Array.prototype.slice.call(arguments); 218 | return function() { 219 | return func.apply(this, [].slice.call(arguments, 0).concat(args)); 220 | }; 221 | }; 222 | var shadesOfBlue = nums2hex.partialApplyRight(255); 223 | console.log(shadesOfBlue(123, 0)); // '7b00ff' 224 | console.log(shadesOfBlue(100, 200)); // '64c8ff' 225 | var someShadesOfGreen = nums2hex.partialApplyRight(255, 0); 226 | console.log(shadesOfGreen(123)); // '7bff00' 227 | console.log(shadesOfGreen(100)); // '64ff00' 228 | ``` 229 | 230 | 部分应用程序使我们可以采用非常通用的功能,并从中提取更多特定的功能。 但是此方法的最大缺陷是参数传递的方式(以多少和顺序排列)可能是模棱两可的。 在编程中,模棱两可绝不是一件好事。 有一种更好的方法可以做到这一点:柯里化。 231 | 232 | ## 柯里化 233 | 234 | 柯里化是将具有多个参数的函数转换为具有一个参数的函数的过程,该函数返回另一个需要根据需要使用更多参数的函数。 形式上,具有 N 个参数的函数可以转换为 N 个函数的函数链,每个函数只有一个参数。 235 | 236 | 一个常见的问题是:局部应用和柯里化有什么区别? 的确,局部应用立即返回了一个值,而柯里化仅返回另一个接受下一个参数的柯里化函数,但根本的区别在于柯里可以更好地控制如何将参数传递给函数。 我们将看到这是怎么回事,但是首先我们需要创建函数来执行该计算。 237 | 238 | 这是我们为函数原型添加 curring 的 polyfill: 239 | 240 | ```js 241 | Function.prototype.curry = function(numArgs) { 242 | var func = this; 243 | numArgs = numArgs || func.length; 244 | // recursively acquire the arguments 245 | function subCurry(prev) { 246 | return function(arg) { 247 | var args = prev.concat(arg); 248 | if (args.length < numArgs) { 249 | // recursive case: we still need more args 250 | return subCurry(args); 251 | } else { 252 | // base case: apply the function 253 | return func.apply(this, args); 254 | } 255 | }; 256 | } 257 | return subCurry([]); 258 | }; 259 | ``` 260 | 261 | numArgs 参数使我们可以选择指定未明确定义的函数所需要的参数数量。 262 | 263 | 让我们看看如何在十六进制转化方法中使用它。 编写一个将 RGB 值转换为适合 HTML 的十六进制字符串的函数: 264 | 265 | ```js 266 | function rgb2hex(r, g, b) { 267 | // nums2hex is previously defined in this chapter 268 | return "#" + nums2hex(r) + nums2hex(g) + nums2hex(b); 269 | } 270 | var hexColors = rgb2hex.curry(); 271 | console.log(hexColors(11)); // returns a curried function 272 | console.log(hexColors(11, 12, 123)); // returns a curried function 273 | console.log(hexColors(11)(12)(123)); // returns #0b0c7b 274 | console.log(hexColors(210)(12)(0)); // returns #d20c00 275 | ``` 276 | 277 | 它将返回 curried 函数,直到传递了所有需要的参数为止。它们以与 curryed 函数定义的相同的顺序从左到右传递。 278 | 279 | 但是我们可以将其提高一个级别,并定义我们需要的更具体的功能,如下所示: 280 | 281 | ```js 282 | var reds = function(g, b) { 283 | return hexColors(255)(g)(b); 284 | }; 285 | var greens = function(r, b) { 286 | return hexColors(r)(255)(b); 287 | }; 288 | var blues = function(r, g) { 289 | return hexColors(r)(g)(255); 290 | }; 291 | console.log(reds(11, 12)); // returns #ff0b0c 292 | console.log(greens(11, 12)); // returns #0bff0c 293 | console.log(blues(11, 12)); // returns #0b0cff 294 | ``` 295 | 296 | 因此,这是使用 currying 的好方法。 但是,如果我们只想直接使用 nums2hex()函数,则会遇到一些麻烦。 因为该函数没有定义任何参数,它只是需要传递尽可能多的参数。 我们必须定义参数的数量。 我们使用 curry 函数的可选参数,该参数允许我们设置正在 curry 函数的参数数量。 297 | 298 | ```js 299 | var hexs = nums2hex.curry(2); 300 | console.log(hexs(11)(12)); // returns 0b0c 301 | console.log(hexs(11)); // returns function 302 | console.log(hexs(110)(12)(0)); // incorrect 303 | ``` 304 | 305 | 因此,currying 不能与接受可变数量的参数的函数配合使用。 对于这种情况,首选部局部应用。这些不仅是为了函数工厂和代码重用。 柯里化和局部应用会发挥更大的作用,称为函数组合(Function composition)。 306 | 307 | ## 函数组合 308 | 309 | 最后,我们来到了函数组合。 310 | 311 | 在函数式编程中,我们希望一切都成为函数。 如果可能,我们特别希望一元函数。 如果我们可以将所有函数转换为一元函数,那么神奇的事情就会发生。 312 | 313 | `一元函数是仅接受单个输入的函数。 具有多个输入的函数是双元函数,但是对于接受两个输入的函数,我们通常说成二进制,对于三个输入的函数,我们通常说成三进制。 某些功能不接受特定数量的输入。 我们称这些为可变参数。` 314 | 315 | 本小节中,我们将探讨如何从较小的函数组成新的函数:将很小的逻辑单元组合成整个方法,这些逻辑单元大于单独的函数之和。 316 | 317 | ### 组合 318 | 319 | 组合函数使我们可以从许多简单的通用函数中构建复杂的函数。 通过将功能视为其他功能的构建块,我们可以构建具有出色可读性和可维护性的真正模块化应用程序。 320 | 321 | 在定义 compose() polyfill 之前,可以通过以下示例了解工作方式: 322 | 323 | ```js 324 | var roundedSqrt = Math.round.compose(Math.sqrt); 325 | console.log(roundedSqrt(5)); // Returns: 2 326 | 327 | var squaredDate = roundedSqrt.compose(Date.parse); 328 | console.log(squaredDate("January 1, 2014")); // Returns: 1178370 329 | ``` 330 | 331 | 在数学上,将 f 和 g 变量的组成定义为 f(g(x))。在 JavaScript 中,可以这样写: 332 | 333 | ```js 334 | var compose = function(f, g) { 335 | return function(x) { 336 | return f(g(x)); 337 | }; 338 | }; 339 | ``` 340 | 341 | 如果我们不这样做,除其他问题外,我们将无法找到此关键字。 解决方案是使用 apply()和 call()。 与 curry 相比,compose() polyfill 方式非常简单。 342 | 343 | ```js 344 | Function.prototype.compose = function(prevFunc) { 345 | var nextFunc = this; 346 | return function() { 347 | return nextFunc.call(this, prevFunc.apply(this, arguments)); 348 | }; 349 | }; 350 | ``` 351 | 352 | 为了展示其用法,让我们构建一个完整的示例,如下所示: 353 | 354 | ```js 355 | function function1(a) { 356 | return a + " 1"; 357 | } 358 | function function2(b) { 359 | return b + " 2"; 360 | } 361 | function function3(c) { 362 | return c + " 3"; 363 | } 364 | var composition = function3.compose(function2).compose(function1); 365 | console.log(composition("count")); // returns 'count 1 2 3' 366 | ``` 367 | 368 | 是否注意到首先应用了 function3 参数? 功能从右到左应用,这个非常重要。 369 | 370 | ### 反向组合 371 | 372 | 因为许多人喜欢从左到右阅读内容,所以按此顺序应用功能也很有意义。 我们称其为序列而不是合成。 373 | 374 | 要颠倒顺序,我们需要做的就是交换`nextFunc`和`prevFunc`参数。 375 | 376 | ```js 377 | Function.prototype.sequence = function(prevFunc) { 378 | var nextFunc = this; 379 | return function() { 380 | return prevFunc.call(this, nextFunc.apply(this, arguments)); 381 | }; 382 | }; 383 | ``` 384 | 385 | 这使我们现在可以更自然地调用函数。 386 | 387 | ```js 388 | var sequences = function1.sequence(function2).sequence(function3); 389 | console.log(sequences("count")); // returns 'count 1 2 3' 390 | ``` 391 | 392 | ### 组合与链式 393 | 394 | 这是同一 floorSqrt()函数组成的五个不同实现。 它们似乎是相同的,但值得仔细检查。 395 | 396 | ```js 397 | function floorSqrt1(num) { 398 | var sqrtNum = Math.sqrt(num); 399 | var floorSqrt = Math.floor(sqrtNum); 400 | var stringNum = String(floorSqrt); 401 | return stringNum; 402 | } 403 | 404 | function floorSqrt2(num) { 405 | return String(Math.floor(Math.sqrt(num))); 406 | } 407 | 408 | function floorSqrt3(num) { 409 | return [num] 410 | .map(Math.sqrt) 411 | .map(Math.floor) 412 | .toString(); 413 | } 414 | var floorSqrt4 = String.compose(Math.floor).compose(Math.sqrt); 415 | 416 | var floorSqrt5 = Math.sqrt.sequence(Math.floor).sequence(String); 417 | 418 | // all functions can be called like this: 419 | floorSqrt < N > 17; // Returns: 4 420 | ``` 421 | 422 | 但有几个关键的区别,我们应该回顾一下: 423 | 424 | - 显然,第一种方法冗长且效率低下。 425 | - 第二种方法是很好的一行程序,但是这种方法在只应用了几个函数之后就变得非常不可读了。 426 | 427 | `如果说代码越少越好,那就没有意义了。当有效的指令更简洁时,代码更易于维护。如果在不更改执行的有效指令的情况下减少代码字符数,则会导致完全相反的效果:代码变得更难理解,而且不易维护;例如,当我们使用嵌套的三元运算符或将多个命令连在一行上时。这些方法减少了代码量,但并没有减少该代码实际指定的步骤的数量。因此,这样做的结果是混淆并使代码更难理解。使代码更易于维护的一种简洁性是,它有效地减少了指定的指令(例如,通过使用更简单的算法,用更少和/或更简单的步骤来实现相同的结果),或者当我们简单地用消息替换代码时,例如,用有良好文档记录的API调用第三方库。` 428 | 429 | - 第三种方法是一系列数组函数,尤其是 map 函数。 这样写没问题,但在数学上不正确。 430 | - 这是我们正在使用的 compose()函数。所有方法都必须是一元的纯函数,这些函数鼓励使用更好,更简单和更小的函数来完成一件事并做好。 431 | - 最后一种方法反向组合使用 compose()函数,同样有效。 432 | 433 | ### 组合编程 434 | 435 | compose 最重要的是,除了应用的第一个函数外,它还与纯一元函数(仅接受一个参数的函数)一起使用效果最佳。 436 | 437 | 所应用的第一个功能的输出将发送到下一个功能。 这意味着该函数必须接受先前传递给它的函数。 这是类型签名背后的主要影响。 438 | 439 | `类型签名用于显式声明函数接受的输入类型和输出的类型。最初由Haskell使用,Haskell实际上在编译器要使用的函数定义中使用它们。但是,在JavaScript中,我们只是将它们放在代码注释中。它们看起来像这样:foo :: arg1 -> argN -> output` 440 | 441 | 示例: 442 | 443 | ```js 444 | // getStringLength :: String -> Int 445 | function getStringLength(s){return s.length}; 446 | // concatDates :: Date -> Date -> [Date] 447 | function concatDates(d1,d2){return [d1, d2]}; 448 | // pureFunc :: (int -> Bool) -> [int] -> [int] 449 | pureFunc(func, arr){return arr.filter(func)} 450 | ``` 451 | 452 | 为了真正获得 compose 的好处,任何应用程序都需要大量的一元、纯函数集合。这些是组成更大功能的构建块,反过来,这些功能又用于使应用程序非常模块化、可靠和可维护。 453 | 454 | 让我们举个例子。首先,我们需要许多构建块函数。其中一些建立在其他基础之上,如下所示: 455 | 456 | ```js 457 | // stringToArray :: String -> [Char] 458 | function stringToArray(s) { 459 | return s.split(""); 460 | } 461 | // arrayToString :: [Char] -> String 462 | function arrayToString(a) { 463 | return a.join(""); 464 | } 465 | // nextChar :: Char -> Char 466 | function nextChar(c) { 467 | return String.fromCharCode(c.charCodeAt(0) + 1); 468 | } 469 | // previousChar :: Char -> Char 470 | function previousChar(c) { 471 | return String.fromCharCode(c.charCodeAt(0) - 1); 472 | } 473 | // higherColorHex :: Char -> Char 474 | function higherColorHex(c) { 475 | return c >= "f" ? "f" : c == "9" ? "a" : nextChar(c); 476 | } 477 | // lowerColorHex :: Char -> Char 478 | function lowerColorHex(c) { 479 | return c <= "0" ? "0" : c == "a" ? "9" : previousChar(c); 480 | } 481 | // raiseColorHexes :: String -> String 482 | function raiseColorHexes(arr) { 483 | return arr.map(higherColorHex); 484 | } 485 | // lowerColorHexes :: String -> String 486 | function lowerColorHexes(arr) { 487 | return arr.map(lowerColorHex); 488 | } 489 | ``` 490 | 491 | 让我们把它们组合在一起。 492 | 493 | ```js 494 | var lighterColor = arrayToString 495 | .compose(raiseColorHexes) 496 | .compose(stringToArray); 497 | var darkerColor = arrayToString.compose(lowerColorHexes).compose(stringToArray); 498 | console.log(lighterColor("af0189")); // Returns: 'bf129a' 499 | console.log(darkerColor("af0189")); // Returns: '9e0078' 500 | ``` 501 | 502 | 我们甚至可以一起使用 compose()和 curry()函数。 实际上,他们在一起工作得很好。 让我们将 curry 示例与我们的 compose 示例结合在一起。 首先,我们需要以前的帮助程序功能。 503 | 504 | 我们甚至可以同时使用 compose()和 curry()函数。它们搭配使用效果很好。让我们将 curry 示例与我们的 compose 示例结合在一起。我们需要上上小结的函数方法。 505 | 506 | ```js 507 | // component2hex :: Ints -> Int 508 | function componentToHex(c) { 509 | var hex = c.toString(16); 510 | return hex.length == 1 ? "0" + hex : hex; 511 | } 512 | // nums2hex :: Ints* -> Int 513 | function nums2hex() { 514 | return Array.prototype.map.call(arguments, componentToHex).join(""); 515 | } 516 | ``` 517 | 518 | 我们创建柯里函数和部分应用的函数,然后将它们组合到其他组合函数中。 519 | 520 | ```js 521 | var lighterColors = lighterColor.compose(nums2hex.curry()); 522 | 523 | var darkerRed = darkerColor.compose(nums2hex.partialApply(255)); 524 | 525 | var lighterRgb2hex = lighterColor.compose(nums2hex.partialApply()); 526 | 527 | console.log(lighterColors(123, 0, 22)); // Returns: 8cff11 528 | console.log(darkerRed(123, 0)); // Returns: ee6a00 529 | console.log(lighterRgb2hex(123, 200, 100)); // Returns: 8cd975 530 | ``` 531 | 532 | 以上函数整体不错。我们从一件事的小功能开始。然后我们就可以把功能组合在一起了。 533 | 534 | 让我们看最后一个例子。 这是一个可将可变值的 RGB 值变亮的函数。 然后,我们可以使用组合从中创建新功能。 535 | 536 | ```js 537 | // lighterColorNumSteps :: string -> num -> string 538 | function lighterColorNumSteps(color, n) { 539 | for (var i = 0; i < n; i++) { 540 | color = lighterColor(color); 541 | } 542 | return color; 543 | } 544 | 545 | // now we can create functions like this: 546 | var lighterRedNumSteps = lighterColorNumSteps.curry().compose(reds)(0, 0); 547 | // and use them like this: 548 | console.log(lighterRedNumSteps(5)); // Return: 'ff5555' 549 | console.log(lighterRedNumSteps(2)); // Return: 'ff2222' 550 | ``` 551 | 552 | 同样,我们可以轻松创建更多功能,以创建更浅和更深的蓝色,绿色,灰色,紫色或任何想要的颜色。 这是构造 API 的绝佳方法。 553 | 554 | 我们只是勉强了解函数组合可以做什么。 compose 所做的是从 JavaScript 中夺走控制权。 通常,JavaScript 将从左到右求值,但是现在解释器会说“好吧,其他的事情会处理好的,我就转到下一个。”现在 compose()函数可以控制序列求值了! 555 | 556 | 这就是 Lazy.js,Bacon.js 和其他人能够实现诸如惰性求值和无限序列之类的方式的方式。 接下来,我们将研究如何使用这些库。 557 | 558 | ## 主要函数式编程 559 | 560 | 什么是没有副作用的程序?答案是:一个什么也不做的程序。 561 | 562 | 用具有不可避免的副作用的函数式代码来补充我们的代码可以称为“主要函数式编程”。在同一个代码库中使用多个范例并在它们最理想的地方应用它们是最好的方法。大多数情况下,函数式编程是建模纯传统函数式程序的方式::将大部分逻辑保持在纯函数中,并与命令式代码接口。 563 | 564 | 这就是我们要编写自己的:‘a little application’编程方式。 565 | 566 | 在这个例子中,我们有一个老板告诉我们,我们需要一个用于跟踪员工可用性状态的 web 应用程序。这家虚构公司的所有员工只有一个工作:使用我们的网站。员工上班时会签到,离开时会签退。但这还不够,它还需要随着内容的变化自动更新内容,因此我们的老板不必不断刷新页面。 567 | 568 | 我们将使用 Lazy.js 作为我们的功能库:我们不必假装要处理所有登录和注销的用户,WebSocket,数据库等等,而是假装有一个通用应用程序对象为我们完成此任务的完美 API。 569 | 570 | 所以现在,让我们把丑陋的部分,界面和创造副作用的部分去掉。 571 | 572 | ```js 573 | function Receptor(name, available) { 574 | this.name = name; 575 | this.available = available; // mutable state 576 | 577 | this.render = function() { 578 | output = "
  • "; 579 | output += this.available 580 | ? this.name + " is available" 581 | : this.name + " is not available"; 582 | output += "
  • "; 583 | return output; 584 | }; 585 | } 586 | var me = new Receptor(); 587 | var receptors = app.getReceptors().push(me); 588 | app.container.innerHTML = receptors 589 | .map(function(r) { 590 | return r.render(); 591 | }) 592 | .join(""); 593 | ``` 594 | 595 | 仅显示可用性列表就足够了,但我们希望它是反响应式的,这会给我们带来第一个难点。 596 | 597 | 通过使用 Lazy.js 库按顺序存储对象(在调用 toArray()方法之前它实际上不会计算任何东西),我们可以利用其惰性求值来提供一种功能性的响应式编程。 598 | 599 | ```js 600 | var lazyReceptors = Lazy(receptors).map(function(r) { 601 | return r.render(); 602 | }); 603 | app.container.innerHTML = lazyReceptors.toArray().join(""); 604 | ``` 605 | 606 | 因为 Receptor.render()方法返回新的 HTML 而不是修改当前 HTML,所以我们要做的就是将 innerHTML 参数设置为其输出。 607 | 608 | 用于用户管理的通用应用程序将提供回调方法供我们使用。 609 | 610 | ```js 611 | app.onUserLogin = function() { 612 | this.available = true; 613 | app.container.innerHTML = lazyReceptors.toArray().join(""); 614 | }; 615 | app.onUserLogout = function() { 616 | this.available = false; 617 | app.container.innerHTML = lazyReceptors.toArray().join(""); 618 | }; 619 | ``` 620 | 621 | 这样,用户每次登录或注销时,都会再次计算 lazyReceptors 参数,并且将使用最新值打印可用性列表。 622 | 623 | ## 事件处理 624 | 625 | 如果应用程序不提供用户登录和注销时的回调该怎么办? 回调很混乱,代码就变成了面条式代码。相反,我们可以通过直接观察用户来确定它。如果用户的网页处于焦点位置,则他/她必须处于活动状态且可用。我们可以使用 JavaScript 的 focus 和 blur 事件。 626 | 627 | ```js 628 | window.addEventListener("focus", function(event) { 629 | me.available = true; 630 | app.setReceptor(me.name, me.available); // just go with it 631 | container.innerHTML = lazyReceptors.toArray().join(""); 632 | }); 633 | window.addEventListener("blur", function(event) { 634 | me.available = false; 635 | app.setReceptor(me.name, me.available); 636 | container.innerHTML = lazyReceptors.toArray().join(""); 637 | }); 638 | ``` 639 | 640 | 等一下,事件不是也有响应式吗? 也可以惰性计算它们吗? 在 Lazy.js 库中,甚至有一个方便的方法。 641 | 642 | ```js 643 | var focusedReceptors = Lazy.events(window, "focus").each(function(e) { 644 | me.available = true; 645 | app.setReceptor(me.name, me.available); 646 | container.innerHTML = lazyReceptors.toArray().join(""); 647 | }); 648 | var blurredReceptors = Lazy.events(window, "blur").each(function(e) { 649 | me.available = false; 650 | app.setReceptor(me.name, me.available); 651 | container.innerHTML = lazyReceptors.toArray().join(""); 652 | }); 653 | ``` 654 | 655 | 以上非常简单。 656 | 657 | `通过使用Lazy.js库处理事件,我们可以创建无限个事件序列。 每次触发事件时,Lazy.each()函数都可以迭代一次。` 658 | 659 | 到目前为止,我们的老板很喜欢这个应用程序,但他指出,如果一名员工在离开前一天没有关闭页面就从不注销,那么该应用程序会记录该员工仍然工作。 660 | 661 | 为了确定某个员工是否在网站上处于活动状态,我们可以监视键盘和鼠标事件。假设他们被认为在 30 分钟没有活动之后就不可用了。 662 | 663 | ```js 664 | var timeout = null; 665 | var inputs = Lazy.events(window, "mousemove").each(function(e) { 666 | me.available = true; 667 | container.innerHTML = lazyReceptors.toArray().join(""); 668 | clearTimeout(timeout); 669 | timeout = setTimeout(function() { 670 | me.available = false; 671 | container.innerHTML = lazyReceptors.toArray().join(""); 672 | }, 1800000); // 30 minutes 673 | }); 674 | ``` 675 | 676 | Lazy.js 库使我们很容易将事件处理为可以映射的无限流。它之所以能够做到这一点,是因为它使用函数组合来控制执行顺序。 677 | 678 | 但这一切都有点问题。如果没有可以锁定的用户输入事件呢?如果有一个属性值一直在变化呢?在下一节中,我们将详细分析这个问题。 679 | 680 | ## 响应式函数编程 681 | 682 | 让我们来构建另一种几乎相同的应用程序。一种使用函数式编程对状态变化做出响应的程序。但是,这一次,应用程序将无法依赖事件侦听器。 683 | 684 | 想象一下,你在一家新闻媒体公司工作,老板让你开发一个 web 页面,这个应用可以跟踪选举日的政府选举结果。。当本地区域提交结果时,数据不断流入,因此要显示在页面上的结果非常活跃。但我们还需要按每个区域跟踪结果,因此将有多个对象要跟踪。 685 | 686 | 与其创建一个面向对象的层次结构来对接口建模,不如将其声明性地描述为不可变数据。我们可以用纯函数和半纯函数链来转换它,它们的唯一最终副作用是更新绝对必须保持的任何状态位(理想情况下,不是很多)。 687 | 688 | 我们将使用 Bacon.js 库,该库将能够快速开发响应式函数编程(FRP)应用程序。该应用程序仅在一年中的某一天(选举日)使用,而我们的老板认为应该花相应的时间。借助函数式编程和 Bacon.js 等之类的库,我们将在预期一半的时间完成它。 689 | 690 | 首先,我们需要一些对象来代表投票区域,比如州、省、区等等。 691 | 692 | ```js 693 | function Region(name, percent, parties) { 694 | // mutable properties: 695 | this.name = name; 696 | this.percent = percent; // % of precincts reported 697 | this.parties = parties; // political parties 698 | // return an HTML representation 699 | this.render = function() { 700 | var lis = this.parties.map(function(p) { 701 | return "
  • " + p.name + ": " + p.votes + "
  • "; 702 | }); 703 | var output = "

    " + this.name + "

    "; 704 | output += ""; 705 | output += "Percent reported: " + this.percent; 706 | return output; 707 | }; 708 | } 709 | function getRegions(data) { 710 | return JSON.parse(data).map(function(obj) { 711 | return new Region(obj.name, obj.percent, obj.parties); 712 | }); 713 | } 714 | var url = "http://api.server.com/election-data?format=json"; 715 | var data = jQuery.ajax(url); 716 | var regions = getRegions(data); 717 | app.container.innerHTML = regions 718 | .map(function(r) { 719 | return r.render(); 720 | }) 721 | .join(""); 722 | ``` 723 | 724 | 尽管以上内容仅显示静态的选举结果列表就足够了,但我们需要一种动态更新区域的方法。 Bacon.js 和响应式函数编程就派上用场了。 725 | 726 | ### 响应式 727 | 728 | Bacon.js 具有一个函数 Bacon.fromPoll(),该函数可让我们创建事件流,该事件只是在给定间隔上调用的函数和流。 subscription()函数使我们可以向流订阅一个处理函数。 因为是惰性的,所以没有订阅者,流实际上不会做任何事情。 729 | 730 | ```js 731 | var eventStream = Bacon.fromPoll(10000, function() { 732 | return Bacon.Next; 733 | }); 734 | var subscriber = eventStream.subscribe(function() { 735 | var url = "http://api.server.com/election-data?format=json"; 736 | var data = jQuery.ajax(url); 737 | var newRegions = getRegions(data); 738 | container.innerHTML = newRegions 739 | .map(function(r) { 740 | return r.render(); 741 | }) 742 | .join(""); 743 | }); 744 | ``` 745 | 746 | 通过基本上将其置于每 10 秒运行一次的循环中,我们可以完成工作。 但是这种方法会反复请求,效率极低。不是最佳的解决方案,我们应该更深入研究 Bacon.js 库。 747 | 748 | 在 Bacon 中,有`EventStreams`和`Properties`参数。属性可以被认为是变量,随着时间的推移会随着事件的变化而变化。因为它们仍然依赖一系列事件,属性相对于其 EventStream 随时间变化。 749 | 750 | Bacon.js 库还有另外一个技巧。 Bacon.fromPromise()函数是一种使用 Promise 将事件发送到流中的方法。 jQuery1.5.0 版开始,jQuery AJAX 实现了 promises 接口。 因此,我们需要做的就是编写一个 AJAX 搜索函数,该函数在异步调用完成时发出事件。 每当 Promise 被执行时,它都会调用 EvenStream 的订阅。 751 | 752 | ```js 753 | var url = "http://api.server.com/election-data?format=json"; 754 | var eventStream = Bacon.fromPromise(jQuery.ajax(url)); 755 | 756 | var subscriber = eventStream.onValue(function(data) { 757 | newRegions = getRegions(data); 758 | 759 | container.innerHTML = newRegions 760 | .map(function(r) { 761 | return r.render(); 762 | }) 763 | .join(""); 764 | }); 765 | ``` 766 | 767 | Promise 可以被视为最终的求值。 使用 Bacon.js 库,我们只需要关注等待最终值。 768 | 769 | ## 综述 770 | 771 | 既然我们已经讨论了响应式,我们终于可以实现一些方法了。 772 | 773 | 我们可以使用纯函数链来修改订阅,以完成计算和过滤不需要的结果等操作,并且可以在创建 onclick()时处理函数中为创建的按钮进行(和完成)所有操作。 774 | 775 | ```js 776 | // create the eventStream out side of the functions 777 | var eventStream = Bacon.onPromise(jQuery.ajax(url)); 778 | var subscribe = null; 779 | var url = "http://api.server.com/election-data?format=json"; 780 | // our un-modified subscriber 781 | $("button#showAll").click(function() { 782 | var subscriber = eventStream.onValue(function(data) { 783 | var newRegions = getRegions(data).map(function(r) { 784 | return new Region(r.name, r.percent, r.parties); 785 | }); 786 | container.innerHTML = newRegions 787 | .map(function(r) { 788 | return r.render(); 789 | }) 790 | .join(""); 791 | }); 792 | }); 793 | // a button for showing the total votes 794 | $("button#showTotal").click(function() { 795 | var subscriber = eventStream.onValue(function(data) { 796 | var emptyRegion = new Region("empty", 0, [ 797 | { 798 | name: "Republican", 799 | votes: 0 800 | }, 801 | { 802 | name: "Democrat", 803 | votes: 0 804 | } 805 | ]); 806 | var totalRegions = getRegions(data).reduce(function(r1, r2) { 807 | newParties = r1.parties.map(function(x, i) { 808 | return { 809 | name: r1.parties[i].name, 810 | votes: r1.parties[i].votes + r2.parties[i].votes 811 | }; 812 | }); 813 | newRegion = new Region( 814 | "Total", 815 | (r1.percent + r2.percent) / 2, 816 | newParties 817 | ); 818 | return newRegion; 819 | }, emptyRegion); 820 | container.innerHTML = totalRegions.render(); 821 | }); 822 | }); 823 | // a button for only displaying regions that are reporting > 50% 824 | $("button#showMostlyReported").click(function() { 825 | var subscriber = eventStream.onValue(function(data) { 826 | var newRegions = getRegions(data) 827 | .map(function(r) { 828 | if (r.percent > 50) return r; 829 | else return null; 830 | }) 831 | .filter(function(r) { 832 | return r != null; 833 | }); 834 | container.innerHTML = newRegions 835 | .map(function(r) { 836 | return r.render(); 837 | }) 838 | .join(""); 839 | }); 840 | }); 841 | ``` 842 | 843 | 这样做的好处是,当用户在几个按钮之间点击时,事件流不会更改,而订阅者会更改,这使所有操作都顺利进行。 844 | 845 | ## 小结 846 | 847 | JavaScript是一门优美的语言 848 | 849 | 它的内在美真正在函数式编程中大放异彩。它给JavaScript赋予了它卓越的可扩展性,它允许一级函数可以做很多事情,这样方法彼此叠加,功能越来越强大。 850 | 851 | 在本章中,我们首先学习JavaScript的函数范式。 我们介绍了函数工厂,柯里化,函数组成和函数正常运行所需。 我们构建了一个模块化方法。 然后,展示了如何使用一些功能库,这些函数库本身使用这些相同的概念,即函数组合,来控制执行顺序。 852 | 853 | 在本章中,我们讨论了函数式编程的几种类型:数据流范型编程,主要是函数式编程和函数响应式式编程。它们之间并没有什么不同,它们只是在不同的情况下应用函数式编程的不同模式。 854 | 855 | 在前一章中,我们简要地提到了理论范畴。在下一章中,我们将学习更多关于它是什么以及如何使用它的知识。 856 | -------------------------------------------------------------------------------- /docs/book/chapter-second.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 到目前为止,我们已经看到了函数式编程的一小部分功能。 4 | 5 | 但是什么是函数式编程呢?是什么让这一种语言具有这种能力,而不是另一种?是什么让这一种编程风格具有功能,而不是另一种?在本章中,我们将首先回答这些问题,然后介绍函数式编程的核心概念: 6 | 7 | - 使用函数和数组控制流 8 | - 编写纯函数、匿名函数、递归函数等 9 | - 像对象一样传递函数 10 | - 使用 map()、filter()和 reduce()函数 11 | 12 | ## 函数式编程语言 13 | 14 | 函数式编程是促进函数式编程范例的语言。冒着过度简化的风险,我们可以说,如果一种语言包括函数式编程所需的功能,那么它就是一种函数式语言。在大多数情况下,这是编程风格。 15 | 16 | ## 为什么语言有这样的特性 17 | 18 | 功能编程不能在像 C 和 Java 语言中执行,因为这些语言不包含支持它的构造方法。它们是纯粹面向对象的非函数式语言。其次,面向对象的编程不能在纯函数式语言上执行(如 Scheme、Haskell 和 Lisp)。但某些语言也支持这两种模型。 Python 是一个著名的例子,但还有其他例子:Ruby,Julia 和我们感兴趣的 JavaScript。 这些语言如何支持两种截然不同的设计模式? 它们包含两种编程范例所需的功能。 但对于 JavaScript,功能特性值得我们发掘。 19 | 20 | 但实际上,它涉及的更多。 那么什么使语言具有这样的特性? 21 | 22 | | 特性 | 命令 | 功能性 | 23 | | :------: | :------------------------: | :--------------------------------------------: | 24 | | 编程风格 | 执行分步任务并管理状态更改 | 定义问题是什么以及实现解决方案需要哪些数据转换 | 25 | | 状态更改 | 重要 | 不存在 | 26 | | 执行顺序 | 重要 | 不重要 | 27 | | 主程控制 | 循环、条件语句和函数调用 | 函数调用和递归 | 28 | | 操纵单元 | 结构和类对象 | 作为一级对象和数据集的函数 | 29 | 30 | 一种语言的语法必须允许某些设计模式,例如类型推导,以及使用匿名函数的能力。 例如该语言必须实现 λ 演算。 同样,解释器的评估策略应为非严格且按需调用(也称为延迟执行),这允许不可变的数据结构和非严格的惰性计算。 31 | 32 | ## 优势 33 | 34 | 可以这样说,学习和理解函数式编程所得的收获:不管你是否成为成为一名全职的函数式程序员,学习的经历都会使你在以后的项目开发中成为一个更好的程序员。 35 | 36 | 从形式上讲,使用函数式编程的实际优势是什么? 37 | 38 | ### 代码精简 39 | 40 | 函数式编程代码更简洁,更小。 简化了调试,测试和维护成本。 41 | 42 | 例如,假设我们实现一个将二维数组转换为一维数组的函数。 仅使用命令式编程风格,我们这样实现它: 43 | 44 | ```js 45 | function merge2dArrayIntoOne(arrays) { 46 | var count = arrays.length; 47 | var merged = new Array(count); 48 | var c = 0; 49 | for (var i = 0; i < count; ++i) { 50 | for (var j = 0, jlen = arrays[i].length; j < jlen; ++j) { 51 | merged[c++] = arrays[i][j]; 52 | } 53 | } 54 | return merged; 55 | } 56 | ``` 57 | 58 | 使用函数式编程技术,可以这样写: 59 | 60 | ```js 61 | var merge2dArrayIntoOne2 = function(arrays) { 62 | return arrays.reduce(function(p, n) { 63 | return p.concat(n); 64 | }); 65 | }; 66 | ``` 67 | 68 | 这两个函数都使用相同的输入并返回相同的输出。 但是,该下面示例更加简洁明了。 69 | 70 | ### 模块化 71 | 72 | 函数式编程的特点将大问题分解为要解决的同一问题的较小实例。 这意味着代码更加模块化。 明确规定了模块化程序,易于调试和维护。同时测试更加容易,因为可以对每个模块化代码进行正确性检查。 73 | 74 | ### 复用性 75 | 76 | 由于功能编程的模块化,功能程序共享各种常用的方法。 发现这些功能可以在各种不同的应用程序中重复使用。 77 | 78 | 后续章节将展示许多最常用的函数功能。作为函数式编程程序员时,不可避免地会开发自己的工具函数库(utils),这些函数可以反复使用。 例如,设计良好的函数方法可以搜索配置文件的各行,也可以用于搜索哈希表。 79 | 80 | ### 减少耦合 81 | 82 | 耦合是程序中模块之间的依赖量。由于函数式程序员致力于编写一级、高阶、纯函数,这些函数彼此完全独立,对全局变量没有副作用,因此耦合大大减少。当然,功能之间不可避免地会相互依赖。但修改一个函数不会改变另一个函数,只要输入到输出的一对一映射保持正确。 83 | 84 | ### 数学上正确 85 | 86 | 最后一个是从理论上讲的。 由于其起源于 Lambda 微积分,因此可以从数学上证明功能程序是正确的。 对于需要证明程序的增长率,时间复杂度和数学正确性的研究人员来说,这是一个很大的优势。 87 | 88 | 让我们看一下斐波那契数列。 尽管除了概念证明之外,它很少用于其他任何方面,但它很好地说明了这一概念。 评估斐波那契序列的标准方法是创建一个递归函数,该函数表示: 89 | 90 | `fibonnaci(n)= fibonnaci(n-2)+ fibonnaci(n-1`) 91 | 92 | 并在 n <2 时返回 1,这使得可能 停止递归并开始累加递归调用堆栈中每个步骤返回的值。 93 | 94 | 以下描述了计算序列所涉及的中间步骤。 95 | 96 | ```js 97 | var fibonacci = function(n) { 98 | if (n < 2) { 99 | return 1; 100 | } else { 101 | return fibonacci(n - 2) + fibonacci(n - 1); 102 | } 103 | }; 104 | console.log(fibonacci(8)); 105 | // Output: 34 106 | ``` 107 | 108 | 但是,借助实现惰性执行策略的库,可以生成不确定的序列,该序列说明定义整个数字序列的数学方程式。 仅计算所需数量的数字。 109 | 110 | ```js 111 | var fibonacci2 = Lazy.generate( 112 | (function() { 113 | var x = 1, 114 | y = 1; 115 | return function() { 116 | var prev = x; 117 | x = y; 118 | y += prev; 119 | return prev; 120 | }; 121 | })() 122 | ); 123 | 124 | console.log(fibonacci2.length()); // Output: undefined 125 | console.log(fibonacci2.take(12).toArray()); // Output: [1, 1, 2, 3, 5,8, 13, 21, 34, 55, 89, 144] 126 | var fibonacci3 = Lazy.generate( 127 | (function() { 128 | var x = 1, 129 | y = 1; 130 | return function() { 131 | var prev = x; 132 | x = y; 133 | y += prev; 134 | return prev; 135 | }; 136 | })() 137 | ); 138 | 139 | console.log( 140 | fibonacci3 141 | .take(9) 142 | .reverse() 143 | .first(1) 144 | .toArray() 145 | ); 146 | // Output: [34]; 147 | ``` 148 | 149 | 二个例子显然在数学上更合理。 它依靠惰性计算。 JavaScript 库。 还有其他库也可以在这里提供帮助,例如 Sloth.js 和 wu.js。 这些将在第 3 章,建立函数式编程环境中介绍。 150 | 151 | ### 非函数世界中的函数式编程 152 | 153 | 函数式编程和非函数式编程可以混合在一起吗? 这是第 7 章 JavaScript 的功能和面向对象编程的主题,但在此之前,首先要弄清楚一些事情很重要。 154 | 155 | 本书无意教你如何实现严格遵守纯函数式编程进行项目开发,此类应用很少在学术界以外适用。 这本书将教你如何在应用程序中使用函数式编程设计策略来补充必要的命令性代码。 156 | 157 | 例如,如果您需要仅包含某些文本中的字母的前四个单词,则可以这样简单地编写它们: 158 | 159 | ```js 160 | var words = [], 161 | count = 0; 162 | text = myString.split(" "); 163 | for (i = 0; count < 4, i < text.length; i++) { 164 | if (!text[i].match(/[0-9]/)) { 165 | words = words.concat(text[i]); 166 | count++; 167 | } 168 | } 169 | console.log(words); 170 | ``` 171 | 172 | 相比之下,函数式编程这样写: 173 | 174 | ```js 175 | var words = []; 176 | var words = myString 177 | .split(" ") 178 | .filter(function(x) { 179 | return !x.match(/[1-9]+/); 180 | }) 181 | .slice(0, 4); 182 | console.log(words); 183 | ``` 184 | 185 | 使用功能性编程实用程序库,可以进一步简化它们: 186 | 187 | ```js 188 | var words = toSequence(myString) 189 | .match(/[a-zA-Z]+/) 190 | .first(4); 191 | ``` 192 | 193 | 标识可以以更实用的方式编写的函数的关键是查找循环和临时变量,例如上例中的 words 和 count 实例。 194 | 195 | 我们可以通过使用高阶函数代替临时变量和循环来消除它们,我们将在本章稍后进行探讨。 196 | 197 | ## JavaScript 是函数式语言吗 198 | 199 | JavaScript 是函数语言还是非函数式语言? 200 | 201 | JavaScript 世界上最流行、最难理解的函数式编程语言。JavaScript 是一种类似 C 语言的函数式编程语言。不可否认,它的语法类似于 C,这意味着它使用 C 的块语法和中缀顺序(infix ordering)。它是现存的口碑最差语言之一。基本上很多人会把 JavaScript 与 Java 相混淆,实际上,它与 Java 几乎没有什么共同之处。 202 | 203 | 而且,为了真正巩固 JavaScript 是面向对象语言的思想,Dojo.js 和 ease.js 等库和框架一直在努力抽象 JavaScript 并使其适合于面向对象编程。JavaScript 诞生于 20 世纪 90 年代,当时 OOP 思想一时,有人告诉我们 JavaScript 是面向对象的,因为我们非常希望它是面向对象的。但事实并非如此。 204 | 205 | 它的真实身份与 Scheme 和 Lisp 更加一致,这是两种经典的功能语言。 JavaScript 一直是一种函数式语言。 它的功能是一流的,可以嵌套,具有闭包和组合,并且允许使用 curry 和 monads。 所有这些都是函数式编程的关键。 以下是 JavaScript 是函数式语言的一些其他原因: 206 | 207 | - JavaScript 的语法包括将函数传递为参数的能力,具有推断类型,并允许匿名函数、高阶函数、闭包等等。这些事实对于实现函数式编程的结构和行为至关重要。 208 | - 它不是一种纯面向对象的语言,大多数面向对象的设计模式是通过复制原型对象来实现的,这是面向对象编程的一个薄弱模型。European Computer Manufacturers 209 | Association Script(ECMAScript)是 JavaScript 的正式和标准实现规范,在规范 4.2.1 中规定了以下内容: 210 | 211 | `ECMAScript不包含适当的类,例如C++、SimalTalk或Java中的类,而是支持创建对象的构造函数。在基于类的面向对象语言中,一般情况下,状态由实例携带,方法由类携带,继承仅限于结构和行为。在ECMAScript中,状态和方法是由对象携带的,结构、行为和状态都是继承的。` 212 | 213 | - 它也是一种解释性语言。 JavaScript 解释器有时也称为“引擎”,通常与 Scheme 解释器非常相似。 两者都是动态的,都有灵活的数据类型,可以轻松地组合和转换,都将代码评估为表达式块,并且都以相似的方式对待函数。 214 | 215 | 尽管如此,JavaScript 确实不是一种纯函数式语言。缺少的是惰性计算和内置不可变数据。这是因为大多数解释器都是 call-by-name 而不是 call-by-need。由于 JavaScript 处理尾部调用的方式,它对递归也不是很好。然而,只要稍加改进,所有这些问题都可以得到解决。无限序列和延迟计算所需的非严格计算可以通过 lazy.js 的库实现。不可变数据可以简单地通过编程技术来实现,但这需要更多的程序员 polyfill,而不是依赖于语言特性来处理它。而递归的尾部调用消除可以通过一种叫做 Trampolining 的方法来实现。这些问题将在第 6 章,JavaScript 中的高级主题和陷阱中展开。 216 | 217 | 关于 JavaScript 是函数式语言还是面向对象语言,两者兼而有之,还是两者都不是,还未定论。 218 | 219 | 最后,函数式编程是通过巧妙的变异、组合和使用函数的方式来编写更简洁的代码的方法。 JavaScript 为这种方法提供了很好的媒介。 如果您确实想充分利用 JavaScript 的潜力,则必须学习如何将其用作函数式语言。 220 | 221 | ## 使用函数式功能 222 | 223 | 有时,优雅的实现是一个函数。不是方法。不是一门课。不是框架。只是一个功能。 224 | 225 | `-John Carmack, lead programmer of the Doom video game` 226 | 227 | 函数式编程就是将问题分解为一组函数。 通常,功能连在一起,相互嵌套,传递并被视为头等公民。 如果您使用了诸如 jQuery 和 Node.js 之类的框架,那么您可能已经使用了其中的一些技术,您只是没有意识到! 228 | 229 | 让我们从一些 JavaScript 难处入手。 230 | 231 | 假设我们需要编译一个分配给通用对象的值的列表,这些对象可以是任元素:dates,HTML object 等等。 232 | 233 | ```js 234 | var obj1 = { value: 1 }, 235 | obj2 = { value: 2 }, 236 | obj3 = { value: 3 }; 237 | 238 | var values = []; 239 | 240 | function accumulate(obj) { 241 | values.push(obj.value); 242 | } 243 | 244 | accumulate(obj1); 245 | accumulate(obj2); 246 | 247 | console.log(values); // Output: [obj1.value, obj2.value] 248 | ``` 249 | 250 | 以上代码能运行,但不稳定。任何代码都可以在不调用 accumulate()函数的情况下修改 values 数组。如果我们忘记把 values 设置为数组[],那么代码将完全不起作用。 251 | 252 | 但如果变量是在函数内部声明的,它就不能被任何意外问题行所改变。 253 | 254 | ```js 255 | function accumulate2(obj) { 256 | var values = []; 257 | values.push(obj.value); 258 | return values; 259 | } 260 | 261 | console.log(accumulate2(obj1)); // Returns: [obj1.value] 262 | console.log(accumulate2(obj2)); // Returns: [obj2.value] 263 | console.log(accumulate2(obj3)); // Returns: [obj3.value] 264 | ``` 265 | 266 | 代码木起作用!只返回上次传入的对象的值。我们可以在第一个函数中使用嵌套函数来解决这个问题。 267 | 268 | ```js 269 | var ValueAccumulator = function(obj) { 270 | var values = []; 271 | var accumulate = function() { 272 | values.push(obj.value); 273 | }; 274 | accumulate(); 275 | return values; 276 | }; 277 | ``` 278 | 279 | 以上代码是同一个问题,现在我们不能得到累加函数或 values 变量。 280 | 281 | 我们需要的是一个自调用函数(self-invoking function)。 282 | 283 | ## 自调用函数和闭包 284 | 285 | 如果我们可以返回一个反过来返回值数组的函数表达式呢?函数中声明的变量可用于函数中的任何代码,包括自调用函数。 286 | 287 | 通过使用自调用函数,我们的困境得到了解决。 288 | 289 | ```js 290 | var ValueAccumulator = function() { 291 | var values = []; 292 | var accumulate = function(obj) { 293 | if (obj) { 294 | values.push(obj.value); 295 | return values; 296 | } else { 297 | return values; 298 | } 299 | }; 300 | return accumulate; 301 | }; 302 | 303 | //This allows us to do this: 304 | var accumulator = ValueAccumulator(); 305 | accumulator(obj1); 306 | accumulator(obj2); 307 | 308 | console.log(accumulator()); 309 | // Output: [obj1.value, obj2.value] 310 | ``` 311 | 312 | 这都是关于变量作用域的。 即使范围之外的代码调用了这些函数,值变量也可用于内部的 accumulate()函数。闭包是所有功能语言的功能。 传统的命令式语言不允许这样做。 313 | 314 | ## 高阶函数 315 | 316 | 自调用函数实际上是高阶函数的一种形式。 高阶函数是将另一个函数作为输入或将一个函数返回作为输出的函数。 317 | 318 | 高阶函数在传统编程中并不常见。 命令式程序员可能会使用循环来迭代数组,而功能性程序员则会完全采用另一种方法。 通过使用高阶函数,可以通过将该函数应用于数组中的每个项目以创建新数组来处理该数组。 319 | 320 | 这是函数式编程范例的中心思想。 高阶函数允许的是将逻辑传递给其他函数的能力,就像对象一样。 321 | 322 | 在 JavaScript 中,函数被视为一等公民,这是 JavaScript 与 Scheme,Haskell 和其他经典函数语言的共同点。 这听起来可能很奇怪,但是这实际上意味着将功能像数字和对象一样被视为基元。 如果数字和对象可以传递,函数也可以传递。 323 | 324 | 为了了解这一点,让我们在上一节的 ValueAccumulator()函数中使用高阶函数: 325 | 326 | ```js 327 | // using forEach() to iterate through an array and call a 328 | // callback function, accumulator, for each item 329 | var accumulator2 = ValueAccumulator(); 330 | var objects = [obj1, obj2, obj3]; // could be huge array of objects 331 | objects.forEach(accumulator2); 332 | console.log(accumulator2()); 333 | ``` 334 | 335 | ## 纯函数 336 | 337 | 一个简单的例子是数学函数。 `Math.sqrt(4)`将始终返回 2,不使用任何隐藏信息(例如设置或状态),并且永远不会造成任何副作用。 338 | 339 | 纯函数是对“函数”的数学术语的真正解释,“函数”是输入与输出之间的关系。 它们考虑简单,易于重用。 由于它们是完全独立的,因此纯函数可以多次使用。 340 | 341 | 为了说明这一点,请将以下非纯函数与纯函数进行比较。 342 | 343 | ```js 344 | // function that prints a message to the center of the screen 345 | var printCenter = function(str) { 346 | var elem = document.createElement("div"); 347 | elem.textContent = str; 348 | elem.style.position = "absolute"; 349 | elem.style.top = window.innerHeight / 2 + "px"; 350 | elem.style.left = window.innerWidth / 2 + "px"; 351 | document.body.appendChild(elem); 352 | }; 353 | printCenter("hello world"); 354 | // pure function that accomplishes the same thing 355 | var printSomewhere = function(str, height, width) { 356 | var elem = document.createElement("div"); 357 | elem.textContent = str; 358 | elem.style.position = "absolute"; 359 | elem.style.top = height; 360 | elem.style.left = width; 361 | return elem; 362 | }; 363 | 364 | document.body.appendChild( 365 | printSomewhere( 366 | "hello world", 367 | window.innerHeight / 2 + 10 + "px", 368 | window.innerWidth / 2 + 10 + "px" 369 | ) 370 | ); 371 | ``` 372 | 373 | 虽然非纯函数依赖于窗口对象的状态来计算高度和宽度,但是纯自给自足的函数却要求传递这些值。这实际上是在允许将消息打印在任何地方, 这使功能更加通用。 374 | 375 | 虽然非纯函数似乎更容易选择,因为它执行附加自身而不是返回元素,而纯函数printSomewhere()及其返回值在其他函数式编程设计技术中的作用更好。 376 | 377 | ```js 378 | var messages = ["Hi", "Hello", "Sup", "Hey", "Hola"]; 379 | 380 | messages 381 | .map(function(s, i) { 382 | return printSomewhere(s, 100 * i * 10, 100 * i * 10); 383 | }) 384 | .forEach(function(element) { 385 | document.body.appendChild(element); 386 | }); 387 | ``` 388 | 389 | ## 匿名函数 390 | 391 | 将函数视为一级对象的另一个好处是匿名函数的出现,匿名函数是没有名称的函数。 但是它们不仅仅如此。 它们允许的是能够在现场和根据需要定义临时逻辑的能力。 通常,这是为了方便。 如果该函数仅被引用一次,则无需在其上浪费变量名称。 392 | 393 | 匿名函数的示例: 394 | 395 | ```js 396 | // The standard way to write anonymous functions 397 | function () { 398 | return "hello world"; 399 | } 400 | // Anonymous function assigned to variable 401 | var anon = function(x, y) { 402 | return x + y; 403 | }; 404 | // Anonymous function used in place of a named callback function, 405 | // this is one of the more common uses of anonymous functions. 406 | setInterval(function() { 407 | console.log(new Date().getTime()); 408 | }, 1000); 409 | 410 | // Output: 1413249010672, 1413249010673, 1413249010674, ... 411 | // Without wrapping it in an anonymous function, it immediately 412 | // execute once and then return undefined as the callback: 413 | setInterval(console.log(new Date().getTime()), 1000); 414 | // Output: 1413249010671 415 | 416 | ``` 417 | 418 | 高阶函数中使用的匿名函数的一个更复杂的示例: 419 | 420 | ```js 421 | function powersOf(x) { 422 | return function(y) { 423 | // this is an anonymous function! 424 | return Math.pow(x, y); 425 | }; 426 | } 427 | powerOfTwo = powersOf(2); 428 | console.log(powerOfTwo(1)); // 2 429 | console.log(powerOfTwo(2)); // 4 430 | console.log(powerOfTwo(3)); // 8 431 | powerOfThree = powersOf(3); 432 | console.log(powerOfThree(3)); // 9 433 | console.log(powerOfThree(10)); // 59049 434 | 435 | ``` 436 | 437 | 返回的函数不需要命名。 它不能在powersOf()函数之外的任何地方使用,因此它是一个匿名函数。上上一节的累加器功能可以使用匿名函数重写它。 438 | 439 | ```js 440 | var obj1 = { value: 1 }, 441 | obj2 = { value: 2 }, 442 | obj3 = { value: 3 }; 443 | 444 | var values = (function() { 445 | // anonymous function 446 | var values = []; 447 | return function(obj) { 448 | // another anonymous function! 449 | if (obj) { 450 | values.push(obj.value); 451 | return values; 452 | } else { 453 | return values; 454 | } 455 | }; 456 | })(); // make it self-executing 457 | 458 | console.log(values(obj1)); // Returns: [obj.value] 459 | console.log(values(obj2)); // Returns: [obj.value, obj2.value] 460 | ``` 461 | 462 | 不仅如此。 如结构`( function () {...}) ();`所示,它也是自执行的。 匿名函数后面的一对括号使该函数立即被调用。 在上面的示例中,将值实例分配给自执行函数调用的输出。 463 | 464 | 匿名功能的一个缺点仍然存在。 它们很难在调用堆栈中识别,这使调试更加棘手。 应该谨慎使用它们。 465 | 466 | ## 链式调运 467 | 468 | 在JavaScript中将方法链接在一起非常普遍。 如果您使用过jQuery,则可能已经执行了此技术。 有时称为“生成器模式”。这是一种用于简化代码的技术,其中将多个功能一个接一个地应用于一个对象。 469 | 470 | ```js 471 | // Instead of applying the functions one per line... 472 | arr = [1, 2, 3, 4]; 473 | arr1 = arr.reverse(); 474 | arr2 = arr1.concat([5, 6]); 475 | arr3 = arr2.map(Math.sqrt); 476 | 477 | // ...they can be chained together into a one-liner 478 | console.log( 479 | [1, 2, 3, 4] 480 | .reverse() 481 | .concat([5, 6]) 482 | .map(Math.sqrt) 483 | ); 484 | // parentheses may be used to illustrate 485 | console.log( 486 | [1, 2, 3, 4] 487 | .reverse() 488 | .concat([5, 6]) 489 | .map(Math.sqrt) 490 | ); 491 | ``` 492 | 493 | 这仅在函数是要处理的对象的方法时才有效。 例如,如果您创建了自己的函数,该函数需要两个数组并返回两个数组压缩在一起的数组,则必须将其声明为Array.prototype对象的成员。 看一下以下代码片段: 494 | 495 | ```js 496 | Array.prototype.zip = function(arr2) { 497 | // ... 498 | }; 499 | ``` 500 | 501 | 这将使我们能够: 502 | 503 | ```js 504 | arr.zip([11, 12, 13, 14]).map(function(n) { 505 | return n * 2; 506 | }); 507 | // Output: 2, 22, 4, 24, 6, 26, 8, 28 508 | ``` 509 | 510 | ## 递归 511 | 512 | 递归可能是最著名的函数式编程技术。 如果您现在还不知道,那么递归函数就是一个调用自身的函数。 513 | 514 | 当函数调用自身时,会发生一些奇怪的事情。 它既充当循环,多次执行同一代码,又充当函数堆栈。 515 | 516 | 递归函数必须非常小心,以避免无限循环(在这种情况下为无限递归)。 因此,就像循环一样,必须使用条件来知道何时停止。 这称为基本情况。 517 | 518 | 示例如下: 519 | 520 | ```js 521 | var foo = function(n) { 522 | if (n < 0) { 523 | // base case 524 | return "hello"; 525 | } else { 526 | // recursive case 527 | foo(n - 1); 528 | } 529 | }; 530 | console.log(foo(5)); 531 | ``` 532 | 533 | 可以将任何循环转换为递归算法,将任何递归算法转换为循环。但是递归算法更适合,几乎是必要的,对于那些与循环非常不同的情况。 534 | 535 | 一个非常好的例子是树遍历。 虽然使用递归函数遍历树并不难,但循环会复杂得多,并且需要维护堆栈。 这将与函数式编程的精神背道而驰。 536 | 537 | ```js 538 | var getLeafs = function(node) { 539 | if (node.childNodes.length == 0) { 540 | // base case 541 | return node.innerText; 542 | } else { 543 | // recursive case: 544 | return node.childNodes.map(getLeafs); 545 | } 546 | }; 547 | ``` 548 | 549 | ## 分治算法 550 | 551 | 在没有for和while循环的情况下,递归不仅仅是一种有趣的迭代方式。 一种称为分而治之的算法设计将问题递归分解为同一问题的较小实例,直到它们足够小以至于无法解决。 552 | 553 | 这方面的例子是欧几里得算法,用于寻找两个数的最大公分母。 554 | 555 | ```js 556 | function gcd(a, b) { 557 | if (b == 0) { 558 | // base case (conquer) 559 | return a; 560 | } else { 561 | // recursive case (divide) 562 | return gcd(b, a % b); 563 | } 564 | } 565 | console.log(gcd(12, 8)); 566 | console.log(gcd(100, 20)); 567 | ``` 568 | 569 | 因此,从理论上讲,分而治之非常有效,但是在现实世界中有什么用吗? 是! 用于对数组进行排序的JavaScript函数不是很好。 它不仅将数组排序到位,这意味着数据不是不可变的,而且也不可靠且不灵活。 通过分而治之,我们可以做得更好。 570 | 571 | 合并排序算法使用分而治之递归算法设计,通过将数组递归划分为较小的子数组,然后将它们合并在一起,从而有效地对数组进行排序。 572 | 573 | JavaScript的完整实现约为40行代码。 574 | 575 | 但是,伪代码如下: 576 | 577 | ```js 578 | var mergeSort = function(arr) { 579 | if (arr.length < 2) { 580 | // base case: 0 or 1 item arrays don't need sorting 581 | return items; 582 | } else { 583 | // recursive case: divide the array, sort, then merge 584 | var middle = Math.floor(arr.length / 2); 585 | // divide 586 | var left = mergeSort(arr.slice(0, middle)); 587 | var right = mergeSort(arr.slice(middle)); 588 | // conquer 589 | // merge is a helper function that returns a new array 590 | // of the two arrays merged together 591 | return merge(left, right); 592 | } 593 | }; 594 | ``` 595 | 596 | ## 惰性计算 597 | 598 | 惰性计算,也称为非严格计算,按需调用和延迟执行,是一种计算策略,它等待直到需要值才能计算函数的结果,这对函数编程特别有用。 显然,指出`x = func()`的代码行正在要求通过`func()`将 x 分配给返回值。 但是 x 实际等于什么并不重要,直到需要它为止。 等待调用`func ()`直到需要x称为惰性计算。 599 | 600 | 这种策略可以大大提高性能,特别是与方法链和数组一起使用时,这是函数式程序员最喜欢的程序流程技术。 601 | 602 | 惰性计算(求值)的一个很棒的好处是无限级数的存在。 因为实际上什么都不会计算,直到无法进一步延迟为止,所以可以这样做: 603 | 604 | ```js 605 | // wishful JavaScript pseudocode: 606 | var infinateNums = range(1 to infinity); 607 | var tenPrimes = infinateNums.getPrimeNumbers().first(10); 608 | ``` 609 | 610 | 这为许多可能性打开了大门:异步执行,并行化和组合,仅举几例。 611 | 612 | 但是,存在一个问题:JavaScript不会自行执行惰性运算。 就是说,一些JavaScript的库,它们可以很好地模拟惰性运算。 关注三章,搭建功能编程环境。 613 | 614 | ## 函数式编程程序员的工具集 615 | 616 | 仔细查看了到目前为止提供的一些示例,您会注意到正在使用的一些您可能不熟悉的方法。 它们是map(),filter()和reduce()函数,对于任何语言的每个函数程序都至关重要。 它们使您能够删除循环和语句,从而使代码更简洁。 617 | 618 | map(),filter()和reduce()函数构成了函数式程序员工具集的核心,是纯的,高阶函数的集合,这些函数是函数方法的主力军。 实际上,它们是纯函数和高阶函数应该是什么样的一个缩影。 它们将函数作为输入,并返回零副作用的输出。 619 | 620 | 虽然这些方法是ECMAScript 5.1的浏览器的标准配置,但它们仅适用于数组。 每次调用它时,都会创建并返回一个新数组。 现有阵列未修改。 但是,还有更多的情况,它们将函数作为输入,通常采用匿名函数的形式,称为回调函数。 他们遍历数组并将函数应用于数组中的每个项目(我们常用,是不是?)。 621 | 622 | ```js 623 | myArray = [1, 2, 3, 4]; 624 | newArray = myArray.map(function(x) { 625 | return x * 2; 626 | }); 627 | console.log(myArray); // Output: [1,2,3,4] 628 | console.log(newArray); // Output: [2,4,6,8] 629 | ``` 630 | 631 | 另外,这些方法仅适用于数组,因此不适用于其他可迭代的数据结构,例如某些对象。 不用担心,underscore.js,Lazy.js,stream.js等库均实现了自己的map(),filter()和reduce()方法,它们的用途更加广泛(向这些库致敬,大家可以关注我的[bbo工具函数库](https://github.com/Tnfe/bbo.git)项目)。 632 | 633 | ## 回调函数 634 | 635 | 鉴于JavaScript允许声明函数的几种不同方式,callback()函数用于传递给其他函数供他们使用。 这是一种传递逻辑的方法,就像传递对象一样: 636 | 637 | ```js 638 | var myArray = [1, 2, 3]; 639 | function myCallback(x) { 640 | return x + 1; 641 | } 642 | console.log(myArray.map(myCallback)); 643 | 644 | ``` 645 | 646 | 为了简化工作,可以使用匿名函数: 647 | 648 | ```js 649 | console.log( 650 | myArray.map(function(x) { 651 | return x + 1; 652 | }) 653 | ); 654 | 655 | ``` 656 | 657 | 它们不仅用于函数式编程,而且还用于JavaScript中的许多事情。这是在用jQuery进行AJAX调用中使用的callback()函数: 658 | 659 | ```js 660 | function myCallback(xhr) { 661 | console.log(xhr.status); 662 | return true; 663 | } 664 | 665 | $.ajax(myURI).done(myCallback); 666 | ``` 667 | 668 | 请注意,如果仅使用了函数名称,而没有调用回调并且仅传递了回调的名称,是不能执行的。所以以下代码是错误的: 669 | 670 | ```js 671 | $.ajax(myURI).fail(myCallback(xhr)); 672 | // or 673 | $.ajax(myURI).fail(myCallback()); 674 | ``` 675 | 676 | 如果我们调用回调会发生什么? 在那种情况下,`myCallback(xhr)`方法将尝试执行`-"undefined"`将被打印到控制台,并且将返回`True`。 当`ajax()`调用完成时,它将使用`true`作为要使用的回调函数的名称,这将引发错误。 677 | 678 | 这也意味着我们无法指定将哪些参数传递给回调函数。 如果我们需要与`ajax()`调用传递的参数不同的参数,则可以将回调函数包装在匿名函数中。 679 | 680 | ```js 681 | function myCallback(status) { 682 | console.log(status); 683 | return true; 684 | } 685 | $.ajax(myURI).done(function(xhr) { 686 | myCallback(xhr.status); 687 | }); 688 | ``` 689 | 690 | ## 荣誉函数 691 | 692 | `map()` `filter()` `reduce()` 这三个函数为函数式编程带来便利。 693 | 694 | ### Array.prototype.map() 695 | 696 | 用法:`Syntax: arr.map(callback [, thisArg]);` 697 | 698 | 它是函数作用域用的做多的,它对数组中的每个元素应用回调函数。 699 | 700 | 案例: 701 | 702 | ```js 703 | var integers = [1, -0, 9, -8, 3], 704 | numbers = [1, 2, 3, 4], 705 | str = "hello world how ya doing?"; 706 | // map integers to their absolute values 707 | console.log(integers.map(Math.abs)); 708 | // multiply an array of numbers by their position in the array 709 | 710 | console.log( 711 | numbers.map(function(x, i) { 712 | return x * i; 713 | }) 714 | ); 715 | // Capitalize every other word in a string. 716 | console.log( 717 | str.split(" ").map(function(s, i) { 718 | if (i % 2 == 0) { 719 | return s.toUpperCase(); 720 | } else { 721 | return s; 722 | } 723 | }) 724 | ); 725 | ``` 726 | 727 | 小技巧: 728 | 729 | While the Array.prototype.map method is a standard method for the Array object in JavaScript, it can be easily extended to your custom objects as well. 730 | 731 | ```js 732 | MyObject.prototype.map = function(f) { 733 | return new MyObject(f(this.value)); 734 | }; 735 | ``` 736 | 737 | ### Array.prototype.filter() 738 | 739 | `filter()`函数用于将元素从数组中取出。 回调必须返回`true`(将项目包括在新数组中)或`false`(将其删除)。 通过使用`map()`函数并为要删除的项目返回空值,可以实现类似的效果,但是`filter()`函数将从新数组中删除该项目,而不是在其位置插入空值。 740 | 741 | 用法:`Syntax: arr.filter(callback [, thisArg]);` 742 | 743 | 案例: 744 | 745 | ```js 746 | var myarray = [1, 2, 3, 4]; 747 | words = "hello 123 world how 345 ya doing".split(" "); 748 | re = "[a-zA-Z]"; 749 | // remove all negative numbers 750 | console.log( 751 | [-2, -1, 0, 1, 2].filter(function(x) { 752 | return x > 0; 753 | }) 754 | ); 755 | // remove null values after a map operation 756 | console.log( 757 | words.filter(function(s) { 758 | return s.match(re); 759 | }) 760 | ); 761 | // remove random objects from an array 762 | console.log( 763 | myarray.filter(function() { 764 | return Math.floor(Math.random() * 2); 765 | }) 766 | ); 767 | ``` 768 | 769 | ### Array.prototype.reduce() 770 | 771 | 有时称为“fold”的`reduce()`函数用于将数组的所有值累加为一个。 回调需要返回要执行的逻辑以合并对象。 如果是数字,通常将它们加在一起以获得总和,或者相乘得到一个乘积。 对于字符串,通常将字符串附加在一起。 772 | 773 | 用法:`Syntax: arr.reduce(callback [, initialValue]);` 774 | 775 | 案例: 776 | 777 | ```js 778 | var numbers = [1, 2, 3, 4]; 779 | // sum up all the values of an array 780 | console.log( 781 | [1, 2, 3, 4, 5].reduce(function(x, y) { 782 | return x + y; 783 | }, 0) 784 | ); 785 | // sum up all the values of an array 786 | console.log( 787 | [1, 2, 3, 4, 5].reduce(function(x, y) { 788 | return x + y; 789 | }, 0) 790 | ); 791 | // find the largest number 792 | console.log( 793 | numbers.reduce(function(a, b) { 794 | return Math.max(a, b); 795 | }) // max takes two arguments 796 | ); 797 | ``` 798 | 799 | `map()`,`filter()`和`reduce()`函数在我们的辅助函数工具箱中很常见。几乎所有的函数式方法里都用他们的身影。 800 | 801 | ### Array.prototype.forEach 802 | 803 | 用法:`Syntax: arr.forEach(callback [, thisArg]);` 804 | 805 | 本质上是`map()`的非纯版本,`forEach()`遍历数组,并对每个项目应用callback()函数。但它不返回任何内容。 这是执行for循环的一种更干净的方法。 806 | 807 | 例子: 808 | 809 | ```js 810 | var arr = [1, 2, 3]; 811 | var nodes = arr.map(function(x) { 812 | var elem = document.createElement("div"); 813 | elem.textContent = x; 814 | return elem; 815 | }); 816 | // log the value of each item 817 | arr.forEach(function(x) { 818 | console.log(x); 819 | }); 820 | // append nodes to the DOM 821 | nodes.forEach(function(x) { 822 | document.body.appendChild(x); 823 | }); 824 | ``` 825 | 826 | ### Array.prototype.concat 827 | 828 | 当使用数组而不是for和while循环时,通常需要将多个数组连接在一起。另一个JavaScript内置函数`concat()`为我们解决了这一问题。 `concat()`函数返回一个新数组,并保持旧数组不变。 它可以连接传递给它的数组。 829 | 830 | ```js 831 | console.log([1, 2, 3].concat(['a','b','c']) // concatenate two arrays); 832 | // Output: [1, 2, 3, 'a','b','c'] 833 | ``` 834 | 835 | 原始阵列保持不变。 它返回一个新数组,两个数组串联在一起。 这也意味着concat()函数可以连接在一起。 836 | 837 | ```js 838 | var arr1 = [1,2,3]; 839 | var arr2 = [4,5,6]; 840 | var arr3 = [7,8,9]; 841 | var x = arr1.concat(arr2, arr3); 842 | 843 | var y = arr1.concat(arr2).concat(arr3); 844 | var z = arr1.concat(arr2.concat(arr3)); 845 | console.log(x); 846 | console.log(y); 847 | console.log(z); 848 | ``` 849 | 850 | 变量x,y和z都包含[1,2,3,4,5,6,7,8,9]。 851 | 852 | ### Array.prototype.reverse 853 | 854 | 另一个JavaScript函数可帮助进行数组转换。 `reverse()`函数会反转数组,以使第一个元素现在是最后一个元素,而最后一个元素现在是第一个元素。 855 | 856 | 但是,它不会返回新的数组。 相反,它会在适当的位置改变数组。 下面案例做的更好: 这是用于反转数组的纯方法的实现: 857 | 858 | ```js 859 | var invert = function(arr) { 860 | return arr.map(function(x, i, a) { 861 | return a[a.length - (i + 1)]; 862 | }); 863 | }; 864 | var q = invert([1, 2, 3, 4]); 865 | console.log(q); 866 | ``` 867 | 868 | ### Array.prototype.sort 869 | 870 | 就像`map()`,`filter()`和`reduce()`方法一样,`sort()`方法采用`callback()`函数,该函数定义应如何对数组中的对象进行排序。但是,像`reverse()`函数一样,它会在适当的位置改变数组。 871 | 872 | ```js 873 | arr = [200, 12, 56, 7, 344]; 874 | console.log(arr.sort(function(a,b){return a – b}) ); 875 | // arr is now: [7, 12, 56, 200, 344]; 876 | ``` 877 | 878 | 我们可以编写一个纯粹的sort()函数,该函数不会使数组发生变化,但排序算法是造成很多麻烦的根源。 实际上,需要排序的大型数组实际上应为这种目的而设计的数据结构:quickStort,mergeSort,bubbleSort等。 879 | 880 | ### Array.prototype.every 与 Array.prototype.some 881 | 882 | `Array.prototype.every()`和`Array.prototype.some()`函数既是纯函数又是高阶函数,它们是Array对象的方法,用于针对必须具有`callback()`函数的数组元素进行测试 返回各个输入的布尔值。 如果callback()函数对数组中的每个元素都返回`true`,则`every()`函数将返回`true`,如果数组中的某些元素为`true`,则some()函数将返回`true`。 883 | 884 | 例子: 885 | 886 | ```js 887 | function isNumber(n) { 888 | return !isNaN(parseFloat(n)) && isFinite(n); 889 | } 890 | console.log([1, 2, 3, 4].every(isNumber)); // Return: true 891 | console.log([1, 2, "a"].every(isNumber)); // Return: false 892 | console.log([1, 2, "a"].some(isNumber)); // Return: true 893 | ``` 894 | 895 | ## 小结 896 | 897 | 为加深对函数式编程的理解,本章涵盖了相当广泛的主题。 我们分析了一种编程语言起作用的意义,评估了JavaScript的函数式编程能力。 接下来,我们应用了使用JavaScript进行函数式编程的核心概念,并展示了JavaScript的一些用于函数式编程的内置函数。 898 | 899 | 尽管JavaScript确实有一些用于函数式编程的工具,但其功能核心大部分仍处于隐藏状态,并且有很多不足之处。 在下一章中,我们将探索一些JavaScript库,以展示其功能。 900 | -------------------------------------------------------------------------------- /docs/book/chapter-seventh.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 您会经常听到 JavaScript 是一种空白语言,其中 blank 是面向对象的,功能性的或通用的。 这本书着重于 JavaScript 作为一种函数式语言,并竭尽全力地证明了这一点。 但是事实是,JavaScript 是一种通用语言,这意味着它完全能够支持多种编程样式。 像 Python 和 F#一样,JavaScript 是多范式的。但与这些语言不同,JavaScript 是基于原型 OOP 编程,而大多数其他通用语言是基于类的。 4 | 5 | 在最后一章中,我们将把函数式编程和面向对象的编程都与 JavaScript 相关联,并了解这两种范例如何相互补充和并存。 本章将讨论以下主题: 6 | 7 | - JavaScript 如何具有函数式和 OOP? 8 | - JavaScript 的 OOP –使用原型链 9 | - 如何在 JavaScript 中混合函数式和 OOP 10 | - 功能 inheritance (extends) 11 | - 功能 mixins 12 | 13 | 写更好的代码是我们的目标。函数式和面向对象编程正是实现这一目标的手段。 14 | 15 | ## 多范式语言 16 | 17 | 如果面向对象编程意味着把所有变量都当作对象,而函数编程意味着把所有函数都当作变量,那么函数就不能被当作对象来对待吗?在 JavaScript 中,当然可以。 18 | 19 | 但是说函数式编程意味着把函数当作变量是有点不准确的。一个更好的说法是:函数式编程意味着将所有内容都视为值,特别是函数。 20 | 21 | 描述函数式编程的更好方法可能是将其称为声明式。 声明式编程独立于编程风格的必要分支,表达了解决问题所需的计算逻辑。 告诉计算机问题是什么,而不是如何解决问题的过程。 22 | 23 | 同时,面向对象的编程是从命令式编程风格派生而来的:计算机被分步指令如何解决问题。 OOP 要求将计算指令(方法)及其所处理的数据(成员变量)组织为称为对象的单元。 访问该数据的唯一方法是通过对象的方法。 24 | 25 | 那么如何将这两种风格融合在一起? 26 | 27 | - 对象方法中的代码通常以命令式编写。 但是,如果它是函数式风格呢? 毕竟,OOP 不会排除不可变的数据和高阶函数。 28 | - 也许将两者结合起来的更纯粹的方法是将对象视为函数和基于类的对象。 29 | - 也许我们可以简单地将函数式编程中的一些想法(例如 promise、递归)包含到面向对象的应用程序中。 30 | - OOP 涵盖诸如封装,多态性和抽象之类的主题。 函数式编程也是如此,只是以一种不同的方式进行。因此,我们可以在面向函数式的应用程序中包含来自面向对象编程的方法。 31 | 32 | 关键是:OOP 和 FP 可以混合在一起,有几种方法可以做到,它们不相互排斥。 33 | 34 | ## 使用原型实现面向对象 35 | 36 | JavaScript 是一种无类语言。 这并不意味着它比其他计算机语言更不流行。 类更少意味着它不像面向对象语言那样具有类结构。相反,它使用原型进行继承。 37 | 38 | 可能有 C++和 Java 背景下的程序员感到困惑,但基于原型的继承比传统继承更具表现力。下面是 C++与 JavaScript 之间的差异的简单比较: 39 | 40 | | C++ | JavaScript | 41 | | :----- | ---------: | 42 | | 强类型 | 松散型 | 43 | | 静态的 | 动态的 | 44 | | 基于类 | 基于原型 | 45 | | 类 | 功能 | 46 | | 方法 | 功能 | 47 | 48 | ## 继承(Inheritance) 49 | 50 | 在进一步讨论之前,让我们确保充分理解面向对象编程中继承的概念。以下(伪)代码演示了基于类的继承: 51 | 52 | ```js 53 | class Polygon { 54 | int numSides; 55 | function init(n) { 56 | numSides = n; 57 | } 58 | } 59 | class Rectangle inherits Polygon { 60 | int width; 61 | int length; 62 | function init(w, l) { 63 | numSides = 4; 64 | width = w; 65 | length = l; 66 | } 67 | function getArea() { 68 | return w * l; 69 | } 70 | } 71 | class Square inherits Rectangle { 72 | function init(s) { 73 | numSides = 4; 74 | width = s; 75 | length = s; 76 | } 77 | } 78 | ``` 79 | 80 | Polygon 类是其他类继承的父类。 它仅定义一个成员变量,即边数,该变量在 init()函数中设置。 Rectangle 子类继承自 Polygon 类,并添加了另外两个成员变量 length 和 width 和方法 getArea()。 它不需要定义 numSides 变量,因为它已经由其继承的类定义,并且它也覆盖了 init()函数。 Square 类通过从 Rectangle 类继承其 getArea()方法来进一步继承此继承链。 通过再次简单地重写 init()函数以使长度和宽度相同,getArea()函数就可以保持不变,并且需要编写的代码更少。 81 | 82 | 在传统的 OOP 语言中,继承就是这样的。如果我们想向所有对象添加一个颜色属性,我们只需将其添加到多边形对象,而不必修改从其继承的任何对象。 83 | 84 | ## 原型链 85 | 86 | JavaScript 的继承可以归结为原型。 每个对象都有一个称为其原型的内部属性,该属性是指向另一个对象的链接。 该对象具有自己的原型。 此模式可以重复进行,直到到达未定义为其原型的对象为止。 这就是原型链,这就是继承在 JavaScript 中的工作方式。下图解释了 JavaScirpt 中的继承: 87 | 88 | ![img](https://blog.ahthw.com/wp-content/uploads/2020/01/prototype.png) 89 | 90 | 当运行对对象函数定义的搜索时,JavaScript“遍历”原型链,直到找到具有正确名称的函数的第一个定义。因此,重写它就像在子类的原型上提供新的定义一样简单。 91 | 92 | ## Object.create()方法 93 | 94 | 正如用 JavaScript 创建对象有很多方法一样,复制基于类的经典继承也有很多方法。但最好的方法是使用 Object.create()方法。 95 | 96 | ```js 97 | var Polygon = function(n) { 98 | this.numSides = n; 99 | }; 100 | var Rectangle = function(w, l) { 101 | this.width = w; 102 | this.length = l; 103 | }; 104 | // the Rectangle's prototype is redefined with Object.create 105 | Rectangle.prototype = Object.create(Polygon.prototype); 106 | // it's important to now restore the constructor attribute 107 | // otherwise it stays linked to the Polygon 108 | Rectangle.prototype.constructor = Rectangle; 109 | // now we can continue to define the Rectangle class 110 | Rectangle.prototype.numSides = 4; 111 | Rectangle.prototype.getArea = function() { 112 | return this.width * this.length; 113 | }; 114 | var Square = function(w) { 115 | this.width = w; 116 | this.length = w; 117 | }; 118 | Square.prototype = Object.create(Rectangle.prototype); 119 | Square.prototype.constructor = Square; 120 | var s = new Square(5); 121 | console.log(s.getArea()); // 25 122 | ``` 123 | 124 | 对于许多人来说,这种语法可能看起来并不常见,但是通过一点实践,它将变得熟悉。 必须使用 prototype 关键字来访问所有对象都具有的内部属性[[Prototype]]。 Object.create()方法声明一个带有指定对象的新对象,以使其原型从其继承。 这样,可以在 JavaScript 中实现经典继承。 125 | 126 | :::tip 127 | Object.create()方法于 2011 年在 ECMAScript 5.1 中引入,被称为创建对象的新方法和首选方法。 这只是将继承集成到 JavaScript 中的许多尝试之一。 幸运的是,这种方法效果很好。 128 | ::: 129 | 130 | 在第 5 章范畴理论中构建 Maybe 类时看到了这种继承结构。下面是 Maybe、None 和 Just 类,它们像前面的示例一样相互继承。 131 | 132 | ```js 133 | var Maybe = function() {}; 134 | var None = function() {}; 135 | None.prototype = Object.create(Maybe.prototype); 136 | None.prototype.constructor = None; 137 | None.prototype.toString = function() { 138 | return "None"; 139 | }; 140 | var Just = function(x) { 141 | this.x = x; 142 | }; 143 | Just.prototype = Object.create(Maybe.prototype); 144 | Just.prototype.constructor = Just; 145 | Just.prototype.toString = function() { 146 | return "Just " + this.x; 147 | }; 148 | ``` 149 | 150 | 这表明 JavaScript 中的类继承可以帮助我们函数式编程。 151 | 152 | 一个常见的错误是将构造函数传递到 Object.create()而不是原型对象。在子类尝试使用继承的成员函数之前,虽然不会抛出错误,但会使问题更加复杂。 153 | 154 | ```js 155 | Foo.prototype = Object.create(Parent.prototype); // correct 156 | Bar.prototype = Object.create(Parent); // incorrect 157 | Bar.inheritedMethod(); // Error: function is undefined 158 | ``` 159 | 160 | 如果 InheritedMethod()方法已附加到 Foo.prototype 类,则找不到该函数。 如果使用 Bar 构造函数中的`this.inheritedMethod = function(){...}`将`InheritedMethod()`方法直接附加到实例,则使用 Parent 作为`Object.create()`的参数可能是正确的。 161 | 162 | ## 函数式和 OOP 163 | 164 | 几十年来,面向对象编程一直是主要的编程范例。 它在全世界 101 个计算机科学课程中教授,而函数编程则不是。 这是软件架构师用来设计应用程序的功能,而函数式编程则不是。 而且这也很有意义:OOP 使抽象概念的概念化变得容易。 它使编写代码变得更加容易。 165 | 166 | 所以,除非你能让老板相信应用程序必须是全功能的,否则我们将在面向对象的世界中使用函数式编程。本节将探讨如何做到这一点。 167 | 168 | ## 函数继承 169 | 170 | 将函数式编程应用于 JavaScript 应用程序的最方便的方法也许是在 OOP 原则内使用大多数函数式样式,例如继承。 171 | 172 | 为了探索这是如何工作的,让我们构建一个简单的应用程序来计算产品的价格。 首先,我们需要一些产品类别: 173 | 174 | ```js 175 | var Shirt = function(size) { 176 | this.size = size; 177 | }; 178 | var TShirt = function(size) { 179 | this.size = size; 180 | }; 181 | TShirt.prototype = Object.create(Shirt.prototype); 182 | TShirt.prototype.constructor = TShirt; 183 | TShirt.prototype.getPrice = function() { 184 | if (this.size == "small") { 185 | return 5; 186 | } else { 187 | return 10; 188 | } 189 | }; 190 | var ExpensiveShirt = function(size) { 191 | this.size = size; 192 | }; 193 | ExpensiveShirt.prototype = Object.create(Shirt.prototype); 194 | ExpensiveShirt.prototype.constructor = ExpensiveShirt; 195 | ExpensiveShirt.prototype.getPrice = function() { 196 | if (this.size == "small") { 197 | return 20; 198 | } else { 199 | return 30; 200 | } 201 | }; 202 | ``` 203 | 204 | 然后我们可以将它们组织到一个 Store 类中,如下所示: 205 | 206 | ```js 207 | var Store = function(products) { 208 | this.products = products; 209 | }; 210 | Store.prototype.calculateTotal = function() { 211 | return ( 212 | this.products.reduce(function(sum, product) { 213 | return sum + product.getPrice(); 214 | }, 10) * TAX 215 | ); // start with $10 markup, times global TAX var 216 | }; 217 | var TAX = 1.08; 218 | var p1 = new TShirt("small"); 219 | var p2 = new ExpensiveShirt("large"); 220 | var s = new Store([p1, p2]); 221 | console.log(s.calculateTotal()); // Output: 35 222 | ``` 223 | 224 | computeTotal()方法使用数组的 reduce()函数将所有产品的价格干净地加在一起。 225 | 226 | 但是如果我们需要一种动态方法来计算标记值怎么办? 为此,我们可以转向一个称为“战略模式”的概念。 227 | 228 | ## 策略模式 229 | 230 | 策略模式是定义一系列可互换算法的方法。 OOP 程序员使用它在运行时操纵行为,但它基于一些函数式编程原则: 231 | 232 | - 逻辑与数据分离 (Separation of logic and data) 233 | - 功能组成 (Composition of functions) 234 | - 用作一流的对象 (Functions as first-class objects) 235 | 236 | 还有一些 OOP 原则: 237 | 238 | - 封装 239 | - 继承 240 | 241 | 在前面解释过的计算产品成本的示例应用程序中,假设我们希望对某些客户给予优惠,并且必须调整加价以反映这一点。 242 | 243 | 因此,让我们创建一些客户类别: 244 | 245 | ```js 246 | var Customer = function() {}; 247 | Customer.prototype.calculateTotal = function(products) { 248 | return ( 249 | products.reduce(function(total, product) { 250 | return total + product.getPrice(); 251 | }, 10) * TAX 252 | ); 253 | }; 254 | var RepeatCustomer = function() {}; 255 | RepeatCustomer.prototype = Object.create(Customer.prototype); 256 | RepeatCustomer.prototype.constructor = RepeatCustomer; 257 | RepeatCustomer.prototype.calculateTotal = function(products) { 258 | return ( 259 | products.reduce(function(total, product) { 260 | return total + product.getPrice(); 261 | }, 5) * TAX 262 | ); 263 | }; 264 | var TaxExemptCustomer = function() {}; 265 | TaxExemptCustomer.prototype = Object.create(Customer.prototype); 266 | TaxExemptCustomer.prototype.constructor = TaxExemptCustomer; 267 | TaxExemptCustomer.prototype.calculateTotal = function(products) { 268 | return products.reduce(function(total, product) { 269 | return total + product.getPrice(); 270 | }, 10); 271 | }; 272 | ``` 273 | 274 | 每个 Customer 类都封装算法。 现在,我们只需要 Store 类来调用 Customer 类的 calculateTotal()方法。 275 | 276 | ```js 277 | var Store = function(products) { 278 | this.products = products; 279 | this.customer = new Customer(); 280 | // bonus exercise: use Maybes from Chapter 5 instead of a 281 | default customer instance 282 | } 283 | 284 | Store.prototype.setCustomer = function(customer) { 285 | this.customer = customer; 286 | } 287 | Store.prototype.getTotal = function(){ 288 | return this.customer.calculateTotal(this.products); 289 | }; 290 | var p1 = new TShirt('small'); 291 | var p2 = new ExpensiveShirt('large'); 292 | var s = new Store([p1,p2]); 293 | var c = new TaxExemptCustomer(); 294 | s.setCustomer(c); 295 | s.getTotal(); // Output: 45 296 | ``` 297 | 298 | Customer 类进行计算,Product 类保存数据(价格),Store 类维护上下文。这将实现非常高的凝聚力,并将面向对象编程和函数编程很好地结合起来。JavaScript 的高级表达能力使得这一点成为可能,而且非常简单。 299 | 300 | ## Mixins 301 | 302 | 简而言之,mixins 是可以允许其他类使用其方法的类。 这些方法仅应由其他类使用,并且 mixin 类本身永远不会实例化。 这有助于避免继承的歧义。 它们是将函数式编程与面向对象的编程相结合的好方法。 303 | 304 | 在每种语言中,mixin 的实现方式都不同。 由于 JavaScript 的灵活性和表现力,mixin 被实现为仅具有方法的对象。 尽管可以将它们定义为函数对象(即`var mixin = function(){...};`),但是对于代码的结构学科而言,将它们定义为对象常量(即`var mixin = {...};`)。 这将帮助我们区分类和混合。 毕竟,mixins 应该被视为进程,而不是对象。 305 | 306 | 让我们开始声明一些混合。 我们将从上一节扩展我们的 Store 应用程序,使用 mixins 扩展类。 307 | 308 | ```js 309 | var small = { 310 | getPrice: function() { 311 | return this.basePrice + 6; 312 | }, 313 | getDimensions: function() { 314 | return [44, 63]; 315 | } 316 | }; 317 | var large = { 318 | getPrice: function() { 319 | return this.basePrice + 10; 320 | }, 321 | getDimensions: function() { 322 | return [64, 83]; 323 | } 324 | }; 325 | ``` 326 | 327 | 不仅如此。 可以添加更多的 mixin,例如颜色或织物材料。 我们将不得不稍微重写一下 Shirt 类,如以下代码片段所示: 328 | 329 | ```js 330 | var Shirt = function() { 331 | this.basePrice = 1; 332 | }; 333 | Shirt.getPrice = function() { 334 | return this.basePrice; 335 | }; 336 | var TShirt = function() { 337 | this.basePrice = 5; 338 | }; 339 | TShirt.prototype = Object.create(Shirt.prototype); 340 | TShirt.prototype.constructor = TShirt; 341 | ``` 342 | 343 | 我们终于可以使用 mixins 了。 344 | 345 | ### 经典Mixins 346 | 347 | 您可能想知道这些 mixins 如何与类混合。经典方法是将 mixin 的功能复制到接收对象中。 这可以通过对 Shirt 原型的以下扩展来完成: 348 | 349 | ```js 350 | Shirt.prototype.addMixin = function(mixin) { 351 | for (var prop in mixin) { 352 | if (mixin.hasOwnProperty(prop)) { 353 | this.prototype[prop] = mixin[prop]; 354 | } 355 | } 356 | }; 357 | ``` 358 | 359 | 这样可以按以下方式添加 mixins: 360 | 361 | ```js 362 | TShirt.addMixin(small); 363 | var p1 = new TShirt(); 364 | console.log(p1.getPrice()); // Output: 11 365 | TShirt.addMixin(large); 366 | var p2 = new TShirt(); 367 | console.log(p2.getPrice()); // Output: 15 368 | ``` 369 | 370 | 但是,存在一个主要问题。 再次计算 p1 的价格时,它将返回 15,即大型商品的价格。 它应该是一个小的 price。 371 | 372 | ```js 373 | console.log(p1.getPrice()); // Output: 15 374 | ``` 375 | 376 | 问题在于,每次向其添加 mixin 时,都会重写 Shirt 对象的 prototype.getPrice()方法。 这根本不是很有功能,不是我们想要的。 377 | 378 | ### plusMixins 379 | 380 | 还有另一种使用 mixin 的方法,一种与函数式编程更加一致的方法。 381 | 382 | 无需将 mixin 的方法复制到目标对象,我们需要创建一个新对象,该对象是添加了 mixin 的方法的目标对象的副本。必须首先克隆该对象,这可以通过创建一个新对象来实现。 从其继承的对象。 我们将这种变体称为 plusMixin。 383 | 384 | ```js 385 | Shirt.prototype.plusMixin = function(mixin) { 386 | // create a new object that inherits from the old 387 | var newObj = this; 388 | newObj.prototype = Object.create(this.prototype); 389 | for (var prop in mixin) { 390 | if (mixin.hasOwnProperty(prop)) { 391 | newObj.prototype[prop] = mixin[prop]; 392 | } 393 | } 394 | return newObj; 395 | }; 396 | var SmallTShirt = Tshirt.plusMixin(small); // creates a new class 397 | var smallT = new SmallTShirt(); 398 | console.log(smallT.getPrice()); // Output: 11 399 | var LargeTShirt = Tshirt.plusMixin(large); 400 | var largeT = new LargeTShirt(); 401 | console.log(largeT.getPrice()); // Output: 15 402 | console.log(smallT.getPrice()); // Output: 11 (not effected by 2nd mixin call) 403 | ``` 404 | 405 | 现在我们可以使用 mixins 真正发挥作用了。 我们可以创建产品和 mixin 的所有可能组合。 406 | 407 | ```js 408 | // in the real world there would be way more products and mixins! 409 | var productClasses = [ExpensiveShirt, Tshirt]; 410 | var mixins = [small, medium, large]; 411 | // mix them all together 412 | products = productClasses.reduce(function(previous, current) { 413 | var newProduct = mixins.map(function(mxn) { 414 | var mixedClass = current.plusMixin(mxn); 415 | var temp = new mixedClass(); 416 | return temp; 417 | }); 418 | return previous.concat(newProduct); 419 | }, []); 420 | products.forEach(function(o) { 421 | console.log(o.getPrice()); 422 | }); 423 | ``` 424 | 425 | 为了使其更加面向对象,我们可以使用此功能重写 Store 对象。 我们还将向 Store 对象(而不是产品)添加显示功能,以保持界面逻辑和数据分离。 426 | 427 | ```js 428 | // the store 429 | var Store = function() { 430 | productClasses = [ExpensiveShirt, TShirt]; 431 | productMixins = [small, medium, large]; 432 | this.products = productClasses.reduce(function(previous, current) { 433 | var newObjs = productMixins.map(function(mxn) { 434 | var mixedClass = current.plusMixin(mxn); 435 | var temp = new mixedClass(); 436 | return temp; 437 | }); 438 | return previous.concat(newObjs); 439 | }, []); 440 | }; 441 | Store.prototype.displayProducts = function() { 442 | this.products.forEach(function(p) { 443 | $("ul#products").append( 444 | "
  • " + p.getTitle() + ":$" + p.getPrice() + "
  • " 445 | ); 446 | }); 447 | }; 448 | ``` 449 | 450 | 我们要做的就是创建一个 Store 对象并调用其 displayProducts()方法来生成产品和价格的列表` 451 | 452 | ```text 453 | 461 | ``` 462 | 463 | 这些行需要添加到产品类和 mixins 中,以使前面的输出起作用: 464 | 465 | ```js 466 | Shirt.prototype.title = 'shirt'; 467 | TShirt.prototype.title = 't-shirt'; 468 | ExpensiveShirt.prototype.title = 'premium shirt'; 469 | // then the mixins got the extra 'getTitle' function: 470 | var small = { 471 | ... 472 | getTitle: function() { 473 | return 'small ' + this.title; // small or medium or large 474 | } 475 | } 476 | ``` 477 | 478 | 而且,就像这样,我们有一个高度模块化和可扩展的电子商务应用程序。 可以轻松地添加新的衬衫样式,只需定义一个新的 Shirt 子类并向其中添加 Store 类的数组产品类即可。 Mixins 以相同的方式添加。 因此,现在当老板说:“嘿,我们有一种新型的衬衫和外套,每种都有标准颜色,我们需要在今天回家之前将它们添加到网站上”,我们可以放心,我们不会熬夜! 479 | 480 | ## 小结 481 | 482 | JavaScript 具有很高的表现力。 这使得混合函数式和面向对象编程成为可能。 现代 JavaScript 不仅是 OOP 或功能,而且是两者的结合。Strategy Pattern 和 mixins 之类的概念非常适合 JavaScript 的原型结构,它们有助于证明当今的 JavaScript 中的最佳实践共享了相同数量的函数编程和面向对象编程。 483 | 484 | 如果你从这本书中只拿走一件事,我希望它是如何将函数式编程技术应用到实际应用中。这一章向你展示了如何做到这一点。 485 | -------------------------------------------------------------------------------- /docs/book/chapter-sixth.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | JavaScript 被称为“web 的汇编语言”。类比(它不是完美的,还还有更完美的么?),从 JavaScipt 通常被编译的目标(即 Clojure 和 CoffeeScript)这一事实出发,还从许多其他来源(如 pyjamas(python to JS)和 Google Web Kit(Java 到 JS))中得出结论。 4 | 5 | 引用了一个更愚蠢的想法,JavaScript 和 x86 程序集一样具有表现力和低级。也许这个概念源于这样一个事实:自从 JavaScript 在 1995 年首次随 Netscape 一起发布以来,它就一直因其设计缺陷和疏忽而受到抨击。它是在匆忙中开发和发布的,正因为如此,一些有问题的设计模式进入了 JavaScript,这种语言很快成为了事实上的 web 脚本语言。“;”是个大错误。定义函数的方法也不明确。是 var foo = function();还是 function foo(); 6 | 7 | 函数式编程是处理这些错误的一个很好的方法。 通过关注 JavaScript 确实是一种函数式语言这一事实,在前面关于声明函数的不同方法的示例中,最好将函数声明为变量。分号主要是为了让 JavaScript 看起来更像 C 语言。 8 | 9 | 但是请始终记住您使用的语言。 与其他任何语言一样,JavaScript 也有其缺陷。 而且,当以一种经常避开可能的前沿优势的风格进行编程时,这些小问题可能会变成不可恢复的问题陷阱。 其中一些陷阱包括: 10 | 11 | - 递归 12 | - 可变范围和闭包 13 | - 函数声明与函数表达式 14 | 15 | 不过,这些问题只要稍加注意就可以解决。 16 | 17 | ## 递归 18 | 19 | 递归对于任何语言的函数式编程都非常重要。许多函数式语言甚至不提供 for 和 while 循环语句,从而要求迭代递归;只有当语言保证消除尾部调用时,这才是可能的,而 JavaScript 则不是这样。在第 2 章“函数式编程基础”中快速介绍了递归。但在本节中,我们将深入研究递归在 JavaScript 中的工作原理。 20 | 21 | ### 尾递归 22 | 23 | JavaScript 处理递归的例程称为 tail 递归,这是基于堆栈的递归实现。 这意味着,对于每个递归调用,堆栈中都会有一个新帧。 24 | 25 | 为了说明这个方法可能产生的问题,让我们使用经典的阶乘递归算法。 26 | 27 | ```js 28 | var factorial = function(n) { 29 | if (n == 0) { 30 | // base case 31 | return 1; 32 | } else { 33 | // recursive case 34 | return n * factorial(n - 1); 35 | } 36 | }; 37 | ``` 38 | 39 | 该算法将调用 n 次以获取答案。 它实际上是在计算(1 x 1 x 2 x 3 x…x N)。 这意味着时间复杂度为 O(n)。 40 | 41 | ::: tip 42 | 43 | O(n),发音为“big oh to the n,"”,意味着随着输入大小的增长,算法将以 n 的速率增长,即较窄的增长。 O(n2)是指数增长,O(log(n))是对数 44 | 增长等等。 此符号可用于时间复杂度和空间复杂度。 45 | ::: 46 | 47 | 但是,因为在每个迭代中分配了存储器堆栈中的新帧,所以空间复杂度也是 O(n)。这是个问题。这意味着内存的消耗速度将很容易超过内存限制。在我的笔记本电脑上,阶乘 factorial(23456)返回`Uncaught Error:RangeError: Maximum call stack size exceeded`超出最大调用堆栈大小。 48 | 49 | 尽管计算 23,456 的阶乘是一件轻而易举的事,但是可以放心,用递归解决的许多问题将增长到该大小而不会带来太多麻烦。 考虑数据树的情况。 树可以是任何东西:搜索应用程序,文件系统,路由表等等。 下面是树遍历功能的一个非常简单的实现: 50 | 51 | ```js 52 | var traverse = function(node) { 53 | node.doSomething(); // whatever work needs to be done 54 | node.childern.forEach(traverse); // many recursive calls 55 | }; 56 | ``` 57 | 58 | 每个节点只有两个子节点,时间复杂度和空间复杂度(在最坏的情况下,必须遍历整棵树才能找到答案)都将是 O(n2),因为每个将有两个递归调用。 每个节点有许多子代,复杂度将为 O(nm),其中 m 是子代数。 递归是树遍历的首选算法; while 循环会复杂得多,并且需要维护堆栈。 59 | 60 | 这样的指数增长意味着不需要很大的 tree 就可以抛出 RangeError 异常。肯定有更好的办法。 61 | 62 | ### 尾部调用消除 63 | 64 | 我们需要一种方法来消除每个递归调用的新堆栈帧的分配。 这被称为尾部调用消除。 65 | 66 | 使用尾部调用消除,当函数返回调用自身的结果时,语言实际上不会执行另一个函数调用。它把整件事都变成了一个循环。 67 | 68 | 好,那我们该怎么做呢? 有了惰性求值。 如果我们可以重写它来折叠一个延迟序列,这样函数返回一个值,或者它返回调用另一个函数的结果而不对该结果做任何处理,那么就不需要分配新的堆栈帧。 69 | 70 | 要将其放入“尾递归形式”,必须重写阶乘函数,以使内部过程事实在控制流中最后调用自身,如以下代码片段所示: 71 | 72 | ```js 73 | var factorial = function(n) { 74 | var _fact = function(x, n) { 75 | if (n == 0) { 76 | // base case 77 | return x; 78 | } else { 79 | // recursive case 80 | return _fact(n * x, n - 1); 81 | } 82 | }; 83 | return fact(1, n); 84 | }; 85 | ``` 86 | 87 | ::: tip 88 | 结果不是由递归尾中的第一个函数生成(如在 n _factorial(n-1)中),而是由递归尾中的最后一个函数生成(通过调用\_fact(r_ n,n-1) )),并由该尾部的最后一个函数产生(带有 return r;)。 计算仅向下进行一次,而不向上进行。 将其作为解释器的迭代进行处理相对容易。 89 | ::: 90 | 91 | 但是,消除尾部调用在 JavaScript 中不起作用。 将上面的代码放入您最喜欢的 JavaScript 引擎中,`factorial(24567)`仍返回 Uncaught Error:RangeError: Maximum call stack size exceeded exception(最大调用堆栈大小超出异常)。 Tail-call 消除被列为新功能,将包含在下一版 ECMAScript 中,但是所有浏览器都需要一段时间才能实现。 92 | 93 | 语言规范和运行时解释器的功能,简单明了。 它与解释器如何获取堆栈帧资源有关。 某些语言在不需要记住任何新内容时会重用同一堆栈框架,例如前面的函数。 这就是消除 Tail-call 的方法,从而减少了时间和空间的复杂性。 94 | 95 | 不幸的是,JavaScript 无法做到这一点。 但是,如果这样做,它将从此重组堆栈帧: 96 | 97 | ```txt 98 | call factorial (3) 99 | call fact (3 1) 100 | call fact (2 3) 101 | call fact (1 6) 102 | call fact (0 6) 103 | return 6 104 | return 6 105 | return 6 106 | return 6 107 | return 6 108 | ``` 109 | 110 | 具体如下: 111 | 112 | ```js 113 | call factorial (3) 114 | call fact (3 1) 115 | call fact (2 3) 116 | call fact (1 6) 117 | call fact (0 6) 118 | return 6 119 | return 6 120 | ``` 121 | 122 | ### 蹦床函数 123 | 124 | 解决方案? 一个被称为蹦床的过程。 这是通过使用 thunks 将“尾声消除”概念“破解”到程序中的一种方法。 125 | 126 | ::: tip 127 | 因此,Thunk 是带有参数的表达式,这些参数包装了没有自身参数的匿名函数。 例如:`function (str){return function() {console.log(str)}`。 这样可以防止在接收函数调用匿名函数之前对表达式进行求值。 128 | ::: 129 | 130 | 蹦床是一个函数,它接受一个函数作为输入,并重复执行其返回值,直到返回函数以外的其他值。下面的代码片段显示了一个简单的实现: 131 | 132 | ```js 133 | var trampoline = function(f) { 134 | while (f && f instanceof Function) { 135 | f = f.apply(f.context, f.args); 136 | } 137 | return f; 138 | }; 139 | ``` 140 | 141 | 要真正实现 tail-call 消除,我们需要使用 thunks。 为此,我们可以使用 bind()函数,该函数使我们可以将 this 关键字分配给另一个对象的方法应用于一个对象。 在内部,它与 call 关键字相同,但已链接到方法并返回一个新的绑定函数。 bind()函数实际上执行部分应用程序,但实际上确实可以部分应用。 142 | 143 | 要真正实现尾部调用消除,我们需要使用 thunks。为此,我们可以使用 bind()函数,该函数允许我们将一个方法应用于一个对象,并将此关键字分配给另一个对象。在内部,它与 call 关键字相同,但它链接到方法并返回一个新的绑定函数。bind()函数实际上执行部分应用程序,尽管方式非常有限。 144 | 145 | ```js 146 | var factorial = function(n) { 147 | var _fact = function(x, n) { 148 | if (n == 0) { 149 | // base case 150 | return x; 151 | } else { 152 | // recursive case 153 | return _fact.bind(null, n * x, n - 1); 154 | } 155 | }; 156 | return trampoline(_fact.bind(null, 1, n)); 157 | }; 158 | ``` 159 | 160 | 但是,编写 fact.bind(null,...)方法很麻烦,并且会使代码阅读困难。 取而代之的是,让我们编写自己的函数来创建 thunk(), 函数做以下几件事: 161 | 162 | - thunk()函数必须模拟\_fact.bind(null,n \* x,n-1)方法返回一个未求值的函数 163 | - thunk()函数应包含另外两个函数: 164 | - 用于处理给定函数,以及 165 | - 用于处理调用给定函数时将使用的函数参数 166 | 167 | 这样,我们就可以编写函数了。我们只需要几行代码就可以编写它。 168 | 169 | ```js 170 | var thunk = function(fn) { 171 | return function() { 172 | var args = Array.prototype.slice.apply(arguments); 173 | return function() { 174 | return fn.apply(this, args); 175 | }; 176 | }; 177 | }; 178 | ``` 179 | 180 | 现在我们可以在阶乘算法中使用 thunk()函数,如下所示: 181 | 182 | ```js 183 | var factorial = function(n) { 184 | var fact = function(x, n) { 185 | if (n == 0) { 186 | return x; 187 | } else { 188 | return thunk(fact)(n * x, n - 1); 189 | } 190 | }; 191 | return trampoline(thunk(fact)(1, n)); 192 | }; 193 | ``` 194 | 195 | 另外,我们可以通过将\_fact()函数定义为 thunk()函数来进一步简化它。通过将内部函数定义为 thunk()函数,我们就不用在内部函数定义和 return 语句中都使用 thunk()函数了。 196 | 197 | ```js 198 | var factorial = function(n) { 199 | var _fact = thunk(function(x, n) { 200 | if (n == 0) { 201 | // base case 202 | return x; 203 | } else { 204 | // recursive case 205 | return _fact(n * x, n - 1); 206 | } 207 | }); 208 | return trampoline(_fact(1, n)); 209 | }; 210 | ``` 211 | 212 | 结果令人满意。 对于无尾递归,递归调用的函数\_fact()几乎透明地作为迭代处理 213 | 214 | 最后,让我们看看 trampoline()和 thunk()函数如何与我们更有意义的树遍历示例一起工作。下面案例说明如何使用 trampolining 和 thunk 来遍历数据树: 215 | 216 | ```js 217 | var treeTraverse = function(trunk) { 218 | var _traverse = thunk(function(node) { 219 | node.doSomething(); 220 | node.children.forEach(_traverse); 221 | } 222 | 223 | trampoline(_traverse(trunk)); 224 | } 225 | ``` 226 | 227 | 我们已经解决了尾部递归的问题。但还有更好的办法吗?如果我们可以简单地将递归函数转换为非递归函数呢?下一节,我们看看如何做。 228 | 229 | ## Y-Combinator 推导 230 | 231 | 在计算机科学中,Y-combinator 推导甚至让编程大师们感到震惊。它将递归函数自动转换为非递归函数的能力,这就是为什么 Douglas Crockford 将其称为“计算机科学中最奇怪、最奇妙的产物之一”的原因,而 Sussman 和 Steele 曾经说过,“这种方法真的很了不起”。 232 | 233 | 264/5000 234 | 因此,将递归功能带到膝盖的计算机科学的真正卓越,奇特的奇特产物必须庞大而复杂,对吗? 不,不完全是。 它在 JavaScript 中的实现只有九行,非常奇怪。 它们如下: 235 | 236 | 它在 JavaScript 中的实现只有九行的代码。具体如下: 237 | 238 | ```js 239 | var Y = function(F) { 240 | return (function(f) { 241 | return f(f); 242 | })(function(f) { 243 | return F(function(x) { 244 | return f(f)(x); 245 | }); 246 | }); 247 | }; 248 | ``` 249 | 250 | 它的工作原理如下:它找到作为参数传入的函数的“固定点”。定点提供了另一种考虑功能的方法,而不是计算机编程理论中的递归和迭代。它仅通过使用匿名函数表达式,函数应用程序和变量引用来完成此操作。这里注意,Y 并没有引用它自己。实际上,所有这些都是匿名函数。([知乎:函数式编程的 Y Combinator 有哪些实用价值?](https://www.zhihu.com/question/20115649)) 251 | 252 | 正如你可能已经猜到的,Y-combinator 来自 lambda 表达式。它实际上是在另一个叫做 U-combinator 的组合器的帮助下导出的。组合器是一种特殊的高阶函数,它只使用函数应用程序和早期定义的组合器来定义输入的结果。 253 | 254 | 为了演示 Y-combinator,我们将再次讨论阶乘问题,但是我们需要对阶乘函数的定义稍有不同。我们编写的函数不是递归函数,该函数是阶乘的数学定义。 然后,我们可以将其传递给 Y-combinator。 255 | 256 | ```js 257 | var FactorialGen = function(factorial) { 258 | return (function(n) { 259 | if (n == 0) { 260 | // base case 261 | return 1; 262 | } else { 263 | // recursive case 264 | return n * factorial(n – 1); 265 | } 266 | }); 267 | }; 268 | Factorial = Y(FactorialGen); 269 | Factorial(10); // 3628800 270 | ``` 271 | 272 | 但是,当我们给它一个很大的数字时,堆栈会溢出,就像使用了没有 trampolining 函数(蹦床函数)的尾递归一样。 273 | 274 | ```js 275 | Factorial(23456); // RangeError: Maximum call stack size exceeded 276 | ``` 277 | 278 | 但是我们可以将 Y-combinator 用于蹦床函数,如下所示: 279 | 280 | ```js 281 | var FactorialGen2 = function(factorial) { 282 | return function(n) { 283 | var factorial = thunk(function(x, n) { 284 | if (n == 0) { 285 | return x; 286 | } else { 287 | return factorial(n * x, n - 1); 288 | } 289 | }); 290 | return trampoline(factorial(1, n)); 291 | }; 292 | }; 293 | var Factorial2 = Y(FactorialGen2); 294 | Factorial2(10); // 3628800 295 | Factorial2(23456); // Infinity 296 | ``` 297 | 298 | 我们还可以重新排列 Y-combinator 来执行一种叫做 Memoization 的操作。 299 | 300 | ## Memoization 301 | 302 | Memoization 是 JavaScript 中的一种技术,通过缓存结果并在下一个操作中重新使用缓存来加速查找费时的操作。 303 | 304 | 尽管 Y 组合器比递归快得多,但它仍然相对较慢。 为了加快速度,我们可以创建一个记忆定点组合器:类似 Y 的组合器,用于缓存中间函数调用的结果。 305 | 306 | 尽管 Y-combinator 运算比递归运算快得多,但它仍然相对较慢。为了加快速度,我们可以创建一个 Memoization 优化组合:一个类似 Y-combinator 组合器,用于缓存中间函数调用的结果。 307 | 308 | ```js 309 | var Ymem = function(F, cache) { 310 | if (!cache) { 311 | cache = {}; // Create a new cache. 312 | } 313 | return function(arg) { 314 | if (cache[arg]) { 315 | // Answer in cache 316 | return cache[arg]; 317 | } 318 | // else compute the answer 319 | var answer = F(function(n) { 320 | return Ymem(F, cache)(n); 321 | })(arg); // Compute the answer. 322 | cache[arg] = answer; // Cache the answer. 323 | return answer; 324 | }; 325 | }; 326 | ``` 327 | 328 | 那么要快多少呢?通过使用[http://jsperf.com/](http://jsperf.com/),我们可以比较性能。 329 | 330 | 以下结果是 1 到 100 之间的随机数。我们可以看到,记忆的(memoizing) Y-combinator 快得多。 并且向其添加蹦床函数不会使它减慢太多。 您可以在以下 URL 上查看结果并自己运行测试:[http://jsperf.com/memoizing-y-combinator-vs-tail-calloptimization/7](http://jsperf.com/memoizing-y-combinator-vs-tail-calloptimization/7)。 331 | 332 | ![img](https://blog.ahthw.com/wp-content/uploads/2019/12/y-combinator.png) 333 | 334 | 底线是:在 JavaScript 中执行递归的最安全有效的方法是通过蹦床函数和 thunk 来使用带有 Tail-call 消除的 memoization Y-combinator 组合器。 335 | 336 | ## 变量作用域 337 | 338 | JavaScript 中变量作用域不是既定的,有人说 JavaScript 程序员可以通过对代码的理解程度来判断其作用域。 339 | 340 | ## 域范围 341 | 342 | 让我们讨论一下 JavaScript 中的不同域解析,JavaScript 使用作用域链来建立变量的作用域。 解析变量时,它从最内部的域开始并向外搜寻。 343 | 344 | ## 全局作用域 345 | 346 | 在此级别定义的变量,函数和对象可用于整个程序中的任何代码。 这是最外部的作用域。 347 | 348 | ## 局部作用域 349 | 350 | 每个函数都有自己的局部作用域。在另一个函数中定义的任何函数都具有链接到外部函数的嵌套局部作用域,几乎总是由源中的位置定义范围。 351 | 352 | ```js 353 | var x = "hi"; 354 | function a() { 355 | console.log(x); 356 | } 357 | function b() { 358 | var x = "hello"; 359 | console.log(x); 360 | } 361 | b(); // hello 362 | a(); // hi 363 | ``` 364 | 365 | 局部作用域仅适用于函数,不适用于任何表达式语句(if, for, while 等),这与大多数语言对待作用域的方式不同。 366 | 367 | ```js 368 | function c() { 369 | var y = "greetings"; 370 | if (true) { 371 | var y = "guten tag"; 372 | } 373 | console.log(y); 374 | } 375 | function d() { 376 | var y = "greetings"; 377 | function e() { 378 | var y = "guten tag"; 379 | } 380 | console.log(y); 381 | } 382 | c(); // 'guten tag' 383 | d(); // 'greetings' 384 | ``` 385 | 386 | 在函数式编程中,这并不是什么大问题,因为函数的使用频率更高,而表达式语句的使用频率更低。 例如: 387 | 388 | ```js 389 | function e(){ 390 | var z = 'namaste'; 391 | [1,2,3].foreach(function(n) { 392 | var z = 'aloha'; 393 | } 394 | isTrue(function(){ 395 | var z = 'good morning'; 396 | }); 397 | console.log(z); 398 | } 399 | e(); // 'namaste' 400 | ``` 401 | 402 | ## 对象属性 403 | 404 | 对象属性也有自己的作用域链。 405 | 406 | ```js 407 | var x = "hi"; 408 | var obj = function() { 409 | this.x = "hola"; 410 | }; 411 | var foo = new obj(); 412 | console.log(foo.x); // 'hola' 413 | foo.x = "bonjour"; 414 | console.log(foo.x); // 'bonjour' 415 | ``` 416 | 417 | 并且对象的原型在作用域链的下游。 418 | 419 | ```js 420 | obj.prototype.x = "greetings"; 421 | obj.prototype.y = "konnichi ha"; 422 | var bar = new obj(); 423 | console.log(bar.x); // still prints 'hola' 424 | console.log(bar.y); // 'konnichi ha' 425 | ``` 426 | 427 | ## 闭包 428 | 429 | 这种作用域结构的一个问题是它没有空间容纳私有变量。如以下代码段: 430 | 431 | ```js 432 | var name = "Ford Focus"; 433 | var year = "2006"; 434 | var millage = 123456; 435 | function getMillage() { 436 | return millage; 437 | } 438 | function updateMillage(n) { 439 | millage = n; 440 | } 441 | ``` 442 | 443 | 这些变量和函数是全局的,这意味着后面的代码很容易意外覆盖它们。 一种解决方案是将它们封装到一个函数中,并在定义它后立即调用该函数。 444 | 445 | ```js 446 | var car = (function() { 447 | var name = "Ford Focus"; 448 | var year = "2006"; 449 | var millage = 123456; 450 | function getMillage() { 451 | return Millage; 452 | } 453 | function updateMillage(n) { 454 | millage = n; 455 | } 456 | })(); 457 | ``` 458 | 459 | 函数之外什么都没有发生,因此我们应该通过使匿名函数来丢弃它。 460 | 461 | ```js 462 | (function() { 463 | var name = "Ford Focus"; 464 | var year = "2006"; 465 | var millage = 123456; 466 | function getMillage() { 467 | return millage; 468 | } 469 | function updateMillage(n) { 470 | millage = n; 471 | } 472 | })(); 473 | ``` 474 | 475 | 为了使函数 getValue()和 updateMillage()在匿名函数之外可用,我们需要以对象文本形式返回它们,如以下代码片段所示: 476 | 477 | ```js 478 | var car = (function() { 479 | var name = "Ford Focus"; 480 | var year = "2006"; 481 | var millage = 123456; 482 | return { 483 | getMillage: function() { 484 | return millage; 485 | }, 486 | updateMillage: function(n) { 487 | millage = n; 488 | } 489 | }; 490 | })(); 491 | console.log(car.getMillage()); // works 492 | console.log(car.updateMillage(n)); // also works 493 | console.log(car.millage); // undefined 494 | ``` 495 | 496 | 以上方法我们为我们提供了伪私有变量,但问题并不止于此。下一节将探讨 JavaScript 中变量作用域的更多问题。 497 | 498 | ## 一些问题 499 | 500 | 在 JavaScript 中可以找到许多可变范围的细微差别。以下并非一份全面的清单,但涵盖了最常见的情况: 501 | 502 | - 以下将输出 4,而不是预期的'undefined':`for (var n = 4; false; ) { } console.log(n);`这是因为在 JavaScript 中,变量定义发生在相应作用域的开头,而不仅仅是在声明时。 503 | - 如果在外部作用域中定义了一个变量,然后让 If 语句在函数内部使用相同的名称定义一个变量,即使没有到达分支,也会重新定义它。例如: 504 | 505 | ```js 506 | var x = 1; 507 | function foo() { 508 | if (false) { 509 | var x = 2; 510 | } 511 | return x; 512 | } 513 | foo(); // Return value: 'undefined', expected return value:2; 514 | // 同样,这是由于将变量定义移至未定义值的范围的开头而引起的。 515 | ``` 516 | 517 | - 在浏览器中,全局变量实际上存储在 window 对象中。 518 | 519 | ```js 520 | window.a = 19; 521 | console.log(a); // Output: 19 522 | ``` 523 | 524 | 全局范围内的 a 表示 a 作为当前上下文的属性,因此 a === this.a 和浏览器中的 window 对象等效于全局范围内 this 关键字。 525 | 526 | 前两个示例是 JavaScript 功能(称为提升)的结果,这将成为下一部分有关编写函数的关键概念。 527 | 528 | ## 函数声明 vs 函数表达式 vs 函数构造函数 529 | 530 | 这三个语句有什么区别? 531 | 532 | ```js 533 | function foo(n) { 534 | return n; 535 | } 536 | var foo = function(n) { 537 | return n; 538 | }; 539 | var foo = new Function("n", "return n"); 540 | ``` 541 | 542 | 乍一看,它们只是编写同一函数的不同方式。 但是这里还有更多事情要做。 而且,如果我们要充分利用 JavaScript 中的功能,以便将其操纵为功能编程风格,那么我们最好能够做到这一点。 如果有更好的方法可以在计算机编程中做某事,那么该方法应该是唯一的方法。 543 | 544 | ## 函数声明 545 | 546 | 函数声明(有时也称为函数语句)通过使用 function 关键字定义函数。 547 | 548 | ```js 549 | function foo(n) { 550 | return n; 551 | } 552 | ``` 553 | 554 | 使用此语法声明的函数将提升到当前作用域的顶部。 这实际上意味着的是,即使将函数定义了几行,JavaScript 也会知道它,并且可以在范围内更早地使用它。 例如,以下将方法正确打印数字 6: 555 | 556 | ```js 557 | foo(2, 3); 558 | function foo(n, m) { 559 | console.log(n * m); 560 | } 561 | ``` 562 | 563 | ### 函数表达式 564 | 565 | 通过定义匿名函数并将其分配给变量,命名函数也可以定义为表达式。 566 | 567 | ```js 568 | var bar = function(n, m) { 569 | console.log(n * m); 570 | }; 571 | ``` 572 | 573 | 它们不像函数声明那样被提升。这是因为,在提升函数声明时,变量声明不会。例如,这将不起作用并引发错误: 574 | 575 | ```js 576 | bar(2, 3); 577 | var bar = function(n, m) { 578 | console.log(n * m); 579 | }; 580 | ``` 581 | 582 | 在函数式编程中,我们将要使用函数表达式,以便将函数视为变量,使它们可用作回调和高阶函数(例如 map()函数)的参数。 将函数定义为表达式使它们更明显地成为分配给函数的变量。 另外,如果我们要以一种分格编写函数,为了一致性和清晰性,我们应该用这种样式编写所有函数。 583 | 584 | ## Function()构造函数 585 | 586 | JavaScript 实际上还有第三种创建函数的方法:使用 Function()构造函数。 就像函数表达式一样,不会悬挂使用 Function()构造函数定义的函数。 587 | 588 | ```js 589 | var func = new Function("n", "m", "return n+m"); 590 | func(2, 3); // returns 5 591 | ``` 592 | 593 | 但是 Function()构造函数不仅令人困惑,而且非常危险。 无法进行语法校正,无法进行优化。 编写相同的函数,如下所示更加容易,安全和避免混淆: 594 | 595 | ```js 596 | var func = function(n, m) { 597 | return n + m; 598 | }; 599 | func(2, 3); // returns 5 600 | ``` 601 | 602 | ## 不可预测的行为 603 | 604 | 所以不同的是,函数声明是提升的,而函数表达式不是。这会导致意想不到的事情发生。请考虑以下几点: 605 | 606 | ```js 607 | function foo() { 608 | return "hi"; 609 | } 610 | console.log(foo()); 611 | function foo() { 612 | return "hello"; 613 | } 614 | ``` 615 | 616 | 实际打印到控制台的是hello。这是因为foo()函数的第二个定义被提升到了顶部,成为JavaScript解释器实际使用的声明。 617 | 618 | 乍一看,这似乎并不是关键的区别,但在函数式编程中,这可能会造成混乱。 考虑以下代码片段: 619 | 620 | ```js 621 | if (true) { 622 | function foo() { 623 | console.log("one"); 624 | } 625 | } else { 626 | function foo() { 627 | console.log("two"); 628 | } 629 | } 630 | foo(); 631 | ``` 632 | 633 | 当调用foo()函数时,two被打印到控制台,而不是one! 634 | 635 | 最后,还有一种方法可以将函数表达式和声明结合起来: 636 | 637 | ```js 638 | var foo = function bar(){ console.log('hi'); }; 639 | foo(); // 'hi' 640 | bar(); // Error: bar is not defined 641 | ``` 642 | 643 | 这种方式的使用方法没有什么意义,因为声明中使用的名称(前例中的bar()函数)在函数外部不可用,并导致混淆。它只适用于递归,例如: 644 | 645 | ```js 646 | var foo = function factorial(n) { 647 | if (n == 0) { 648 | return 1; 649 | } else { 650 | return n * factorial(n - 1); 651 | } 652 | }; 653 | foo(5); 654 | ``` 655 | 656 | ## 小结 657 | 658 | JavaScript被称为“web的汇编语言”,因为它和x86汇编一样无处不在,不可避免。它是所有浏览器上唯一运行的语言。它也有缺陷,但把它作为一种低级语言来指是没有意义的。 659 | 660 | 相反,可以将JavaScript看作是web的原始咖啡豆。当然,有些豆子坏了,有些烂了。但是,如果好的咖啡豆是由一位熟练的咖啡师挑选、烘焙和酿造的,那么这些咖啡豆就可以变成一种绝妙的果酱,而这种果酱不可能只吃一次就被遗忘。它的消费成为一种日常习惯,没有它的生活将是静止的,更难回顾,更不令人兴奋。有些人甚至更喜欢使用插件和诸如奶油、糖和可可之类的附加组件来增强啤酒的质量,这些插件和附加组件可以很好地补充啤酒的质量。 661 | 662 | 引用JavaScript的最大批评家之一Douglas Crawford说:“肯定有很多人拒绝考虑JavaScript有没有可能做对任何事情。我以前也是那种人。但现在我仍然对那里的辉煌感到惊讶。 663 | 664 | JavaScript真是太棒了。 665 | -------------------------------------------------------------------------------- /docs/book/chapter-third.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 我们是否需要了解高级数学(类别理论,Lambda微积分,多态性),才能使用函数式编程编写应用程序? 我们需要重新造轮子吗? 毋庸置疑,不需要。 4 | 5 | 在本章中,我们将调研影响我们用JavaScript编写函数式编程方式的各个方面。 6 | 7 | - 库 Libraries 8 | - 工具包 Toolkits 9 | - 开发环境 Development environments 10 | - 编译为函数式编程的JavaScript特性 Functional language that compiles to JavaScript 11 | - 其它 12 | 13 | 首先明确,JavaScript基础库的当前状况是非常不稳定的。 像其它编程语言一样,开发者社区对语言的维护在周期性变化。 可以采用新的基础库,随时放弃旧库。 例如,在本书撰写过程中,其开源社区已为I/O提供了流行且稳定的Node.js平台,它的未来还是不确定的。 14 | 15 | 因此,从本章中最重要的概念不是如何使用当前的库进行函数编程,而是如何使用任何增强JavaScript函数编程方法的库。 本章将以探索JavaScript中存在的所有多种风格的函数式编程为目标,探索尽可能多的库。 16 | 17 | ## JavaScript的函数式编程库 18 | 19 | 每个程序员都编写自己的函数库,而函数式JavaScript程序员也不例外。 使用当今的开源代码托管平台(例如GitHub,Bower和NPM),可以更轻松地共享,协作和扩展这些库。 存在许多用于使用JavaScript进行功能编程的库,范围从微型工具包到整体式模块库。 20 | 21 | 每个库都推广自己的函数式编程风格。 从严格的,基于数学的风格到宽松的,非正式的样式,每个库都是不同的,但是它们都具有一个共同的特征:它们都具有抽象的JavaScript功能,以提高代码的重用性,可读性和健壮性。 22 | 23 | 在撰写本文时,单个库尚未定为为官方js标准。 有人可能会说underscore.js是其中之一,但是,正如您将在下一节中看到的那样,建议避免使用underscore.js。 24 | 25 | ## Underscore.js 26 | 27 | 在许多人眼中,underscore已成为标准的功能性JavaScript库。 它是成熟,稳定的,由Backbone.js和CoffeeScript库创建者Jeremy Ashkenas开发。 underscore实际上是Ruby的Enumerable模块的重新实现,这解释了为什么CoffeeScript也受Ruby影响。 28 | 29 | 与jQuery相似,underscore没有扩展任何 JavaScript 内置对象,而是使用符号定义其自己的对象:下划线字符“ _”。 因此,使用underscore.js可以像这样书书写: 30 | 31 | ```js 32 | var x = _.map([1, 2, 3], Math.sqrt); // Underscore's map function 33 | console.log(x.toString()); 34 | ``` 35 | 36 | JavaScrip Array对象的原生map()方法,其工作方式如下: 37 | 38 | ```js 39 | var x = [1, 2, 3].map(Math.sqrt); 40 | ``` 41 | 42 | 区别在于,在underscore中,将Array对象和callback()函数作为参数传递给underscore对象的map()方法(_.map),而不是仅将回调传递给数组的原生map()方法(Array.prototype.map)。 43 | 44 | 除了map()和其他内置函数之外,underscore还有其他方法来加强。它有很多非常方便的函数,比如find()、invoke()、cluck()、sortyBy()、groupBy()等等。 45 | 46 | ```js 47 | var greetings = [ 48 | { origin: "spanish", value: "hola" }, 49 | { origin: "english", value: "hello" } 50 | ]; 51 | console.log(_.pluck(greetings, "value")); 52 | // Grabs an object's property. 53 | // Returns: ['hola', 'hello'] 54 | console.log( 55 | _.find(greetings, function(s) { 56 | return s.origin == "spanish"; 57 | }) 58 | ); 59 | // Looks for the first obj that passes the truth test 60 | // Returns: {origin: 'spanish', value: 'hola'} 61 | greetings = greetings.concat( 62 | _.object(["origin", "value"], ["french", "bonjour"]) 63 | ); 64 | console.log(greetings); 65 | // _.object creates an object literal from two merged arrays 66 | // Returns: [{origin: 'spanish', value: 'hola'}, 67 | //{origin: 'english', value: 'hello'}, 68 | //{origin: 'french', value: 'bonjour'}] 69 | ``` 70 | 71 | underscore提供了一种将方法链接在一起的方法: 72 | 73 | ```js 74 | var g = _.chain(greetings) 75 | .sortBy(function(x) { 76 | return x.value.length; 77 | }) 78 | .pluck("origin") 79 | .map(function(x) { 80 | return x.charAt(0).toUpperCase() + x.slice(1); 81 | }) 82 | .reduce(function(x, y) { 83 | return x + " " + y; 84 | }, "") 85 | .value(); 86 | // Applies the functions 87 | // Returns: 'Spanish English French' 88 | console.log(g); 89 | ``` 90 | 91 | _.chain() 方法返回包含所有underscore函数的包装对象。然后使用值方法来提取包装对象的值。包裹对象对于将underscore与面向对象编程混合也是非常有用的。 92 | 93 | 尽管开发者社区代码易于使用,但underscore.js库却因编写过于冗长的代码并鼓励错误的模式而受到批评。 underscore的结构可能不理想,甚至功能不全! 94 | 95 | 在Brian Bons Lonsdorf的演讲youtube视频中“Hey underscore! you're doing it wrong!”中,underscore1.7.0版本中明确给我们我们扩展诸如map(),reduce(),filter()和更多带来阻力。 96 | 97 | > 视频:[www.youtube.com/watch?v=m3svKOdZij](https://www.youtube.com/watch?v=m3svKOdZij) 98 | 99 | ```js 100 | _.prototype.map = function(obj, iterate, [context]) { 101 | if (Array.prototype.map && obj.map === Array.prototype.map) 102 | return obj.map(iterate, context); 103 | // ... 104 | }; 105 | ``` 106 | 107 | 就范畴理论而言,map()是一个同态函子接口(在第5章范畴理论中对此有更多介绍)。而且我们应该能够将map定义为所需的函子。 因此,这并不是underscore的功能。 108 | 109 | 而且由于JavaScript没有内置的不可变数据,因此libraries库应谨慎使用,以免其辅助函数改变传递给它的对象。 下面展示了此问题的一个很好的例子。 该代码段的目的是返回一个新的selected列表,并将一个选项设置为默认选项。 但是实际发生的是selected列表在适当位置发生了变化。 110 | 111 | ```js 112 | function getSelectedOptions(id, value) { 113 | options = document.querySelectorAll("#" + id + " option"); 114 | var newOptions = _.map(options, function(opt) { 115 | if (opt.text == value) { 116 | opt.selected = true; 117 | opt.text += " (this is the default)"; 118 | } else { 119 | opt.selected = false; 120 | } 121 | return opt; 122 | }); 123 | return newOptions; 124 | } 125 | var optionsHelp = getSelectedOptions("timezones", "Chicago"); 126 | ``` 127 | 128 | 我们将不得不插入`opt = opt.cloneNode()`到`callback()`函数,以复制要传递给该函数的列表中的每个对象。 underscore的map()函数为提高性能,但这是以功能缺失付出代价的。 原生Array.prototype.map()函数不需要此功能,因为它可以进行复制,但也不适用于nodelist列表集合。 129 | 130 | 对于数学上正确的函数式编程,underscore可能不太理想,但是它也从来没有打算将JavaScript扩展或转换为纯函数式语言。 它将自己定义为一个JavaScript库,该库提供了许多有用的功能编程tips。 它可能只是类似的功能或functional-like的集合,也不是严肃的功能库。 131 | 132 | 有没有更好的库? 也许是一种基于数学的方法? 133 | 134 | ## Fantasy Land 135 | 136 | [Fantasy Land 简介](https://github.com/fantasyland/fantasy-land):JavaScript中常见代数结构的互操作性规范 137 | 138 | Fantasy Land是功能库的集合,以及有关如何在JavaScript中实现“代数结构”的正式规范。 更具体地说,Fantasy Land指定了常见代数结构或简称代数的互操作性:monads,monoid,setoid,functors,chains,等。 他们的名字听起来很吓人,但它们只是一组值,一组运算符以及必须遵守的一些规则。 换句话说,它们只是对象。 139 | 140 | 运作方式如下。 每个代数都是一个单独的Fantasy Land规范,并且可能依赖于需要实现的其他代数。 141 | 142 | ![fantasy-land](https://blog.ahthw.com/wp-content/uploads/2019/12/fantasy-land.png) 143 | 144 | 一些代数规范包括: 145 | 146 | - Setoids: 147 | - 实现自反性、对称性和传递性 148 | - 定义equals()方法 149 | - Semigroups 150 | - 实现联想法 151 | - 定义concat()方法 152 | - Monoid 153 | - 实现right identity, left identity 154 | -定义empty()方法 155 | - Functor 156 | - 实现identity 和 composition规则 157 | - 实现map()方法 158 | 159 | 我们不一定需要确切知道每个代数的用途,但是它一定会有所帮助,特别是如果您正在编写自己的符合规范的库。 它不只是抽象的废话,它概述了一种实现称为类别理论的高级抽象的方法。 类别理论的完整解释可以在第5章类别理论中找到。 160 | 161 | Fantasy Land不仅告诉我们如何实现函数式编程,还提供了一组JavaScript函数模块。 但是,文档少且不完整,文档很少。 其实,Fantasy Land并不是唯一实现其开源规范的库。 其他的也有,即:Bilby.js。 162 | 163 | ## Bilby.js 164 | 165 | [Bilby.js 简介](https://github.com/puffnfresh/bilby.js):面向JavaScript的严肃函数式编程库。 166 | 167 | Bilby到底是什么? 看logo像老鼠又像兔子,比较怪异。 但是,bilib.js库符合Fantasy Land规范。 168 | 169 | 正如其文档所述,它应用类别理论来实现高度抽象的代码功能性,这意味着它启用了引用透明的程序。更多说明可以看它的[文档](http://bilby.brianmckenna.org/)。 170 | 171 | - 多种ad-hoc多态性的不可变多方法 172 | - 功能数据结构 173 | - 函数语法的运算符重载 174 | - 自动化规范测试(ScalaCheck、QuickCheck) 175 | 176 | 到目前为止,Bilby.js是最成熟的库,它符合Fantasy Land的代数结构规范,它是完全致力于函数式编程方式的绝佳资源。 177 | 178 | 举个例子: 179 | 180 | ```js 181 | // environments in bilby are immutable structure for multimethods 182 | var shapes1 = bilby 183 | .environment() 184 | // can define methods 185 | .method( 186 | "area", // methods take a name 187 | function(a) { 188 | return typeof a == "rect"; 189 | }, // a predicate 190 | function(a) { 191 | return a.x * a.y; 192 | } // and an implementation 193 | ) 194 | // and properties, like methods with predicates that always 195 | // return true 196 | .property( 197 | "name", // takes a name 198 | "shape" 199 | ); // and a function 200 | // now we can overload it 201 | var shapes2 = shapes1.method( 202 | "area", 203 | function(a) { 204 | return typeof a == "circle"; 205 | }, 206 | function(a) { 207 | return a.r * a.r * Math.PI; 208 | } 209 | ); 210 | var shapes3 = shapes2.method( 211 | "area", 212 | function(a) { 213 | return typeof a == "triangle"; 214 | }, 215 | function(a) { 216 | return (a.height * a.base) / 2; 217 | } 218 | ); 219 | // and now we can do something like this 220 | var objs = [ 221 | { type: "circle", r: 5 }, 222 | { type: "rect", x: 2, y: 3 } 223 | ]; 224 | var areas = objs.map(shapes3.area); 225 | // and this 226 | var totalArea = objs.map(shapes3.area).reduce(add); 227 | ``` 228 | 229 | 这就是范畴理论和ad-hoc多态性的作用。再次,范畴理论将在第5章:范畴理论中全面介绍。 230 | 231 | > 类别理论是数学的一个最近活跃的分支,功能程序员使用它来最大化其代码的抽象性和实用性。 但是有一个主要缺点:很难概念化并快速入门。 232 | 233 | 事实是,Bilby和Fantasy Land确实实现了在扩展JavaScript函数编程的可能性。 尽管发展令人兴奋,但业界可能还没有为Bibly和Fantasy Land所推动的那种硬核功能风格做好准备。 234 | 235 | 如此功能强大的JavaScript上如此宏伟的库也许不是我们要做的。 毕竟,我们着手探索补充JavaScript的功能技术,而不是建立功能性编程条款。 让我们将注意力转向另Lazy.js。 236 | 237 | ## Lazy.js 238 | 239 | [lazy.js 简介](https://github.com/dtao/lazy.js):Like Underscore, but lazier. 240 | 241 | Lazy.js是一个更实用的工具库,与underscore.js库类似,但具有惰性求值策略。 因此,Lazy通过功能上计算无法立即解释的序列结果来使不可能成为可能,具有显着的性能提升。 242 | 243 | Lazy.js库很年轻。 但是它背后蕴藏着巨大的发展动力和代码社区热情。 244 | 245 | 在Lazy中思想是:所有的东西都是一个序列,我们可以迭代。由于库控制方法应用顺序的方式,可以实现许多真正酷的事情:异步迭代(并行编程)、无限序列、函数反应式编程等。 246 | 247 | 举个例子: 248 | 249 | ```js 250 | // Get the first eight lines of a song's lyrics 251 | var lyrics = "Lorem ipsum dolor sit amet, consectetur adipiscing eli"; 252 | // Without Lazy, the entire string is first split into lines 253 | console.log(lyrics.split("\n").slice(0, 3)); 254 | // With Lazy, the text is only split into the first 8 lines 255 | // The lyrics can even be infinitely long! 256 | console.log( 257 | Lazy(lyrics) 258 | .split("\n") 259 | .take(3) 260 | ); 261 | 262 | //First 10 squares that are evenly divisible by 3 263 | var oneTo1000 = Lazy.range(1, 1000).toArray(); 264 | var sequence = Lazy(oneTo1000) 265 | .map(function(x) { 266 | return x * x; 267 | }) 268 | .filter(function(x) { 269 | return x % 3 === 0; 270 | }) 271 | .take(10) 272 | .each(function(x) { 273 | console.log(x); 274 | }); 275 | // asynchronous iteration over an infinite sequence 276 | var asyncSequence = Lazy.generate(function(x) { 277 | return x++; 278 | }) 279 | .async(100) // 0.100s intervals between elements 280 | .take(20) // only compute the first 20 281 | .each(function(e) { 282 | // begin iterating over the sequence 283 | console.log(new Date().getMilliseconds() + ": " + e); 284 | }); 285 | ``` 286 | 287 | 第4章,在JavaScript中实现函数式编程技术中会介绍更多示例和用例。 288 | 289 | 但是,将Lazy.js库完全归功于此想法并不完全正确。 它的前身之一Bacon.js库的功能几乎相同。 290 | 291 | ## Bacon.js 292 | 293 | [Bacon.js简介](https://github.com/baconjs/bacon.js) :用于TypeScript和JavaScript的函数响应式编程库。 294 | 295 | ![Bacon.js](https://blog.ahthw.com/wp-content/uploads/2019/12/bacon.png) 296 | 297 | 函数式编程库的必备者,Bacon.js本身就是用于函数式反应式编程的库。 功能性反应式编程仅表示功能性设计模式用于表示反应性且始终在变化的值,例如鼠标在屏幕上的位置或价格 298 | 公司的股票。 与Lazy可以通过不计算直到需要的值来摆脱创建无限序列的方式一样,Bacon可以避免在最后一秒钟之前就不必计算不断变化的值。 299 | 300 | Bacon.js是函数式编程库的必备者,本身就是用于函响应式编程的库。 函数响应式编程仅仅意味着函数式设计模式被用来表示反应式且总是变化的值,例如鼠标在屏幕上的位置或公司股票的价格。就像Lazy可以通过在需要时才计算值来创建无限序列一样,Bacon可以避免在最后一秒之前计算不断变化的值。 301 | 302 | 在Lazy.js中被称为序列的序列在Bacon中称为EventStreams和Properties,因为它们更适合处理事件(onmouseover、onkeydown等)和反应性属性(滚动位置、鼠标位置、切换等)。 303 | 304 | ```js 305 | Bacon.fromEventTarget(document.body, "click").onValue(function() { 306 | alert("Bacon!"); 307 | }); 308 | ``` 309 | 310 | Bacon.js比Lazy.js体积稍大一点,但是它的功能集大约只有Lazy.js的一半,它的开发者社区热情也比较活跃。 311 | 312 | ## 其它提名 313 | 314 | 在本书的范围之内,实在太多库无法做到全部覆盖。 让我们看看更多用于JavaScript进行函数式编程的库。 315 | 316 | - Functional 317 | - 可能是JavaScript中第一个用于函数式编程的库,Functional是一个包含全面的高阶函数支持以及字符串lambda的库 318 | - wu.js 319 | - wu.js库尤其因其curryable()函数而倍受赞誉,它是一个非常不错的函数式编程库。 这是(我所知道的)第一个实现惰性计算的库,它使Bacon.js,Lazy.js和其他库的运行更加顺畅 320 | - 是的,它是以声名狼藉的说唱团体Wu Tang Clan命名的 321 | - sloth.js 322 | - 与Lazy.js库非常相似,但更小 323 | - stream.js 324 | - stream.js库支持无限流,而没有太多其他支持 325 | - 绝对小巧 326 | - lodash.js 327 | - 顾名思义,lodash.js库的灵感来自underscore.js库 328 | - 高度优化 329 | - Sugar 330 | - Sugar是JavaScript的函数式编程技术(如Underscore)的支持库,但是在实现方式上存在一些关键差异 331 | - 与其在underscore中执行`_.pluck(myObjs,'value')`,不如在Sugar中执行`myObjs.map('value')`。 这意味着它会修改JavaScript内置对象,因此存在很小的风险,即它不能与其他类似原型的库很好地配合使用 332 | - 非常好的文档、单元测试、分析工具等 333 | - from.js 334 | - 用于JavaScript的新功能库和LINQ(语言集成查询)引擎,支持.NET提供的大多数相同的LINQ函数 335 | - 100%惰性求值并支持lambda表达式 336 | - 诞生不久,但是文档非常棒 337 | - JSLINQ 338 | - 另一个用于JavaScript的函数式LINQ引擎 339 | - 比from.js library更老更成熟 340 | - Boiler.js 341 | - 另一个实用程序库,它将JavaScript的函数方法扩展到更多的原语:字符串、数字、对象、集合和数组 342 | - Folktale 343 | - 与Bilby.js库一样,Folktale是另一个实现Fantasy Land规范的新库。和它的前辈一样,也是一个JavaScript函数编程库的集合。它很年轻,但未来可期。 344 | - jQuery 345 | - 看到这里提到的jQuery感到惊讶吗?尽管jQuery不是用来执行函数式编程的工具,但它本身是函数式的。jQuery可能是最广泛使用的库之一,其根源在于函数式编程。 346 | - jQuery对象实际上是一个monad。jQuery使用一元法则来启用方法链接:`$('#mydiv').fadeIn().css('left': 50).alert('hi!');` 347 | 348 | 在第7章,JavaScript中的函数式和面向对象编程中可以找到对此的完整解释。 349 | 350 | - 它的一些方法是高阶的:`$('li').css('left': function(index){return index*50});` 351 | - 从jQuery 1.8开始,deferred.then参数实现了一个功能被称为Promises.的概念。 352 | - jQuery是一个抽象层,主要用于DOM。 它不是框架或工具包,而只是使用抽象来增加代码重用和减少原生丑陋代码的一种方式。 那不是函数式编程的全部内容吗? 353 | 354 | ## 搭建开发生产环境 355 | 356 | ### 环境 357 | 358 | 就编程风格而言,开发应用程序并将在其中部署哪种类型的环境无关紧要。但是,对于库来说,这确实很重要。 359 | 360 | ### 浏览器 361 | 362 | 大多数JavaScript应用程序都设计为在客户端(即客户端的浏览器)中运行。 基于浏览器的环境非常适合开发,因为浏览器无处不在,您可以直接在本地计算机上处理代码,解释器是浏览器的JavaScript引擎,并且所有浏览器都具有开发者控制台。 Firefox的FireBug提供了非常有用的错误消息,并提供了断点等功能,但是在Chrome和Safari中运行相同的代码来交叉引用错误输出通常会很有帮助。 甚至Internet Explorer也包含开发人员工具。 363 | 364 | 浏览器的问题在于它们对JavaScript的解释不同! 尽管不常见,但可以在不同的浏览器中编写返回不同结果的代码。 但是通常区别在于它们对待文档对象模型的方式,而不是原型和函数的工作方式。 显然,`Math.sqrt(4)`方法在所有浏览器和`shell`返回2。 但是`scrollLeft`方法取决于浏览器的布局策略。 365 | 366 | 编写特定于浏览器的代码是浪费时间,这是为什么要使用库的另一个原因。 367 | 368 | ### 服务器端JavaScript 369 | 370 | Node.js库已成为创建服务器端和基于网络的应用程序的标准平台。 函数式编程可以用于服务器端应用程序编程吗? 答案:是! 但是是否存在为此性能关键环境设计的功能库? 答案也是:是的。 371 | 372 | 本章概述的所有功能库都可以在Node.js库中使用,其中许多功能都依赖于browserify.js模块来处理浏览器元素。 373 | 374 | ### 服务器端环境中的功能用例 375 | 376 | 服务器端应用程序开发人员经常关心并发性。 经典示例是一个允许多个用户修改同一文件的应用程序。 但是,如果他们尝试同时修改它,您将陷入一团糟。 这是困扰数十年来程序员的状态问题的维持。 377 | 378 | 假定以下情况: 379 | 380 | 1. 一天早上,亚当打开了一份报告进行编辑,但他在午餐之前没有保存报告。 381 | 2. 比利打开相同的报告,添加他的笔记,然后将其保存。 382 | 3. 亚当从午餐回来后,将他的笔记添加到报告中,然后将其保存,在不知不觉中覆盖了比利的笔记。 383 | 4. 第二天,比利发现他的笔记丢失了。 老板对他大吼; 每个人都会生气,他们会误导那些失职的应用程序开发人员。 384 | 385 | 长期以来,解决此问题的方法是创建有关文件的状态。 当某人开始编辑锁定状态时,将其锁定为打开状态,这将阻止其他人对其进行编辑,然后在保存后将其切换为关闭状态。 在我们的情况下,直到亚当从午餐中回来之前,比利才能做他的工作。 而且,如果它从未保存过(例如,如果亚当决定在午休时间中途辞职),那么没人会对其进行编辑。 386 | 387 | 在这里,函数式编程关于不变数据和状态(或缺少状态)的思想可以真正发挥作用。 与其让用户直接使用功能性方法来修改文件,不如让他们修改文件的副本,这是一个新的修订版。 如果他们去保存修订,并且已经存在一个新修订,那么我们知道其他人已经修改了旧修订。 避免危机。 388 | 389 | 现在,之前的场景将像这样展开: 390 | 391 | 1. 一天早晨,亚当打开一份报告进行编辑。 但是他在午餐之前没有保存它。 392 | 2. 比利打开相同的报告,添加他的笔记,并将其另存为新修订。 393 | 3. 亚当午餐后回来添加笔记。 当他尝试保存新修订时,应用程序告诉他现在存在新修订。 394 | 4. 亚当打开新修订,添加注释,然后保存另一个新修订。 395 | 5. 通过查看修订历史记录,老板可以看到一切运行顺利。 每个人都很高兴,应用程序开发人员得到了晋升和加薪。 396 | 397 | 这称为事件源。 没有要维护的明确状态,只有事件。 该过程更加简洁,事件的清晰历史可以回顾。 398 | 399 | 这个想法以及其他许多想法是为什么服务器端环境中的函数式编程正在兴起的原因。 400 | 401 | ### CLI 402 | 403 | 尽管Web和node.js库是两个主要的JavaScript环境,但一些务实而又冒险的用户正在寻找在命令行中使用JavaScript的方法。 404 | 405 | 使用JavaScript作为命令行界面(CLI)脚本语言可能是应用函数编程的最佳机会之一。 想象一下,在搜索本地文件时可以使用惰性计算,也可以将整个bash脚本重写为功能齐全的JavaScript单行代码。 406 | 407 | ### 将功能库与JavaScript模块一起使用 408 | 409 | Web应用程序由各种各样的东西组成:框架,库,API等。 它们可以作为依赖项,插件或作为共存对象而彼此协同工作。 410 | 411 | - Backbone.js 412 | - 具有RESTful JSON接口的MVP(模型视图提供程序)框架 413 | - 需要underscore.js库,这是Backbone唯一的硬依赖性 414 | - jQuery 415 | - Bacon.js库具有用于与jQuery混合的绑定特性 416 | - underscore.js和jQuery很好地互补 417 | - Prototype JavaScript Framework 418 | - 以最接近Ruby可枚举的方式为JavaScript提供集合函数 419 | - Sugar.js 420 | - 修改JavaScript内置对象及其方法 421 | - 与其他库(尤其是原型库)混合使用时必须小心 422 | 423 | ### 编译成JavaScript的功能语言 424 | 425 | 有时候,在JavaScript的内部功能之上,类似C的语法的厚实外表足以让您想要切换到另一种函数式语言。 426 | 427 | - Clojure和ClojureScript 428 | - Closure是一种现代的Lisp实现和一种功能齐全的函数式语言 429 | - ClojureScript将Clojure转换为JavaScript 430 | - CoffeeScript 431 | - CoffeeScript是功能语言和用于将该语言反编译为JavaScript的编译器的名称 432 | - CoffeeScript中的表达式与JavaScript中的表达式之间的一对一映射 433 | 434 | 还有更多,包括Pyjs,Roy,TypeScript,UHC等。 435 | 436 | ## 小结 437 | 438 | 选择使用哪个库取决于你的需求。 需要响应式编程来处理事件和动态值吗? 使用Bacon.js库。 只需要无限的流,别无其他? 使用stream.js库。 想要用函数助手来补充jQuery吗? 试试underscore.js库。 是否需要一个结构化的环境来实现严肃的多态性? 使用bilby.js库。 需要功能完善的工具进行功能编程吗? 使用Lazy.js库。如果这些都不满足你的功能,可以尝试自己开发。 439 | 440 | 何库都只能使用它的方式。 尽管本章概述的一些库有一些缺陷 正确使用库并满足你的需要由你的具体场景决定。 441 | 442 | 而且,如果我们将代码库导入到我们的JavaScript环境中,那么也许我们也可以导入思想和原则。 也许我们可以通过蒂姆·彼得(Tim Peter)《python之禅》来了解: 443 | 444 | ```text 445 | Beautiful is better than ugly 446 | Explicit is better than implicit. 447 | Simple is better than complex. 448 | Complex is better than complicated. 449 | Flat is better than nested. 450 | Sparse is better than dense. 451 | Readability counts. 452 | Special cases aren't special enough to break the rules. 453 | Although practicality beats purity. 454 | Errors should never pass silently. 455 | Unless explicitly silenced. 456 | In the face of ambiguity, refuse the temptation to guess. 457 | There should be one—and preferably only one—obvious way to do it. 458 | Although that way may not be obvious at first unless you're Dutch. 459 | Now is better than never. 460 | Although never is often better than "right" now. 461 | If the implementation is hard to explain, it's a bad idea. 462 | If the implementation is easy to explain, it may be a good idea. 463 | Namespaces are one honking great idea—let's do more of those! 464 | 465 | 优美胜于丑陋(Python 以编写优美的代码为目标) 466 | 明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似) 467 | 简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现) 468 | 复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁) 469 | 扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套) 470 | 间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题) 471 | 可读性很重要(优美的代码是可读的) 472 | 即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上) 473 | 不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码) 474 | 当存在多种可能,不要尝试去猜测 475 | 而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法) 476 | 虽然这并不容易,因为你不是 Python 之父(这里的 Dutch 是指 Guido ) 477 | 做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量) 478 | 如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准) 479 | 命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召) 480 | ``` 481 | -------------------------------------------------------------------------------- /docs/book/contribution.md: -------------------------------------------------------------------------------- 1 | # 贡献内容 2 | 3 | 如果你想参与这本书的共同创作,修改或添加内容,可以先 [Fork](https://github.com/hex-translate/natpagle) 这本书的仓库,然后将修改的内容提交 [Pull requests](https://github.com/hex-translate/natpagle/pulls) ;或者创建 [Issues](https://github.com/hex-translate/natpagle/issues)。 4 | 5 | Fork 后的仓库如何同步本仓库? 6 | 7 | ```bash 8 | # 添加 upstream 源,只需执行一次 9 | git remote add upstream git@github.com:halldwang/natpagle.git 10 | 11 | # 拉取远程代码 12 | git pull upstream master 13 | 14 | # 提交修改 15 | git add . 16 | git commit 17 | 18 | # 更新 fork 仓库 19 | git push origin master 20 | ``` 21 | 22 | 更多参考: [Syncing a fork](https://help.github.com/articles/syncing-a-fork/) 23 | 24 | 注意,本书内容在 `/docs` 目录中, `/dist`是网站文件,通过脚本自动生成的。 25 | 26 | ## 生成电子书 27 | 28 | 这本书使用 [Vuepress](https://vuepress.vuejs.org/zh/) 撰写并生成[网站](https://github.com/hex-translate/natpagle),请查看 `package.json` 中的 `scripts` 配置和 `/scripts` 目录中的脚本来了解这本书的构建和发布过程。 29 | 30 | ```bash 31 | # 初始化 nodejs 依赖 32 | npm install 33 | 34 | # 安装 vuepress 插件 35 | npm install -g vuepress 36 | 37 | # 进入图书目录 38 | cd docs 39 | 40 | # 开始写作 41 | vuepress dev . 42 | 43 | # 构建静态文件 44 | vuepress build . 45 | 46 | # 查看写作内容 47 | # visit http://localhost:8080 48 | 49 | ``` 50 | 51 | ## 更新日志 52 | 53 | [https://github.com/hex-translate/natpagle/tree/master](https://github.com/hex-translate/natpagle/tree/master) 54 | -------------------------------------------------------------------------------- /docs/book/cover-preface.md: -------------------------------------------------------------------------------- 1 | # 封面 2 | 3 | ![cover](https://blog.ahthw.com/wp-content/uploads/2019/12/Functional_Programming_in_JavaScript.jpg) 4 | 5 | 发现JavaScript中的函数式编程能力,构建智能、简洁、可持续交付的web应用。 6 | 7 | -- Dan Mantyla 8 | 9 | ## 章节 10 | 11 | 函数式编程是一种代码的编程范式,可最大程度地减少复杂性并增加模块化。这是通过巧妙的变异,组合和使用函数的方式编写更简洁的代码的方式。 JavaScript 为这种方法提供了很好的媒介。 JavaScript 是一种脚本语言,也是一种功能性语言。通过学习如何将其作为一种功能性语言的特性体现出来,我们可以实现功能强大,易于维护且更可靠的 Web 应用程序。通过函数式编程,JavaScript 中的很多特殊的语言特性会突然变得清晰起来,并且整个语言将变得更加有活力。学习如何使用函数式编程将使你成为一名更好的程序员。 12 | 13 | 本书为有兴趣学习函数式编程的新手和有经验的前端开发人员提供了指南。本书聚焦于函数式编程技术,形式的发展以及有关 JavaScript 库的详细信息,将助你编写更高效的代码。 14 | 15 | 本书的内容: 16 | 17 | ## 第1章 18 | 19 | JavaScript功能方面的展示,通过在传统方法和功能编程的帮助下创建一个小型web应用程序来设定本书的进度。然后比较这两种方法来强调函数式编程的重要性。 20 | 21 | ## 第2章 22 | 23 | 函数式编程的基本原理,向您介绍函数式编程的核心概念以及内置的JavaScript函数。 24 | 25 | ## 第3章 26 | 27 | 建立函数式编程环境,探讨了不同的JavaScript库以及如何为函数式编程优化它们。 28 | 29 | ## 第4章 30 | 31 | 在JavaScript中实现函数式编程技术,介绍了JavaScript中的功能范例。它涵盖了几种类型的函数式编程并演示了如何在不同情况下使用它们。 32 | 33 | ## 第5章 34 | 35 | 范畴论,详细解释范畴论的概念,然后用JavaScript实现。 36 | 37 | ## 第6章 38 | 39 | JavaScript的高级主题和陷阱,重点介绍了您的各种缺点使用JavaScript进行编程时可能会遇到的问题以及成功的各种方法处理他们。 40 | 41 | ## 第7章 42 | 43 | 介绍了JavaScript中的函数式编程和面向对象的编程JavaScript进行功能和面向对象的编程,并向您展示如何两种范式可以互补,并存。 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "natpagle", 3 | "version": "1.0.0", 4 | "description": "JavaScript 函数式编程中文翻译", 5 | "main": "index.js", 6 | "scripts": { 7 | "docs:dev": "vuepress dev docs", 8 | "docs:build": "vuepress build docs" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/halldwang/natpagle.git" 13 | }, 14 | "author": "halldwang", 15 | "license": "Apache License", 16 | "bugs": { 17 | "url": "https://github.com/halldwang/natpagle/issues" 18 | }, 19 | "homepage": "https://github.com/halldwang/natpagle#readme" 20 | } 21 | --------------------------------------------------------------------------------