这里是用 JavaScript 做的逆转序列(数组/字符串)的递归/尾递归实现。另外还尝鲜用了一下 ES6 的destructuring assignment + spread operator 做了一个更 functional 的版本(只支持数组)。
正确性能通过测试(参见 放在我 Github 上的 demo,顺手写了一个小小的测试框架),不过效率就要打问号了——特别是用了 ES6 特性的版本。这里主要是写来玩 JS 的函数式特性的。
1. 逆转序列的递归实现
先用 Haskell 实现做草稿(因为比较自然),数组版长这样
reverse' :: [a] -> [a] reverse' [] = [] reverse' (x:xs) = reverse' xs ++ [x]
在 JavaScript 里,数组和字符串都有 slice
和 concat
,所以可以写成两种“序列”(在离散数学之类的领域里经常把两者合称为 sequence) 都支持的版本。
Note:
Array.prototype.slice()
Array.prototype.concat()
String.prototype.slice()
String.prototype.concat()
均出现于 ECMAScript 3,实现于 JavaScript 1.2
对应的 JS 实现如下,把 base case 设定到序列长度小于 1 的时候,这样缩减了一步递归。相比而言还是 Haskell 版比较一目了然啊……
function recursiveReverse(seq) { return seq.length > 1 ? recursiveReverse(seq.slice(1)).concat(seq.slice(0, 1)) : seq; }
经过测试,数组和字符串都能通过。
2. 尾递归实现
Haskell 版本长这样,用一个 ret
传递已完成的部分:
reverse' :: [a] -> [a] reverse' list = rev list [] where rev [] ret = ret rev (x:xs) ret = rev xs (x:ret)
对应的 JS 版本,这里用 slice(0, 0)
来避开类型检查创建空序列。
function tailReverse(seq) { return (function rev(seq, ret) { return seq.length > 0 ? rev(seq.slice(1), seq.slice(0, 1).concat(ret)) : ret; })(seq, seq.slice(0, 0)); }
经过测试,数组和字符串都能通过。
3. 使用 ES6 新特性的递归版本
ES6 引入了 destructuring assignment 和 spread operator,这样就可以部分地使用函数式编程中的模式匹配(pattern matching)。不过我在 ES6 的新特性里还没找到能在语法层面上让 JS 的函数像 Haskell 那样将模式匹配融合进重载(overloading)的组合(destructuring assignment 本身就是模式匹配的一个子集而已),要重载还是得用当前 JS 里最常见的解决办法——if-else或者三目运算符配合类型检查。这里因为用法简单,直接三目运算符就行。
配合 ES6 新特性实现的简单递归版如下,因为 JS 里数组和字符串没有通用的功能类似 append
(往后加元素并返回新的数组)的函数,懒得折腾类型检查了,所以这里只实现了数组版(由于 destructuring assignment + spread operator 的组合[x, ...xs]
会将字符串分割成第一个字符,[其他字符组成的数组]
,因此直接给下面的函数传入字符串之后再将返回值 join
回字符串也可以达到一样的效果)。
function pmReverse([x, ...xs]) { return typeof x === "undefined" ? [] : pmReverse(xs).concat([x]); }
经过测试,数组都可以正常逆转(只有在浏览器打开 ES6 特性的时候这个页面才会正常跑掉这个版本的测试,不然就只有一个 alert)。
4. 使用 ES6 新特性的尾递归版本
function pmTailReverse(list) { return (function rev([x, ...xs], ret) { return typeof x === "undefined" ? ret : rev(xs, [x].concat(ret)); })(list, []); }
经过测试,数组都可以正常逆转(只有在浏览器打开 ES6 特性的时候这个页面才会正常跑掉这个版本的测试,不然就只有一个 alert)。
关于测试页面
为了方便写了一个小小的测试框架,参见源代码。
因为不支持 ES6 的浏览器在解析 ES6 特性的代码时会出语法错误,用 try-catch 抓不了,这里用了 window.onerror
来捕捉并提示。不知为何我的 Chrome 开了实验性 JS 依然打不开 ES6 支持…… FireFox 倒是默认就能用。