js foreach 跳出循环_代码详解:以forEach为跳板洞悉函数式编程

dda6b6c6d313d0cf9171159198674dbe.png
全文共 3388字,预计学习时长 7分钟

635197a990ac737f630ab46072a7e057.png
图片来源:Unsplash/JR Korpa

JavaScript编程员每天都会用多范式语言来处理大量函数式编程工作。那么,你了解多少呢?

6942abcdb82db98c7bf79c250006966a.png

一个简单的列表迭代

将下列数据视作为一个字符串列表

const langs = ['lisp', 'haskell', 'ocaml'];

JavaScript并不是强类型语言,因此采用比langs还庞大的集合可能会出现纰漏。鉴于此,我们可对其迭代,并检查控制台,观察所得结果。从迈入编程大门的那一刻起,我们便对该步骤了然于胸。

// imperative good old wayfor (let i = 0; i < langs.length; i++) {  console.log(langs[i]);}

对我们来说,这条代码看起来清晰易懂,但真是这样吗?

这是命令式语言,因为它描述的是计算机应如何迭代,而非人类自己对迭代的理解。

虽然这条代码命令性比不上带有goto的if语句,但仍属于命令式语言。

除此之外,该代码自身还有几处缺陷:

· 此代码不具有可重用性/不符合DRY原则(即使是相似情形下还是要重写先前所有代码)。

· 鉴于其为程序代码块,该代码既不能分解,也无法重组。

· 维护难度大:也许会有语句排版错误,在循环中倘若i值遭到改变,便会产生程序漏洞。

· 可扩展性差,这是因为嵌套for循环语句足以让每个程序员抓狂不已。

6942abcdb82db98c7bf79c250006966a.png

语法糖

fe9cae5c4fa65059cf288dd96eb9261b.png
图片来源:pexels.com/@negativespace

多亏了ES6这款对程序员相当友好的算法语言,我们只需使用一些语法糖,无须将额外变量声明为计数器。

// semi declarative syntactic sugar
for (const lang of langs) {
  console.log(lang);
}

现在就更像是个声明式语言了:一个编程小白都能够看懂这串代码,只要从左往右念即可。

虽然可维护性与可扩展性有所提高,但其复杂性仍差强人意。

6942abcdb82db98c7bf79c250006966a.png

老当益壮的forEach

早在ES6问世的五年前,JavaScript就已推出一个用处颇大的编程工具:forEach。

forEach既非本地控制结构,也非保留关键字,而是一个数组方法,可在任何数组中执行,就连空数组也不例外。

langs.forEach();

上述例子会导致错误,因为forEach需要一个参数[1]。

但不是某个简单、随机的参数,得是一个函数。

// try to familiarize with this notationconst sayHi = () => alert("hi."); // let's use our brand new functionlangs.forEach(sayHi)

谨记,函数被尊为JS界的第一等公民。

这并不意味着函数是独一无二的,实际上刚好相反,在JS中,函数与其他数值(对象,本原布尔函数,数字&字符串)地位相当:它们可成为变量,可进行组合,可作为参数传递,可得返回值等。(在Haskell中,就连+也能被作为参数传递)

forEach即函数编程师口中的“高阶函数”,不过也没什么复杂的,不过是一个用于运行并返回其他函数值的函数而已。

// 3 "hello" alerts, one by elementlangs.forEach(() => alert('hello'))

因此, forEach 也不是了不得的魔法秘术; 它采用一个声明式函数(未执行!),辅之以下列签名 :(any) -> void[2] 。而后便可将给定函数连续地应用于数组中的每个元素。

该参数any是用来替换迭代中的现有元素, 而void表明该函数不应返回任何数值。

langs.forEach(lang => alert(`hi ${lang}`));

运行上述代码,会跳出一个警示框,内容为 “hi <language>”,针对本数组中的每一语言。这样以来,似乎可根据所学知识解决该问题。

// declarative higher order methodlangs.forEach(lang => console.log(lang));

且慢!

console.log 是函数吗?是的。

那 console.log 有 (any) -> void 这样一个签名吗?或许吧[3]。

那么问题来了,这个函数

const log = a => console.log(a); log("hi");

和以下这一函数

console.log("hi");

到底有何分别?

似乎看不出任何区别,此包装函数只是噪声污染,所以[4]……

// higher order method enlightenlangs.forEach(console.log);

听上去有些奇怪,但从某种程度上却显得也更加透彻。

这难道是……现在做的就是函数式编程吗?

其实不然,真正的函数式编程极其严格,其中,只允许可证明的程序与可控的副作用存在。若想达到这一目标,需遵循以下规则。

6942abcdb82db98c7bf79c250006966a.png

纯函数与不可变性

e1c27149a1d7d8cead9fccbf9dcb7fae.png
图片来源:pexels.com/@divinetechygirl

还记得forEach中所用的函数签名吗?那个函数得不出任何返回值(也就是JS中所谓的未定义),或编程中的“void”。对forEach本身而言,它也是一个“未定义”的方法。这是函数副作用的明显症状,若函数无返回值,那它应该在返回语句发生前,对程序在其他方面上产生影响。例如改变了一些全局变量值,或改变数组本身。

这些事情某天我们可能会将其抛之脑后,但它们终将引发程序漏洞。总之,这些副作用是不可控的,而函数编程师都是控制狂。他们既要知其然,也要知其所以然。

