[译] 函数式程序员的 JavaScript 简介 (软件编写)(第三部分)

烟雾艺术魔方 — MattysFlicks — (CC BY 2.0)javascript

注意:这是“软件编写”系列文章的第三部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 Composability)。后续还有更多精彩内容,敬请期待!
< 上一篇 | <<第一篇 | 下一篇 >前端

对于不熟悉 JavaScript 或 ES6+ 的同窗,这里作一个简短的介绍。不管你是 JavaScript 开发新手仍是有经验的老兵,你均可能学到一些新东西。如下内容仅是浅尝辄止,吊吊你们的兴致。若是想知道更多,还需深刻学习。敬请期待吧。java

学习编程最好的方法就是动手编程。我建议您使用交互式 JavaScript 编程环境(如 CodePenBabel REPL)。react

或者,您也可使用 Node 或浏览器控制台 REPL。android

表达式和值

表达式是能够求得数据值的代码块。ios

下面这些都是 JavaScript 中合法的表达式:git

7;

7 + 1; // 8

7 * 2; // 14

'Hello'; // Hello复制代码

表达式的值能够被赋予一个名称。执行此操做时,表达式首先被计算,取得的结果值被赋值给该名称。对于这一点咱们将使用 const 关键字。这不是惟一的方式,但这将是你使用最多的,因此目前咱们就能够坚持使用 constgithub

const hello = 'Hello';
hello; // Hello复制代码

var、let 和 const

JavaScript 支持另外两种变量声明关键字:var,还有 let。我喜欢根据选择的顺序来考虑它们。默认状况下,我选择最严格的声明方式:const。用 const 关键字声明的变量不能被从新赋值。最终值必须在声明时分配。这可能听起来很严格,但限制是一件好事。这是个标识在提醒你“赋给这个名称的值将不会改变”。它能够帮你全面了解这个名称的意义,而无需阅读整个函数或块级做用域。web

有时,给变量从新赋值颇有用。好比,若是你正在写一个手动的强制性迭代,而不是一个更具功能性的方法,你能够迭代一个用 let 赋值的计数器。npm

由于 var 能告诉你不多关于这个变量的信息,因此它是最无力的声明标识。自从开始用 ES6,我就再也没在实际软件项目中有意使用 var 做声明了。

注意一下,一个变量一旦用 letconst 声明,任何再次声明的尝试都将致使报错。若是你在 REPL(读取-求值-输出循环)环境中更喜欢多一些实验性和灵活性,那么建议你使用 var 声明变量,与 letconst 不一样,使用 var 从新声明变量是合法的。

本文将使用 const 来让您习惯于为实际程序中用 const,而出于试验的目的自由切换回 var

数据类型

目前为止咱们见到了两种数据类型:数字和字符串。JavaScript 也有布尔值(truefalse)、数组、对象等。稍后咱们再看其余类型。

数组是一系列值的有序列表。能够把它比做一个可以装不少元素的容器。这是一个数组字面量:

[1, 2, 3];复制代码

固然,它也是一个可被赋予名称的表达式:

const arr = [1, 2, 3];复制代码

在 JavaScript 中,对象是一系列键值对的集合。它也有字面量:

{
  key: 'value'
}复制代码

固然,你也能够给对象赋予名称:

const foo = {
  bar: 'bar'
}复制代码

若是你想将现有变量赋值给同名的对象属性,这有个捷径。你能够仅输入变量名,而不用同时提供一个键和一个值:

const a = 'a';
const oldA = { a: a }; // 长而冗余的写法
const oA = { a }; // 短小精悍!复制代码

为了好玩而已,让咱们再来一次:

const b = 'b';
const oB = { b };复制代码

对象能够轻松合并到新的对象中:

const c = {...oA, ...oB}; // { a: 'a', b: 'b' }复制代码

这些点是对象扩展运算符。它迭代 oA 的属性并分配到新的对象中,oB 也是同样,在新对象中已经存在的键都会被重写。在撰写本文时,对象扩展是一个新的试验特性,可能尚未被全部主流浏览器支持,但若是你那不能用,还能够用 Object.assign() 替代:

const d = Object.assign({}, oA, oB); // { a: 'a', b: 'b' }复制代码

