创建一个数字数组的功能性方法

11

我该如何使用ES6编写以下代码,使其更加函数化,而且不使用任何第三方库?

// sample pager array
// * output up to 11 pages
// * the current page in the middle, if page > 5
// * don't include pager < 1 or pager > lastPage
// * Expected output using example:
//     [9,10,11,12,13,14,15,16,17,18,19]

const page = 14 // by example
const lastPage = 40 // by example
const pagerPages = page => {
  let newArray = []
  for (let i = page - 5; i <= page + 5; i++) {
    i >= 1 && i <= lastPage ? newArray.push(i) : null
  }
  return newArray
}

我希望避免使用Array.push,可能也不想用for循环,但是我不确定如何在这种情况下实现。

1
new Array((page + 5) - (page - 5)).fill().map((d, i) => i + (page - 5)) - BenM
3
@BenM 或者 Array.from({length: 10}, (d, i) => i + (page - 5))。该代码段可以用来生成一个长度为10的数组,其中每个元素的值都等于它的索引加上一个偏移量(page-5)。 - Sebastian Simon
1
@BenM 不是(page + 5) - (page - 5)总是等于10吗? - Mark
1
@Mark_M 是的,它确实有。只是为了更清晰地说明。 - BenM
1
您还需要过滤掉小于1和大于lastPage的数字,因此基本上是这样的: new Array((page + 5) - (page - 5)).fill().map((d, i) => i + (page - 5)).filter(n => 1 <= n && n <= lastPage ) - Mark Kvetny
3个回答

7
函数式编程不仅限于使用 reducefiltermap;它是关于函数的。这意味着我们不必依赖于奇怪的知识,比如 Array.from ({ length: x }),其中一个带有 length 属性的对象可以被视为数组。这种行为对于初学者来说很困惑,对于其他人来说则增加了心理负担。我认为你会喜欢编写更清晰表达你意图的程序。 reduce 从 1 个或多个值开始缩减到(通常)一个值。在这种情况下,你实际上想要的是 反转 reduce(或称为 fold),这里称为 unfold。区别在于我们从单个值开始,将其扩展或 展开 成(通常)多个值。
我们以一个简化的例子 alphabet 开始展开。我们从初始值 97 开始展开,这是字母 a 的字符代码。当字符代码超过 122,即字母 z 的字符代码时,我们停止展开。

const unfold = (f, initState) =>
  f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
    , () => []
    , initState
    )

const alphabet = () =>
  unfold
    ( (next, done, char) =>
        char > 122
          ? done ()
          : next ( String.fromCharCode (char) // value to add to output
                 , char + 1                   // next state
                 )
    , 97 // initial state
    )
    
console.log (alphabet ())
// [ a, b, c, ..., x, y, z ]

上面,我们使用一个整数作为状态,但其他展开可能需要更复杂的表述。下面,我们通过展开一个由“[n,a,b]”组成的复合初始状态来显示经典的斐波那契数列,其中n是递减计数器,而a和b是用于计算序列项的数字。这说明了unfold可以与任何种子状态一起使用,甚至是数组或对象。
const fib = (n = 0) =>
  unfold
    ( (next, done, [ n, a, b ]) =>
        n < 0
          ? done ()
          : next ( a                   // value to add to output
                 , [ n - 1, b, a + b ] // next state
                 )
    , [ n, 0, 1 ] // initial state
    )

console.log (fib (20))
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765 ]

现在我们有信心编写“分页”功能。同样,我们的初始状态是复合数据“[ page, count ]”,因为我们需要跟踪要添加的“page”以及我们已经添加了多少个页面(“count”)。
这种方法的另一个优点是,您可以轻松地将诸如“10”、“-5”或“+1”之类的参数化,并且有一个明智、语义化的结构来放置它们。

const unfold = (f, initState) =>
  f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
    , () => []
    , initState
    )
    
const pagination = (totalPages, currentPage = 1) =>
  unfold
    ( (next, done, [ page, count ]) =>
        page > totalPages
          ? done ()
          : count > 10
            ? done ()
            : next (page, [ page + 1, count + 1 ])
    , [ Math.max (1, currentPage - 5), 0 ]
    )

console.log (pagination (40, 1))
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]

console.log (pagination (40, 14))
// [ 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ]

console.log (pagination (40, 38))
// [ 33, 34, 35, 36, 37, 38, 39, 40 ]

console.log (pagination (40, 40))
// [ 35, 36, 37, 38, 39, 40 ]

上面有两种情况会导致调用done()函数。我们可以使用||将它们合并,这样代码看起来会更加清晰易懂。
const pagination = (totalPages, currentPage = 1) =>
  unfold
    ( (next, done, [ page, count ]) =>
        page > totalPages || count > 10
          ? done ()
          : next (page, [ page + 1, count + 1 ])
    , [ Math.max (1, currentPage - 5), 0 ]
    )

