Fork me on GitHub

JavaScript 中的匿名递归

代码如果这么写,过一段时间之后自己还能明白?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(
(
(f) => f(f)
)
(
(f) =>
(l) => {
console.log(l)
if (l.length) f(f)(l.slice(1))
console.log(l)
}
)
)
(
[1, 2, 3]
)

是的,这就是想要分享给大家的一个有趣的示例。这个例子包含以下特性:闭包),自执行函数,箭头函数,函数式编程 和 匿名递归。

你可以复制粘贴上述代码到浏览器控制台,会看到打印结果如下:

1
2
3
4
5
6
7
8
[ 1, 2, 3 ]
[ 2, 3 ]
[ 3 ]
[]
[]
[ 3 ]
[ 2, 3 ]
[ 1, 2, 3 ]

Unwind

像其他很多编程语言一样,js函数调用是通过在函数名称后添加括号 () 来完成的:

1
2
function foo () { return 'hey' }
foo()

在 JavaScript 中我们可以使用括号包裹任意数量的表达式:

1
('hey', 2+5, 'dev.to')

上面代码返回结果是 ‘dev.to’,原因是 JavaScript 返回最后一个表达式作为结果。

使用括号 () 包裹一个匿名函数表示其结果就是 匿名函数 本身.

1
(function () { return 'hey' })

这本身并没有用处,因为匿名函数没有命名,无法被引用,除非在初始化的时候立即调用它。

就像是普通函数一样,我们可以在其后面添加括号 () 来进行调用。

1
(function () { return 'hey' })()

也可以使用箭头函数:

1
(() => 'hey')()

同样地,在匿名函数后添加括号 () 来执行函数,这被称为 自执行函数。

闭包

闭包) 指的是函数和该函数声明词法环境的组合。结合 箭头功能,我们可以定义如下:

1
var foo = (hi) => (dev) => hi + ' ' + dev

在控制台调用上述函数会打印 hey dev.to:

1
foo('hey')('dev.to')

注意,我们可以在内部函数作用域访问外部函数的参数 hi。

以下代码跟上述代码一样:

1
2
3
function foo (hi) {
return function (dev) { return hi + ' ' + dev }
}

自执行的版本如下:

1
2
3
4
5
6
7
8
(
(hi) =>
(
(dev) => `${hi} ${dev}`
)
('dev.to')
)
('hey')

首先,将 hey 作为参数 hi 的值传给最外层作用域的函数,然后这个函数返回另一个自执行函数。dev.to 作为参数 dev 的值传给内部函数,最后这个函数返回最终值:’hey dev.to’。

再深入一点

这个一个上述自执行函数的修改版本:

1
2
3
4
5
6
7
8
(
(
(dev) =>
(hi) => `${hi} ${dev}`
)
('dev.to')
)
('hey')

需要注意的是,自执行函数 和 闭包) 用作初始化和封装状态,接下来我们来看另外一个例子。

匿名递归

回到我们最初的例子,这次加点注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(
(
(f) => f(f) // 3.
)
(
(f) => // 2.
(l) => { // 4.
console.log(l)
if (l.length) f(f)(l.slice(1))
console.log(l)
}
)
)
(
[1, 2, 3] // 1.
)

等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(
(
function a(f){
return f(f)
}
)
(
function b(f){
return function c(l){
console.log('1',l)
if(l.length)
f(f)(l.slice(1))
console.log('2',l)
}
}
)
)([1,2,3])

等价于

1
2
3
4
5
6
7
8
9
function b(f){
return function c(l){
console.log('1',l)
if(l.length)
f(f)(l.slice(1))
console.log('2',l)
}
}
b(b)([1,2,3])

  • 输入函数 [1, 2, 3] 传给最外层作用域
  • 整个函数作为参数传给上面函数
  • 这个函数接收下面函数作为参数 f 的值,然后自身调用
  • 2.将被调用被作为 3.的结果然后返回函数 4. ,该函数是满足最外层作用域的函数,因此接收输入数组作为 l 参数的值

至于结果为什么是那样子,原因是在递归内部有一个对函数 f 的引用来接收输入数组 l。所以能那样调用:

1
f(f)(l.slice(1))

注意,f 是一个闭包,所以我们只需要调用它就可以访问到操作输入数组的最里面的函数。

为了说明目的,第一个 console.log(l) 语句表示递归自上而下,第二个语句表示递归自下而上。

结论

希望你喜欢这篇文章,并从中学到了新的东西。闭包、自执行函数、函数式编程模式不是黑魔法。它们遵循一套易于理解和玩乐的简单原则。

话虽如此,你必须培养自己何时使用它们,何时不用的这样一种感觉。如果你的代码变得难以维护,那这可能会成为重构中一些好点子。

然而,理解这些基本技术对于创建清晰优雅的解决方案以及提升自我是至关重要的。

坚持原创技术分享,您的支持将鼓励我继续创作!