这个 Object.assign() 的例子代码不多,若是你想合并不少对象,它甚至能够节省一些打字。注意当你使用 Object.assign() 时,你必须传一个目标对象做为第一个参数。它就是那个源对象的属性将被复制过去的对象。若是你忘了传,第一个参数传递的对象将被改变。

以个人经验,改变一个已经存在的对象而不建立一个新的对象经常引起 bug。至少至少,它很容易出错。要当心使用 Object.assign()

解构

对象和数组都支持解构,这意味着你能够从中提取值分配给命过名的变量:

const [t, u] = ['a', 'b'];
t; // 'a'
u; // 'b'

const blep = {
  blop: 'blop'
};

// 下面等同于:
// const blop = blep.blop;
const { blop } = blep;
blop; // 'blop'复制代码

和上面数组的例子相似,你能够一次解构屡次分配。下面这行你在大量的 Redux 项目中都能见到。

const { type, payload } = action;复制代码

下面是它在一个 reducer(后面的话题再详细说) 的上下文中的使用方法。

const myReducer = (state = {}, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case 'FOO': return Object.assign({}, state, payload);
    default: return state;
  }
};复制代码

若是不想为新绑定使用不一样的名称,你能够分配一个新名称:

const { blop: bloop } = blep;
bloop; // 'blop'复制代码

读做:把 blep.blop 分配给 bloop

比较运算符和三元表达式

你能够用严格的相等操做符(有时称为“三等于”)来比较数据值:

3 + 1 === 4; // true复制代码

还有另一种宽松的相等操做符。它正式地被称为“等于”运算符。非正式地能够叫“双等于”。双等于有一两个有效的用例,但大多数时候默认使用 === 操做符是更好的选择。

其它比较操做符有:

  • > 大于
  • < 小于
  • >= 大于或等于
  • <= 小于或等于
  • != 不等于
  • !== 严格不等于
  • && 逻辑与
  • || 逻辑或

三元表达式是一个可让你使用一个比较器来问问题的表达式,运算出的不一样答案取决于表达式是否为真:

14 - 7 === 7 ? 'Yep!' : 'Nope.'; // Yep!复制代码

函数

JavaScript 支持函数表达式,函数能够这样分配名称:

const double = x => x * 2;复制代码

这和数学表达式 f(x) = 2x 是一个意思。大声说出来,这个函数读做 xf 等于 2x。这个函数只有当你用一个具体的 x 的值应用它的时候才有意思。在其它方程式里面你写 f(2),就等同于 4

换种说话就是 f(2) = 4。您能够将数学函数视为从输入到输出的映射。这个例子里 f(x) 是输入数值 x 到相应的输出数值的映射,等于输入数值和 2 的乘积。

在 JavaScript 中,函数表达式的值是函数自己:

double; // [Function: double]复制代码

你可使用 .toString() 方法看到这个函数的定义。

double.toString(); // 'x => x * 2'复制代码

若是要将函数应用于某些参数,则必须使用函数调用来调用它。函数调用会接收参数而且计算一个返回值。

你可使用 <functionName>(argument1, argument2, ...rest) 调用一个函数。好比调用咱们的 double 函数,就加一对括号并传进去一个值:

double(2); // 4复制代码

和一些函数式语言不一样,这对括号是有意义的。没有它们,函数将不会被调用。

double 4; // SyntaxError: Unexpected number复制代码

签名

函数的签名能够包含如下内容:

  1. 一个 可选的 函数名。
  2. 在括号里的一组参数。 参数的命名是可选的。
  3. 返回值的类型。

JavaScript 的签名无需指定类型。JavaScript 引擎将会在运行时判定类型。若是你提供足够的线索,签名信息也能够经过开发工具推断出来,好比一些 IDE(集成开发环境)和使用数据流分析的 Tern.js

JavaScript 缺乏它本身的函数签名语法,因此有几个竞争标准:JSDoc 在历史上很是流行,但它太过笨拙臃肿,没有人会不厌其烦地维护更新文档与代码同步,因此不少 JS 开发者都弃坑了。