1
另外两个人使用fromfillreducemapfilter删除了他们的答案。 - Mulan
@JonasW。如果您不习惯高阶函数和将函数视为普通数据的概念,那么“回调函数”可能会让您感到奇怪。但这是几乎所有现代语言都具有的一个众所周知的特性。另一方面,“Array.from ({length:x})”是Javascript的一个怪癖(由鸭子类型引起),深深地隐藏在语言的内部。 - user6445533
@JonasW。我的回答大部分是针对被其作者删除的答案的。我认为这种利用语言行为的方式是反常的。尽管输出可以变得正确,但程序是不可读的(甚至由您自己承认),因为它是以一种利用漏洞的方式构思出来的。我们的程序应该接受两个数字并构建一个数组,但使用 fromfill 的答案从一个数组开始(或者说是反常的“类似于数组”的东西),然后再从那里向后工作。 - Mulan
@JonasW。我稍微调整了unfold定义中的标识符。如果我们放大一点,我们会看到一个二元函数,在递归结果上添加一个值,并返回一个空结果的零元函数 - 我们将用户提供的lambda与这两个函数以及初始状态一起调用。现在用户可以使用lambda提供的信号来驱动递归。 - Mulan
@user633183 或者为了扭曲语言,你可以使用 Array.apply(null, Array(n)).map(.... - Jared Smith
显示剩余3条评论

6
  const pageRange = (lastPage, page) => ((start, end) => Array.from({length: end - start + 1}, (_,i) => i + start))(Math.max(1, page - 5), Math.min(lastPage, page + 5));
 const newArray = pageRange(40, 14);

这是一种纯函数式方法。它使用 Math.max/min 来实现边界,并使用 IIFE 将这些边界传递给 Array.from,它将创建一个包含 end - start 个元素的数组,每个元素都是数组中的位置加上开始值。
PS:在我看来,你的代码实际上比我的更简洁(除了那个不必要的三元运算符),而且更易读。

糟糕,Jonas,你的程序出了问题 :( 它生成了 [9,10,...14,...17,18](缺少 19)。对于 pageRange(40, 1),它生成了 [1,2,3,4,5](缺少 [6,7,...11])。对于 pageRange(40, 40),它生成了 [35,36,...39](缺少 40)。 - Mulan
@user633183 对的,上边界需要包含在内。但是1 -> [1,2,3,4,5]似乎是预期的行为,原帖的代码也会产生这种结果。 - Jonas Wilms

2

有许多方法可以通过函数创建数组,但是根据一些相关项目(如数学系列)创建数组通常通过展开来完成。您可以将展开视为减少的反义词。您的情况不一定需要展开,但是出于适当的函数式编程考虑,让我们看看如何完成。

JS没有本地展开函数,但我们可以简单地实现它。首先,unfold函数是什么样子..?

Array.unfold = function(p,f,t,v){
  var res = [],
   runner = d =>  p(d,res.length,res) ? [] : (res.push(f(d)),runner(t(d)), res);
  return runner(v);
};

如您所见,它需要4个参数。
  1. p这是一个回调函数,就像我们在reduce中使用的一样。它使用当前种子元素e、要插入的索引i和当前可用的数组a调用,如p(e,i,a)。当返回true时,展开操作结束并返回创建的数组。
  2. f是我们将为每个要构建的项目应用的函数。它接受一个参数,即当前迭代值。您可以将迭代值视为索引值,但我们可以控制如何迭代它。
  3. t是我们将应用于迭代值并获取下一个迭代值的函数。对于类似索引的迭代,应该是x=>x+1
  4. v是辉煌的初始值。
到目前为止还不错。我们如何使用unfold来完成这项工作。首先让我们通过一个以页面为参数的函数找到我们的初始值。
var v = (pg => pg - 5 > 0 ? pg - 5 : 1)(page)

那么关于决定停止的 p 函数呢?

var p = (_,i) => i > 10

我们将逐页增加页面,但如果我们的值大于lastpage,则需要提供空值。因此,f可能看起来像这样:
var f = (lp => v => v > lp ? null : v)(lastpage)

最后,t是我们增加迭代值的函数。它是x => x + 1

Array.unfold = function(p,f,t,v){
  var res = [],
   runner = d =>  p(d,res.length,res) ? [] : (res.push(f(d)),runner(t(d)), res);
  return runner(v);
};

var v = pg => pg - 5 > 0 ? pg - 5 : 1,
    p = (_,i) => i > 10,
    f = lp => v => v > lp ? null : v,
    t = x => x + 1,
    a = Array.unfold(p,f(40),t,v(14));

console.log(a);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接