文章目录
-
- [1.3. JavaScript 是函数式编程语言吗?Is JavaScript functional?](#1.3. JavaScript 是函数式编程语言吗?Is JavaScript functional?)
-
- [1.3.1. 作为一种工具](#1.3.1. 作为一种工具)
- [1.3.2. 用 JavaScript 实现函数式编程](#1.3.2. 用 JavaScript 实现函数式编程)
- [1.3.3. 关键语言特性](#1.3.3. 关键语言特性)
-
- [1.3.3.1 函数作一等对象](#1.3.3.1 函数作一等对象)
- [1.3.3.2 递归](#1.3.3.2 递归)
- [1.3.3.3 闭包](#1.3.3.3 闭包)
- [1.3.3.4. 箭头函数](#1.3.3.4. 箭头函数)
- [1.3.3.5 展开运算符](#1.3.3.5 展开运算符)
1.3. JavaScript 是函数式编程语言吗?Is JavaScript functional?
差不多是时候弄明白另一个重要问题了:JavaScript
是函数式语言吗?通常,JavaScript
都不会出现在函数式编程语音的清单里。列入清单的都是些不怎么常见的语言,例如 Clojure
、Erlang
、Haskell
和 Scala
;但函数式编程语言并没有一个准确的定义,也没有该语言所特有的功能特性的精确描述。最主要的是,如果一种语言支持与函数式编程相关的通用编程风格,就可以认为是函数式编程语言。让我们首先来了解一下为什么要使用 JavaScript
,看看该语言是如何发展到其当前版本的,然后再来了解用 JavaScript 进行函数式编程的一些关键特性。
1.3.1. 作为一种工具
JavaScript
是什么样的语言呢?就流行指数而言,正如在 www.tiobe.com/tiobe-index/ 或 http://pypl.github.io/PYPL.html 上看到的一样,JavaScript
一直是最受欢迎的十大语言之一。从更偏学术的角度来看,JavaScript
更像一个从多种不同语言借用了相关特性所构成的综合体。一些库通过提供不太容易获得的特性来帮助该语言发展,例如类(class
)和继承(inheritance
)(目前的版本的确支持类,不久前情况并非如此),否则必须从 原型 上取巧实现。
小知识
当年选择
JavaScript
这个名字是为了蹭Java
语言的热度------出于营销的目的。其最早的名字叫Mocha
,然后又叫LiveScript
,最后才叫JavaScript
现如今 JavaScript
已经变得非常强大了。但是与所有强大的工具一样,在提供出色的解决方案的同时,也可能带来巨大的危害。函数式编程或许能够减少或避开该语言中一些最糟糕的部分,以一种更安全、更好的方式工作;然而,由于现有的 JavaScript
代码量巨大,要是指望它来推动 JavaScript
代码 ------ 这种可能导致绝大多数网站失效的语言 ------ 进行大规模重构,最终也是痴人说梦。无论好坏都得去学习了解,然后再扬长避短。
此外,JavaScript
还拥有大量现成的第三方库,以多种方式丰富并拓展了该语言的适用范围。本书将围绕 JavaScript
本身,引用现成代码进行论述讲解。
至于 JavaScript
是否是实际意义上的函数式编程语言,答案仍旧是:可能是。因为 JavaScript
具备的几个特性,例如一等公民函数、匿名函数、递归和闭包(稍后会有论述),让其可被视为函数式编程语言;但另一方面,它也有一些非函数式风格的特点,例如副作用(不纯性(impurity
))、可变对象和受限的递归。因此,采用函数式方式编程时,既要利用好 JavaScript
所有相关的、恰当的语言特性,也要尽量减少由语言本身引入的问题。从这个意义上说,JavaScript
是不是函数式的语言,取决于所采用的编程风格。
要使用函数式编程,应该决定选用哪种语言;然而,一味选择纯函数式语言未必就是明智之举。如今写代码并不仅仅是使用一种编程语言那么简单,还得有框架、库和其他各种工具的支撑。如果能在利用好现有工具的同时,在代码中恰当地引入函数式的编程风格,这样双管齐下,JavaScript
是否是函数式编程语言,已经不重要了。
1.3.2. 用 JavaScript 实现函数式编程
JavaScript
一直在与时俱进。本系列将采用的版本为 JS13
(非正式),或者 ECMAScript 2022
(正式),通常缩写为 ES2022
或 ES13
;该版本于 2022 年 6 月发布,早起主要版本沿革如下:
ECMAScript 1
,1997 年 6 月ECMAScript 2
,1998 年 6 月,基本和上一版相同ECMAScript 3
,1999 年 12 月,加入一些新功能ECMAScript 5
,2009 年 12 月(没有ECMAScript 4
,被废弃了)ECMAScript 5.1
,2011 年 6 月ECMAScript 6
(或ES6
,后更名为ES2015
)2015 年 6 月ECMAScript 7
(亦即ES7
,或ES2016
)2016 年 6 月ECMAScript 8
(ES8
或ES2017
)2017 年 6 月ECMAScript 9
(ES9
或ES2018
),2018 年 6 月ECMAScript 10
(ES10
或ES2019
),2019 年 6 月ECMAScript 11
(ES11
或ES2020
),2020 年 6 月ECMAScript 12
(ES12
或ES2021
),2021 年 6 月ECMAScript 13
(ES13
或ES2022
),2022 年 6 月
小知识
ECMA
最初表示欧洲计算机制造协会(European Computer Manufacturers Association),现如今已不仅仅被视为首字母缩写了。该组织还负责除JavaScript
外的其他标准化工作,例如JSON
、C#
、Dart
等等。详见 www.ecma-international.org/
您可以在 www.ecma-international.org/ecma-262/7.0/ 查看语言标准规范。没有明确规定的情况下,本书中的 JavaScript
皆为 ES10
(ES2019
)版。至于书中涉及的语言特性,如果用的是 ES2015
,基本上也不会有什么问题。
目前还没有浏览器完全实现 ES10
标准;大多数都提供旧版的 JavaScript 5
(始于 2009 年),并夹带一些不断增长地、从 ES6
到 ES10
的少量新特性。这会带来一个问题,所幸是可以解决的问题,后文会有论述;本书将使用 ES10
。
小知识
事实上
ES2016
与ES2015
之间的差别很小,比如方法Array.prototype.includes
,以及取幂运算符**
。而 ES2017 与 ES2016 的差别较大------像async
与await
语法、字符串填充函数等等------但均不影响我们的代码。在后续章节中,本书将对甚至是最近新增的功能,flatMap
,给出相应的替代方案。
正式使用 JavaScript 前,先来考察一下与函数式编程目标相关的最重要的几个语言特性。
1.3.3. 关键语言特性
JavaScript
并非纯粹的函数式编程语言,但也具备一旦视为函数式编程语言所应该具备的全部语言特性。本书将用到的主要特性如下:
- 函数作一等公民对象
- 递归
- 箭头函数
- 闭包
- 展开运算符
接下来,就每个特性给出示例,考察它们有所助益的原因。同时也要注意,JavaScript
的特性远比我们即将用到的要多。下面介绍的内容旨在突出函数式编程最重要的语言特性。
1.3.3.1 函数作一等对象
函数作一等对象(也叫一级公民),表示可以像处理其他对象那样处理函数。例如,将函数存入一个变量、将其传给另一个函数,打印出来等等。这也是函数式编程的关键所在:将函数作为参数传给其他函数,或是将返回一个函数作为某函数调用的结果。
如果运行的是异步 Ajax
,那么您已经使用过这个特性了:回调函数(callback
)是一个 Ajax
调用执行完毕时触发执行的函数。该函数以参数形式传入。利用 jQuery
,可以写出像下方那样的代码:
javascript
$.get("some/url", someData, function(result, status) {
// 检查 status 状态,
// 并用 result 完成相关逻辑
});
$.get()
函数接收一个回调函数作参数,并在获取到结果(result
)时调用该函数。
小贴士
回调函数的问题,在更现代的解决方案中,通过使用
promise
或async/await
语法可以得到更好的处理。鉴于仅供演示,走老路也无伤大雅。本书将在第 12 章"构建更好的容器------函数式数据类型"论述monad
相关概念时,重新讨论promise
的问题,详见 "UnexpectedMonads
-Promises
" 相关章节。
鉴于函数可以存到变量中,上述代码也可以改写如下。注意 $.get(...)
调用中变量 doSomething
的使用:
js
var doSomething = function(result, status) {
// 检查 status 状态,
// 并用 result 完成相关逻辑
};
$.get("some/url", someData, doSomething);
更多示例将在第 6 章产出函数 - 高阶函数中论述。
1.3.3.2 递归
递归 是开发算法的最为行之有效的工具,也是解决大类问题的有力助手。其核心理念是函数可以在某个时刻调用自身,当 那一次 调用完成后,将得到的结果再作进一步处理。 该方法通常对某些特定问题或定义很有帮助。最经典的示例是定义非负整数 n
的阶乘函数(n
的阶乘写作 n!
):
- 若
n
为 0,则n!
= 1;- 若
n
大于 0,则n! = n * (n-1)!
小贴士
n!
值是对n
个不同元素进行全排列的所有可能的排序方式的描述。例如将五本书排成一排,任选其一放入第一个位置,然后对剩余四本作全排列,则5! = 5 * 4!
。以此类推,将得到5! = 5 * 4 * 3 * 2 * 1 = 120
,因此n!
是 1 到n
的所有整数的乘积。
这可以快速转成代码描述:
js
function fact(n) {
if (n === 0) {
return 1;
} else {
return n * fact(n - 1);
}
}
console.log(fact(5)); // 120
递归对算法设计大有帮助。递归可以等效替换所有 while
或 for
循环------并非刻意为之,关键是提供了一种可能性!本书将在 第 9 章 设计函数 - 递归 中全面论述以递归方式设计算法和编写函数的相关知识。
1.3.3.3 闭包
闭包是隐藏数据的一种方式(结合私有变量),由此引入模块及其他不错的语言特性。闭包的关键在于,定义函数时,既可以引用自身的局部变量,也可以引用函数上下文以外的所有内容。利用闭包可以创建一个统计自身运行次数的计数函数:
js
function newCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const nc = newCounter();
console.log(nc()); // 1
console.log(nc()); // 2
console.log(nc()); // 3
即使 newCount()
退出执行,其内部函数仍可以访问 count
,但对任何其余代码而言该变量却无法访问。
这是函数式编程的一个绝佳示例。函数(本例为 nc()
)在参数相同的情况下不会返回其他截然不同的结果。
本书将考察闭包的几个实际应用,例如 memoization
(记忆功能)(详见 第四章 正确的行为 - 纯函数 、第六章 产出函数 - 高阶函数 ),以及 模块模式 (module pattern
)(详见 第三章 从函数入手 - 核心概念 、第十一章 实践设计模式 - 以函数式的风格)。
1.3.3.4. 箭头函数
箭头函数是一种更简短、更简洁创建(具名或匿名)函数的方法。除了不能用作构造函数外,箭头函数几乎可以在任何地方与传统函数互换。其语法如下:
(parameter, anotherparameter, ...etc) => { statements }
(parameter, anotherparameter, ...etc) => expression
前者可以编写任意数量的代码,后者是 { statements }
的缩写。重写之前的 Ajax
示例如下:
js
$.get("some/url", data, (result, status) => {
// 检查 status 状态,
// 并用 result 完成相关逻辑
});
箭头函数版阶乘函数可以改写如下:
js
const fact2 = n => {
if (n === 0) {
return 1;
} else {
return n * fact2(n - 1);
}
};
console.log(fact2(5)); // also 120
小贴士
箭头函数通常被称作匿名函数(
anonymous function
),因为它们没有名字。若要引用一个箭头函数,须将其赋给一个变量或对象属性,如上例所示;否则无法使用。本书将在 第三章 从函数入手 - 核心概念 的箭头函数小节作进一步探讨。
新版阶乘函数也可以写为单行代码------您能看出与之前的代码是等效吗?采用三目运算符来代替 if
语句是很常见的做法:
js
const fact3 = n => (n === 0 ? 1 : n * fact3(n - 1));
console.log(fact3(5)); // again 120
有了这样的简写形式,保留字 return
也可以省略不写了。
在
lambda
演算中,诸如x => 2x
之类的函数可以表示为λx.2x
。尽管存在语法差异,但定义是类似的。多参函数的表示稍微复杂一点:(x, y) => x + y
表示为λx.λy.x+y
。本书将在第三章的 Lambda and functions 小节、以及第七章的 Currying 小节作进一步介绍。
还有一点要记住:箭头函数在只有一个参数时,该参数两旁的括号可以省略不写。笔者更倾向于保留括号,但书中代码已经过 JS
美化工具 Prettier
处理,从而删除了括号;是否保留括号完全取决于个人喜好(详见 https://github.com/prettier/prettier)。顺便说一下,笔者使用的格式化配置参数是 --print-width 75 --tab-width 2 --no-bracket-spacing
。
1.3.3.5 展开运算符
展开运算符 可以将一个表达式扩展到需要传入多个参数、元素或变量的地方。例如替换函数的参数:
js
const x = [1, 2, 3];
function sum3(a, b, c) {
return a + b + c;
}
const y = sum3(...x); // equivalent to sum3(1,2,3)
console.log(y); // 6
也可以创建或连结数组如下:
js
const f = [1, 2, 3];
const g = [4, ...f, 5]; // [4,1,2,3,5]
const h = [...f, ...g]; // [1,2,3,4,1,2,3,5]
展开运算符也可以作用于对象:
js
const p = { some: 3, data: 5 };
const q = { more: 8, ...p }; // { more:8, some:3, data:5 }
还可用于需要传入多个参数而非数组的函数中,常见的例子是 Math.min()
与 Math.max()
:
js
const numbers = [2, 2, 9, 6, 0, 1, 2, 4, 5, 6];
const minA = Math.min(...numbers); // 0
const maxArray = arr => Math.max(...arr);
const maxA = maxArray(numbers); // 9
利用 .apply()
方法接收一个 参数数组 、.call()
方法接收 独立的参数 的特性,也可以得到下面的恒等式:
js
someFn.apply(thisArg, someArray) === someFn.call(thisArg, ...someArray);
小知识
如果在记住
.apply()
与.call()
各自所接收的参数问题上犯难,这个顺口溜或可助您一臂之力:A is for an array, and C is for a comma (A 对应Array
数组,C 对应逗号),详见 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 及 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call。
利用展开运算符可以写出更简短、简洁的代码。
至此,我们回顾了即将用到的所有重要语言特性。作为本章的结尾,再来看看即将用到的一些工具。