JavaScript 函数式编程存在性能问题么?

最近在看 clean-code-javascript,看到关于函数式编程的描述时有些困惑。 毋庸置疑函数式的写法更加清晰,但是先map再reduce把数据遍历了两次,而原来的写法只需要遍历一次,在list非常大的场景下,或者链式过程更长一些,遍历N次显得太蠢了吧?是否有必要为了提升一点可读性来牺牲性能,求解惑~ 引入ramdajs这种库又觉得没有必要,太重了...
关注者
632
被浏览
41501
首先回答问题,这个问题是存在的,先map再reduce就遍历两遍了。

纯函数式语言里我不知道,我比较熟悉C#,也很欣赏C#的LINQ,所以用C#来做一个类比。

C#的LINQ会把IQueryable<T>上的类似map/reduce这类方法(.Where(), .Aggregate)全都“暂存”起来,然后通过魔法实现(其实就是yield啦),让它们能在一次遍历就完成,而且一般来说它都要.ToList()或者.ToArray()或者.First()这类的操作才会真正执行。
JS里要这么搞就意味着API要被魔改,比如(并不是真实代码,意思意思):
Queryable.from([1, 2, 3, 4, 5])
         .map(k => k + 1)
         .reduce((sum, k) => sum + k, 0) // 前面map操作是“暂存”起来,这一步才真正执行
其实这种写法并不鲜见,在很多提供类似API的ORM库里面都会用这个方法,瞎举个例子(并不是真实代码,意思意思):
var query = db.table('users')
if (name) {
  query = query.eq('name', name)
}
if (minAge > 0) {
  query = query.ge('age', minAge)
}
if (maxAge > 0) {
  query = query.le('age', maxAge)
}
var list = query.list() // 这里就会真正构建SQL并执行
而放在C#里,用LINQ写就是:
var query = db.Users;
if (!String.IsNullOrEmpty(name)) {
  query = query.Where(u => u.Name.Contains(name));
}
if (minAge > 0) {
  query = query.Where(u => u.Age >= minAge);
}
if (maxAge > 0) {
  query = query.Where(u => u.Age <= maxAge);
}
var list = query.ToList(); // 这里会真正构建SQL并执行
这样的动态构建查询的需求在做多条件组合查询的时候很常用到。
并且这里可以看到,这里C#还要更优雅一些,得益于C#提供的表达式树相关的类,可以直接用lambda表达式(剪头函数)来构建查询条件。

由于JS的一些限制,我们即使重写Array.prototype.map, Array.prototype.reduce也能把这些操作“暂存”起来,但map/filter完了需要自己toArray()一下。这么看来,先用Queryable.from来把原生Array包一下呢,也不是不可以接受。

所以对于这种问题,我自己一般的态度是:
  • 如果列表不大,比如几十,我直接懒得去管执行一遍还是执行两遍的问题,直接链式。
  • 如果列表较大,但是先filter,再map,那么如果我有预期filter会得到一个较小的结果,还是可以链式。
  • 如果列表较大,或者有特殊需求,比如一个列表要进行多个不相关的map和reduce,那么我会考虑手写for取代。