在开发函数式工具库的过程,引发了对于参数数量的一些思考,本篇博客只是随意发散写写。

map 的参数数量

看一个经典的例子:

1
2
['123', '123'].map(parseInt)
// [123, NaN]

原因在于,map 的回调,会传入三个参数,而 parseInt 函数的签名为 parseInt(string, radix),第二个参数 radix 为一个 2 ~ 36 之间的基数,代表以什么进制去解析第一个参数 string,而一旦无法解析,则会返回 NaN。这个例子中,map 第二个 123 时,传入 parseInt 的参数为 ('123', 1, ['123', '123']),所以返回的结果就出乎意料了。

解决这种问题,有两种思路。

第一种,多一层拦截,限制传入 parseInt 的参数。

定义一个辅助函数

1
2
3
4
5
function unary(fn) {
  return function(arg) {
    return fn(arg)
  }
}

该函数的作用为,包裹一个原始函数,确保只会传入一个参数到该原始函数。

使用:

1
2
['123', '123'].map(unary(parseInt))
// [123, 123]

这样结果就正确了。

第二种思路,我们定制 map 方法,只为回调传入每个元素作为参数,不传入多余的参数。

1
2
3
4
5
6
7
8
9
10
function map(iteratee, list) {
  if (!list || !list.length) list = []
  const len = list.length || 0
  const results = Array(len)
  for (let index = 0; index < len; index += 1) {
    // 只传入一个参数
    results[index] = iteratee(list[index])
  }
  return results
}

使用:

1
2
map(parseInt, ['123', '123'])
// [123, 123]

这种方法,比较匹配函数式编程风格,被一些函数式编程库采用,例如 Ramda

函数式编程库倾向于使用单一参数,这样可以方便于函数组合,更好地实现 “Pointfree” 编程风格。

不过集合操作函数,只为回调传入一个参数,虽然非常方便,但有时候我们真的需要用到下标,而且这种场景还不少,因此需要一种合理的解决方案。

我们当然可以为每个函数写一个双参数的版本,诸如 mapIndexed, forEachIndexed 等等。例如:

1
2
3
4
5
6
7
8
9
10
function mapIndexed(iteratee, list) {
  if (!list || !list.length) list = []
  const len = list.length || 0
  const results = Array(len)
  for (let index = 0; index < len; index += 1) {
    // 传入两个参数
    results[index] = iteratee(list[index], index)
  }
  return results
}

使用:

1
2
mapIndexed((a, b) => `${a}-${b}`, ['a', 'b', 'c'])
// ["a-0", "b-1", "c-2"]

这种实现的好处是,代码运行效率非常高,实现也非常简单。

除此之外,还有一种做法,就是采用 Ramda 那样的方式,编写一个通用的 addIndex 函数,基于原集合操作函数,生成一个带 index 的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const slice = Array.prototype.slice
function addIndex(originFn) {
  let newFn = function(orginIteratee) {
    const args = slice.call(arguments, 0)

    let index = 0

    // 加一层包裹,额外传递 index 进去
    const newIteratee = function(a, b, c) {
      let result
      switch (arguments.length) {
        case 1: {
          result = orginIteratee(a, index)
          break
        }
        case 2: {
          result = orginIteratee(a, b, index)
          break
        }
        case 3: {
          result = orginIteratee(a, b, c, index)
          break
        }
        default: {
          const args = slice.call(arguments, 0)
          args.push(index)
          result = orginIteratee.apply(void 0, args)
        }
      }
      index += 1
      return result
    }

    args[0] = newIteratee
    return originFn.apply(void 0, args)
  }

  return newFn
}

1
2
3
const mapIndexed = addIndex(map)
mapIndexed((a, b) => `${a}-${b}`, ['a', 'b', 'c'])
// ["a-0", "b-1", "c-2"]

注: Ramda 的实现,还会额外传入第三个参数,即操作的列表本身,跟 ES 原生的方法一样。