TypeScript 和 Flow 是目前的大竞争者。这两者都不能让我肯定地知道怎么表达我须要的一切,因此我使用 Rtype,仅仅用于写文档。一些人倒退回 Haskell 的 curry-only Hindley–Milner 类型系统。若是仅用于文档,我很乐意看到 JavaScript 能有一个好的标记系统标准,但目前为止,我以为当前的解决方案没有能胜任这个任务的。如今,怪异的类型标记即便和你在用的不尽相同,也就将就先用着吧。

functionName(param1: Type, param2: Type) => Type复制代码

double 函数的签名是:

double(x: n) => n复制代码

尽管事实上 JavaScript 不须要注释签名,知道何为签名和它意味着什么依然很重要,它有助于你高效地交流函数是如何使用和如何构建的。大多数可重复使用的函数构建工具都须要你传入一样类型签名的函数。

默认参数值

JavaScript 支持默认参数值。下面这个函数相似一个恒等函数(以你传入参数为返回值的函数),一旦你用 undefined 调用它,或者根本不传入参数——它就会返回 0,来替代:

const orZero = (n = 0) => n;复制代码

如上,若想设置默认值,只需在传入参数时带上 = 操做符,好比 n = 0。当你用这种方式传入默认值,像 Tern.js、Flow、或者 TypeScript 这些类型检测工具能够自行推断函数的类型签名,甚至你不须要刻意声明类型注解。

结果就是这样,在你的编辑器或者 IDE 中安装正确的插件,在你输入函数调用时,你能够看见内联显示的函数签名。依据它的调用签名,函数的使用方法也一目了然。不管起不起做用,使用默认值可让你写出更具可读性的代码。

注意: 使用默认值的参数不会增长函数的 .length 属性,好比使用依赖 .length 值的自动柯里化会抛出不可用异常。若是你碰上它,一些柯里化工具(好比 lodash/curry)容许你传入自定义参数来绕开这个限制。

命名参数

JavaScript 函数能够传入对象字面量做为参数,而且使用对象解构来分配参数标识,这样作能够达到命名参数的一样效果。注意,你也可使用默认参数特性传入默认值。

const createUser = ({
  name = 'Anonymous',
  avatarThumbnail = '/avatars/anonymous.png'
}) => ({
  name,
  avatarThumbnail
});

const george = createUser({
  name: 'George',
  avatarThumbnail: 'avatars/shades-emoji.png'
});

george;
/* { name: 'George', avatarThumbnail: 'avatars/shades-emoji.png' } */复制代码

剩余和展开

JavaScript 中函数共有的一个特性是能够在函数参数中使用剩余操做符 ... 来将一组剩余的参数汇集到一块儿。

例以下面这个函数简单地丢弃第一个参数,返回其他的参数:

const aTail = (head, ...tail) => tail;
aTail(1, 2, 3); // [2, 3]复制代码

剩余参数将各个元素组成一个数组。而展开操做偏偏相反:它将一个数组中的元素扩展为独立元素。研究一下这个:

const shiftToLast = (head, ...tail) => [...tail, head];
shiftToLast(1, 2, 3); // [2, 3, 1]复制代码

JavaScript 数组在使用扩展操做符的时候会调用一个迭代器,对于数组中的每个元素,迭代器都会传递一个值。在 [...tail, head] 表达式中,迭代器按顺序从 tail 数组中拷贝到一个刚刚建立的新的数组。以前 head 已是一个独立元素了,咱们只需把它放到数组的末端,就完成了。

柯里化

能够经过返回另外一个函数来实现柯里化(Curry)和偏应用(partial application):

const highpass = cutoff => n => n >= cutoff;
const gt4 = highpass(4); // highpass() 返回了一个新函数复制代码

你能够不使用箭头函数。JavaScript 也有一个 function 关键字。咱们使用箭头函数是由于 function 关键字须要打更多的字。
这种写法和上面的 highPass() 定义是同样的:

const highpass = function highpass(cutoff) {
  return function (n) {
    return n >= cutoff;
  };
};复制代码

JavaScript 中箭头的大体意义就是“函数”。使用不一样种的方式声明,函数行为会有一些重要的不一样点(=> 缺乏了它本身的 this ,不能做为构造函数),但当咱们碰见那就知道不一样之处了。如今,当你看见 x => x,想到的是 “一个携带 x 而且返回 x 的函数”。因此 const highpass = cutoff => n => n >= cutoff; 能够这样读:

highpass 是一个携带 cutoff 返回一个携带 n 并返回结果 n >= cutoff 的函数的函数”

既然 highpass() 返回一个函数,你可使用它建立一个更独特的函数:

const gt4 = highpass(4);

gt4(6); // true
gt4(3); // false复制代码

自动柯里化函数,有利于得到最大的灵活性。好比你有一个函数 add3():

const add3 = curry((a, b, c) => a + b + c);复制代码

使用自动柯里化,你能够有不少种不一样方法使用它,它将根据你传入多少个参数返回正确结果:

add3(1, 2, 3); // 6
add3(1, 2)(3); // 6
add3(1)(2, 3); // 6
add3(1)(2)(3); // 6复制代码

令 Haskell 粉遗憾的是,JavaScript 没有内置自动柯里化机制,但你能够从 Lodash 引入:

$ npm install --save lodash复制代码

而后在你的模块里:

import curry from 'lodash/curry';复制代码

或者你可使用下面这个魔性写法:

// 精简的递归自动柯里化
const curry = (
  f, arr = []
) => (...args) => (
  a => a.length === f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);复制代码

函数组合

固然你可以开始组合函数了。组合函数是传入一个函数的返回值做为参数给另外一个函数的过程。用数学符号标识:

f . g复制代码

翻译成 JavaScript:

f(g(x))复制代码

这是从内到外地求值:

  1. x 是被求数值
  2. g() 应用给 x
  3. f() 应用给 g(x) 的返回值

例如:

const inc = n => n + 1;
inc(double(2)); // 5复制代码

数值 2 被传入 double(),求得 44 被传入 inc() 求得 5

你能够给函数传入任何表达式做为参数。表达式在函数应用以前被计算:

inc(double(2) * double(2)); // 17复制代码

既然 double(2) 求得 4,你能够读做 inc(4 * 4),而后计算得 inc(16),而后求得 17

函数组合是函数式编程的核心。咱们后面还会介绍不少。

数组

数组有一些内置方法。方法是指对象关联的函数,一般是这个对象的属性:

const arr = [1, 2, 3];
arr.map(double); // [2, 4, 6]复制代码

这个例子里,arr 是对象,.map() 是一个以函数为值的对象属性。当你调用它,这个函数会被应用给参数,和一个特别的参数叫作 thisthis 在方法被调用之时自动设置。这个 this 的存在使 .map() 可以访问数组的内容。

注意咱们传递给 map 的是 double 函数而不是直接调用。由于 map 携带一个函数做为参数并将函数应用给数组的每个元素。它返回一个包含了 double() 返回值的新的数组。

注意原始的 arr 值没有改变:

arr; // [1, 2, 3]复制代码

方法链

你也能够链式调用方法。方法链是指在函数返回值上直接调用方法的过程,在此期间不须要给返回值命名:

const arr = [1, 2, 3];
arr.map(double).map(double); // [4, 8, 12]复制代码

返回布尔值(truefalse)的函数叫作 断言(predicate)。.filter() 方法携带断言并返回一个新的数组,新数组中只包含传入断言函数(返回 true)的元素:

[2, 4, 6].filter(gt4); // [4, 6]复制代码

你经常会想要从一个列表选择一些元素,而后把这些元素序列化到一个新列表中:

[2, 4, 6].filter(gt4).map(double); [8, 12]复制代码

注意:后面的文章你将看到使用叫作 transducer 东西更高效地同时选择元素并序列化,不过这以前还有一些其余东西要了解。

总结

若是你如今有点发懵,没必要担忧。咱们仅仅概览了一下不少事情的表面,它们尚需大量的解释和总结。很快咱们就会回过头来,深刻探讨其中的一些话题。

继续阅读 “高阶函数”…

接下来

想要学习更多 JavaScript 函数式编程知识?

和 Eric Elliott 一块儿学习 JavaScript。 若是你不是其中一员,千万别错过!

Eric Elliott“JavaScript 应用程序设计” (O’Reilly) 以及 “和 Eric Elliott 一块儿学习 JavaScript” 的做者。 曾就任于 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC and top recording artists including Usher、Frank Ocean、Metallica 等公司,具备丰富的软件实践经验。

他大多数时间在 San Francisco By Area ,和世界上最美丽的姑娘在一块儿。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索