如何对一个JavaScript数组进行Listify(附代码)

80 阅读4分钟

当你想向用户显示一个项目列表时,我担心.join(', '),这是不可能的:

console.log(['apple'].join(', ')) // apple
// looks good
console.log(['apple', 'grape'].join(', ')) // apple, grape
// nah, I want "apple and grape"
console.log(['apple', 'grape', 'pear'].join(', ')) // apple, grape, pear
// wut?

好吧,那就拿出你的字符串连接技巧吧?这就是我所做的......但是等等,还有一个更好的方法。

你听说过吗?Intl.ListFormat好吧,这很可能可以做到你所需要的一切,甚至更多。让我们来看看:

const items = [
  'Sojourner',
  'Opportunity',
  'Spirit',
  'Curiosity',
  'Perseverance',
]

const formatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction',
})
console.log(formatter.format(items))
// logs: "Sojourner, Opportunity, Spirit, Curiosity, and Perseverance"

这个方法的好处是,因为它来自Intl 标准,所以支持大量的地区性语言,所以你可以免费获得国际化。

ListFormat构造函数的第一个参数是地区设置(我们在上面使用'en' 表示 "英语")。第二个参数是styletype 的选项。style 选项可以是'long', 'short', 或'narrow' 中的一个。type 可以是'conjunction','disjunction', 或'unit' 中的一个。

有了上面的列表,这里是所有这些的组合(以en 作为地区名称)。

long
conjunction索杰尔,机遇,精神,好奇心,和毅力
disjunction索杰尔,机遇,精神,好奇心,或毅力
unit闯荡者、机遇、精神、好奇心、毅力
short
conjunction寄居者,机遇,精神,好奇心,和毅力
disjunction闯荡者、机遇、精神、好奇心或毅力
unit闯荡者、机遇、精神、好奇心、毅力
narrow
conjunction闯荡者、机遇、精神、好奇心、毅力
disjunction索杰尔,机会,精神,好奇心,或毅力
unit索杰纳 机会 精神 好奇心 毅力

有趣的是,如果我们玩玩地方,事情的表现会出人意料:

new Intl.ListFormat('en', {style: 'narrow', type: 'unit'}).format(items)
// Sojourner Opportunity Spirit Curiosity Perseverance

new Intl.ListFormat('es', {style: 'narrow', type: 'unit'}).format(items)
// Sojourner Opportunity Spirit Curiosity Perseverance

new Intl.ListFormat('de', {style: 'narrow', type: 'unit'}).format(items)
// Sojourner, Opportunity, Spirit, Curiosity und Perseverance

也许说德语的人可以为我们澄清,为什么narrowunit的组合对于de 的表现更像longconjunction ,因为我不知道。

还有一个鲜为人知的localeMatcher 选项,它可以被配置为'lookup''best fit' (默认为'best fit' )。就我从cdn中了解到的情况而言,它的目的是告诉浏览器如何根据构造函数中给出的语言来决定使用哪种语言。在我自己的测试中,我无法确定在这些选项之间切换的功能有什么不同 🤷♂️

坦率地说,我认为在这样的事情上,通常最好相信浏览器,而不是写浏览器提供的东西。这是因为我花时间写软件的时间越长,就越发现事情很少像我们想象的那样简单(尤其是涉及到国际化的时候)。但肯定有一些时候,平台会出现问题,你不能完全做到你想要做的事情。这时,自己做就有意义了。

因此,我组装了一个小功能,很适合我的使用情况,我想与你分享它。在这之前,我想说明的是,我确实尝试过不使用reduce(使用for循环),我认为reduce方法要简单得多。如果你想的话,可以在for循环的版本中进行尝试:

function listify(
  array,
  {conjunction = 'and ', stringify = item => item.toString()} = {},
) {
  return array.reduce((list, item, index) => {
    if (index === 0) return stringify(item)
    if (index === array.length - 1) {
      if (index === 1) return `${list} ${conjunction}${stringify(item)}`
      else return `${list}, ${conjunction}${stringify(item)}`
    }
    return `${list}, ${stringify(item)}`
  }, '')
}

在我的代码库中,是这样使用的:

const to = `To: ${listify(mentionedMembersNicknames)}`
// and
const didYouMean = `Did you mean ${listify(closeMatches, {
  conjunction: 'or ',
})}?`

我本想花时间和你一起看一下这段代码,但实际上**在我写这篇文章的时候,**我意识到我的用例并不像我想象的那样特殊,我试着重写这段代码以使用Intl.ListFormat ,你不知道吗,通过对API的一个小改动,我能够在标准之上做出一个更简单的实现:

function listify(
  array,
  {
    type = 'conjunction',
    style = 'long',
    stringify = item => item.toString(),
  } = {},
) {
  const stringified = array.map(item => stringify(item))
  const formatter = new Intl.ListFormat('en', {style, type})
  return formatter.format(stringified)
}

有了这个,现在我就这样做了:

// no change for the default case
const to = `To: ${listify(mentionedMembersNicknames)}`
// switch to using the "type" option rather than overloading/abusing the term "conjunction"
const didYouMean = `Did you mean ${listify(closeMatches, {
  type: 'disjunction',
})}?`

结论

所以,这只是告诉你,你可能在做一些你可能不需要做的额外工作。该平台可能会自动为你做这些事情哦,只是为了好玩,这是完成代码的TypeScript版本(我正在将这个项目迁移到TypeScript)。

// unfortunately TypeScript doesn't have Intl.ListFormat yet 😢
// so we'll just add it ourselves:
type ListFormatOptions = {
  type?: 'conjunction' | 'disjunction' | 'unit'
  style?: 'long' | 'short' | 'narrow'
  localeMatcher?: 'lookup' | 'best fit'
}
declare namespace Intl {
  class ListFormat {
    constructor(locale: string, options: ListFormatOptions)
    public format: (items: Array<string>) => string
  }
}

type ListifyOptions<ItemType> = {
  type?: ListFormatOptions['type']
  style?: ListFormatOptions['style']
  stringify?: (item: ItemType) => string
}
function listify<ItemType>(
  array: Array<ItemType>,
  {
    type = 'conjunction',
    style = 'long',
    stringify = (thing: {toString(): string}) => thing.toString(),
  }: ListifyOptions<ItemType> = {},
) {
  const stringified = array.map(item => stringify(item))
  const formatter = new Intl.ListFormat('en', {style, type})
  return formatter.format(stringified)
}

现在我们得到了这些选项的甜蜜自动完成和stringify方法的类型检查。很好!

哦,顺便说一下,你总是想仔细检查你所使用的浏览器的支持情况。Caniuse.comMDN关于Intl.ListFormat的文章也有一个图表

我希望这对你来说是有趣和有用的!