虽然作为一个自定义函数,forEach比for…of语句更具可组合性,但它仍受数组的限制。forEach不能将自行传递给另一种函数。

无论怎么说,可组合性与可扩展性均未实现。

这样一来,想进入FP(函数式编程)的殿堂,就要先定义出一个独具特色的forEach。先用一个全新的函数来隐藏实现细节:

// custom pre-functional for each
const forEach = (arr, func) => {  for (let i = 0; i < arr.length; i++) {    func(arr[i]);  }};forEach(langs, console.log);

你可能会说,这明明是命令式语句,小编你个骗纸!

然而,笔者似乎之前并未提到这一方式的具体实现方法。

诚然,这样确实是有瞒天过海之嫌。但如今有了这独家定制的forEach,只要全部编写完毕,就无需为此类函数费心。这样的forEach将前无古人后无来者,起码能用上三十年。函数的真谛,即通过隐藏命令性细节,打造出一个声明性工具。没人让你将先前所学的计算机知识抛之脑后,情况恰恰相反!

即便如此,哪怕不用for语句,我们仍可以完成迭代,这就是递归,不过这是后话,而且没有TCO的话,递归还会带来新的问题[5]。

若想开开眼,可研究下列代码:

// recursive for each

const forEach = (arr, func) => {  if (arr.length) {    const [head, ...tail] = arr;    func(head);          forEach(tail, func);  }};forEach(langs, console.log);

接下来是总结时间,关于纯函数,后文还会有所叙述,现在,只需牢记,一个纯函数有两个特点:

· 应该有返回值

· 不能改变所给范围外一切数据,哪怕是给定的引用参数也不例外[6]

// custom functional for eachconst forEach = (arr, func) => {  const newArr = [...arr]; // copy  for (let i = 0; i < newArr.length; i++) {    // @see footnote 2    func(newArr[i], i, newArr);   }  return newArr; // return copy};forEach(langs, console.log);

这个forEach看上去真是好多了,可用来甄别与消除副作用。

但显然,代码的可维护性与可扩展性并未得到完全解决。

试问,倘若所有函数均为纯函数,且你的程序均由这些纯函数构成,这样一来易变性又将从何谈起?无从谈起!纯函数的引用透明性,换一种角度来看,恰恰就是其不变性。

6942abcdb82db98c7bf79c250006966a.png

科里化

接下来讨论函数式编程另一个有用的工具——科里化。

由于上文已谈到,函数被尊为第一等公民(和其他值类似),因此易知一个函数可以返回另一个函数,并将父变量囊括其中。

const f = a => b => a;const otherF = f('Hello');console.log(otherF('Goodbye')); // will log "Hello".// Play with that until you'll understand why.

这样,一个可重用性与可组合性极佳的forEach就此问世:

// curried pure functionconst forEach = func => arr => {  const newArr = [...arr];  for (let i = 0; i < newArr.length; i++) {    func(newArr[i], i, newArr);  }  return newArr;};
const logEach = forEach(console.log);logEach(langs);logEach(['see', 'you', 'soon']);
const doubleAndLogEach =       forEach(a => console.log(a * 2));doubleAndLogEach([1, 2, 3]);

注意,此类forEach参数顺序如下所示:

· func, 将被部分应用

· 接着是数列 arr

这种新顺序可以让程序员创造或重写logEach ,甚至是doubleAndLogEach 。未来还可实现forEach与其他函数的组合。

鉴于在JavaScript中,对象均通过引用项传递,因此这款forEach 还有一处较大的纰漏,即包含对象的数列有突变的风险。

JavaScript 既非静态类型语言,也非强类型语言,因此会将不纯函数甚至不良数据类导入 forEach(对于该问题,唯一的应对方式恐怕就是使用TypeScript 或Elm )

因此,理想的完美情形并未实现。读完全文也许你已发现:forEach 本身并非FP向,毕竟定义中也的确暗示了副作用的存在。这也是为何三巨头不包括forEach,而是map, filter, 和reduce。

[¹]: 实际上,这还需要第二个参数,this的语境值。

[²]: 然而代码更像是 (any, number, any[]) -> void, 数字即当前索引, any[] 即调用数组,其他的args参数也有用处,例如,若想知道当前元素是否是该数列的最后一个,这些参数便派上了用场。

[³]: 例外:真正的 console.log 签名下,可以包含无限个逗号隔开的可选args参数,并返回错误值 (any, …any[]) -> false.

[⁴]: 实际上,由于签名存在差别, console.log w呈现的信息会更多(详见备注二与备注三^^).

[⁵]: 尾调用优化是用递归取代for循坏的先决条件。若采用递归,到最后所调用栈帧可能增长过多,而可调用的栈帧是有限的。TCO不存在于JavaScript中(Node 6除外),因此我们暂时仍困于文中的循环

[⁶]: 在JavaScript中, 所有对象(包括数组)总是通过引用共享,从不拷贝。你若对此有所疑窦,还需在这方面多下些功夫。

3edbd899cb4c616e5b73938d0503406f.png

留言 点赞 关注

我们一起分享AI学习与发展的干货

编译组:董宇阳、张婷华相关链接:https://medium.com/better-programming/functional-js-from-%CE%B1-to-%CF%89-8dc0cfe1f4e1

如需转载,请后台留言,遵守转载规范

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值