如何在.map()中跳过一个元素?

856

如何在.map中跳过数组元素?

我的代码:

var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }
});

这将返回:

["img.png", null, "img.png"]

51
无法做到,但可以在操作之后过滤掉所有的空值。 - Felix Kling
1
为什么不呢?我知道使用continue不起作用,但是了解原因会很好(也可以避免双重循环)-编辑-对于您的情况,您是否可以反转if条件并仅在split pop的结果!= json时返回img.src - GrayedFox
1
@GrayedFox,那么隐式的 undefined 将被放入数组中,而不是 null。并没有更好... - FZs
@GrayedFox对于初始数组中的每个元素,map函数将插入子函数的返回值,因此无法避免返回一个值。要避免双重循环,请使用for循环或forEach来填充一个新数组。 - undefined
19个回答

1214

首先,使用.filter()函数:

var sources = images.filter(function(img) {
  if (img.src.split('.').pop() === "json") {
    return false; // skip
  }
  return true;
}).map(function(img) { return img.src; });

如果您不想这样做,这是可以理解的,因为这需要一些成本,您可以使用更通用的.reduce()。通常可以用.reduce()来表达.map():
someArray.map(function(element) {
  return transform(element);
});

可以写成

someArray.reduce(function(result, element) {
  result.push(transform(element));
  return result;
}, []);

如果您需要跳过元素,可以使用.reduce()轻松完成:

var sources = images.reduce(function(result, img) {
  if (img.src.split('.').pop() !== "json") {
    result.push(img.src);
  }
  return result;
}, []);

在那个版本中,第一个示例中.filter()中的代码是.reduce()回调的一部分。仅在筛选操作保留了图片源时,才将其推送到结果数组中。
更新-这个问题得到了很多关注,我想添加以下澄清说明。作为一个概念,.map()的目的正是做到“映射”的字面意思:根据某些规则将值列表转换为另一个值列表。就像一个国家的纸质地图如果缺少几个城市会感觉奇怪一样,从一个列表到另一个列表的映射只有在有1对1的结果值集时才真正有意义。
我并不是说用一些值被排除的旧列表创建新列表没有意义。我只是想明确.map()有一个单一的简单意图,即创建一个与旧数组长度相同的新数组,只不过值是通过旧值的转换得到的。

51
需要你对整个数组进行两次循环,有没有什么办法可以避免这种情况? - Alex McMillan
11
你可以使用.reduce()在一个循环中完成所有操作,但从性能角度来看,它可能不会有显著的差异。 - Pointy
13
对于所有这些负面的“空”式数值(例如:nullundefinedNaN等),如果我们能够利用其中的一个作为指示符表明此对象不需要映射,那将是很好的。在使用 map() 时,我经常遇到我想映射其中98%的数组(例如:使用 String.split() 方法留下最后一个空字符串,而我不关心它)。感谢您的提问 :) - Alex McMillan
9
@AlexMcMillan,“.reduce()”基本上是“随心所欲”的函数,因为您可以完全控制返回值。您可能会对Clojure的Rich Hickey在有关转换器概念方面做出的出色工作感兴趣。 - Pointy
4
@vsync 你不能用 .map() 跳过一个元素。不过,你可以使用 .reduce() 来代替,所以我会添加这个内容。 - Pointy
显示剩余12条评论

335
自2019年以来,Array.prototype.flatMap是一个不错的选择。
images.flatMap(({src}) => src.endsWith('.json') ? [] : src);

来自MDN:

flatMap 可以用作在 map 过程中添加和删除项目(修改项目数量)的一种方法。换句话说,它允许您将多个项映射到多个项(通过单独处理每个输入项),而不总是一对一。在这种意义上,它的工作方式类似于 filter 的相反操作。只需返回一个长度为1的数组即可保留该元素,返回一个长度大于1的数组即可添加新元素,或者返回一个长度为0的数组即可删除该元素。


24
最佳答案毋庸置疑! 更多信息请参见: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#%E4%BD%BF%E7%94%A8_flatMap_%E6%B7%BB%E5%8A%A0%E5%92%8C%E5%88%A0%E9%99%A4%E9%A1%B9%E7%9B%AE%E5%AF%B9%E8%B1%A1 - Dominique PERETTI
11
这就是确实的答案,简单而且足够强大。我们学习这比filter和reduce更好。 - defend orca
11
首先,向 MDN 致敬,因为他们提供了这种评论。文档通常没有这种实际用例的例子。其次,我希望它在“稍微更有效”的部分更加具体。比 map 后跟 flat 更高效多少? - maletor
8
你发现了一个非常可爱的函数,谢谢你! - Brandon Johnson
5
很棒的功能!使用空值合并运算符的简短现代语法:foo.flatMap(bar => bar.baz ?? [ ]) - n4ks
显示剩余6条评论

41

我认为从数组中跳过一些元素最简单的方法是使用filter()方法。

通过使用这个方法(ES5)和 ES6 语法,你可以在一行代码中编写代码,并返回你想要的结果

let images = [{src: 'img.png'}, {src: 'j1.json'}, {src: 'img.png'}, {src: 'j2.json'}];

let sources = images.filter(img => img.src.slice(-4) != 'json').map(img => img.src);

console.log(sources);


1
这正是 .filter() 的用途。 - avalanche1
3
这种方法相较于使用forEach并将其在两次传递中完成会更好吗? - wuliwong
1
随你便 @wuliwong。但请注意,这仍然是 O(n) 的复杂度度量,请至少查看这两篇文章:http://frontendcollisionblog.com/javascript/2015/08/15/3-reasons-you-should-not-be-using-foreach.html 和 https://coderwall.com/p/kvzbpa/don-t-use-array-foreach-use-for-instead祝一切顺利! - simhumileco
1
谢谢@simhumileco!正是因为这个原因,我在这里(可能还有很多其他人)。问题可能是如何通过仅迭代一次来结合.filter和.map。 - Marc

27
TLDR: 可以先对数组进行筛选(filter),然后再进行映射(map),但这将需要对数组进行两次遍历 (filter返回一个数组用于map)。由于这个数组很小,所以性能代价非常小。你也可以使用简单的reduce方法。但是,如果你想重新构想如何使用一次遍历(或任何数据类型)完成操作,可以使用 Rich Hickey 提出的“transducers”思想。 Answer:

我们不应该要求增加点链式操作并操作数组 [].map(fn1).filter(f2)...,因为这种方法会在每个reducing函数中创建中间数组。

最好的方法是在实际的 reducing 函数上操作,这样只进行一次数据传递,而没有额外的数组。

reducing 函数是传递给reduce函数的函数,它接受累加器和来自源的输入,并返回类似于累加器的内容。

// 1. create a concat reducing function that can be passed into `reduce`
const concat = (acc, input) => acc.concat([input])

// note that [1,2,3].reduce(concat, []) would return [1,2,3]

// transforming your reducing function by mapping
// 2. create a generic mapping function that can take a reducing function and return another reducing function
const mapping = (changeInput) => (reducing) => (acc, input) => reducing(acc, changeInput(input))

// 3. create your map function that operates on an input
const getSrc = (x) => x.src
const mappingSrc = mapping(getSrc)

// 4. now we can use our `mapSrc` function to transform our original function `concat` to get another reducing function
const inputSources = [{src:'one.html'}, {src:'two.txt'}, {src:'three.json'}]
inputSources.reduce(mappingSrc(concat), [])
// -> ['one.html', 'two.txt', 'three.json']

// remember this is really essentially just
// inputSources.reduce((acc, x) => acc.concat([x.src]), [])


// transforming your reducing function by filtering
// 5. create a generic filtering function that can take a reducing function and return another reducing function
const filtering = (predicate) => (reducing) => (acc, input) => (predicate(input) ? reducing(acc, input): acc)

// 6. create your filter function that operate on an input
const filterJsonAndLoad = (img) => {
  console.log(img)
  if(img.src.split('.').pop() === 'json') {
    // game.loadSprite(...);
    return false;
  } else {
    return true;
  }
}
const filteringJson = filtering(filterJsonAndLoad)

// 7. notice the type of input and output of these functions
// concat is a reducing function,
// mapSrc transforms and returns a reducing function
// filterJsonAndLoad transforms and returns a reducing function
// these functions that transform reducing functions are "transducers", termed by Rich Hickey
// source: http://clojure.com/blog/2012/05/15/anatomy-of-reducer.html
// we can pass this all into reduce! and without any intermediate arrays

const sources = inputSources.reduce(filteringJson(mappingSrc(concat)), []);
// [ 'one.html', 'two.txt' ]

// ==================================
// 8. BONUS: compose all the functions
// You can decide to create a composing function which takes an infinite number of transducers to
// operate on your reducing function to compose a computed accumulator without ever creating that
// intermediate array
const composeAll = (...args) => (x) => {
  const fns = args
  var i = fns.length
  while (i--) {
    x = fns[i].call(this, x);
  }
  return x
}

const doABunchOfStuff = composeAll(
    filtering((x) => x.src.split('.').pop() !== 'json'),
    mapping((x) => x.src),
    mapping((x) => x.toUpperCase()),
    mapping((x) => x + '!!!')
)

const sources2 = inputSources.reduce(doABunchOfStuff(concat), [])
// ['ONE.HTML!!!', 'TWO.TXT!!!']

资源: Rich Hickey的转换器文章


但是.concat()不会反复复制acc,使其成为O(n^2)吗? - netotz
是的,在JavaScript中更多是大脑运动,而Clojure(来自关联的Rich Hickey帖子)将支持这样的函数操作。 - theptrk

25

这是一个有趣的解决方案:

/**
 * Filter-map. Like map, but skips undefined values.
 *
 * @param callback
 */
function fmap(callback) {
    return this.reduce((accum, ...args) => {
        const x = callback(...args);
        if(x !== undefined) {
            accum.push(x);
        }
        return accum;
    }, []);
}

结合bind运算符使用:

[1,2,-1,3]::fmap(x => x > 0 ? x * 2 : undefined); // [2,4,6]

1
这个方法让我不必使用单独的 mapfilterconcat 调用。 - Malekai

21

为什么不直接使用forEach循环?

let arr = ['a', 'b', 'c', 'd', 'e'];
let filtered = [];

arr.forEach(x => {
  if (!x.includes('b')) filtered.push(x);
});

console.log(filtered)   // filtered === ['a','c','d','e'];

甚至可以更简单地使用过滤器:

const arr = ['a', 'b', 'c', 'd', 'e'];
const filtered = arr.filter(x => !x.includes('b')); // ['a','c','d','e'];

1
最好使用一个简单的for循环来过滤和创建一个新数组,但是为了使用“map”的上下文,让我们保持现在的样子。(当我还不懂编程时,我在4年前问过这个问题) - Ismail
考虑到使用 map 没有直接的方法来实现上述功能,而且所有的解决方案都采用了替代方法,我认为我可以提供一种最简单的方式来实现相同的功能。 - Alex

15

回答不包含多余边缘情况:

const thingsWithoutNulls = things.reduce((acc, thing) => {
  if (thing !== null) {
    acc.push(thing);
  }
  return acc;
}, [])

12
var sources = images.map(function (img) {
    if(img.src.split('.').pop() === "json"){ // if extension is .json
        return null; // skip
    }
    else{
        return img.src;
    }
}).filter(Boolean);

.filter(Boolean)会过滤掉给定数组中的任何假值(falsey values),在您的情况下是null


10

为了进一步解释Felix Kling的评论,你可以像这样使用.filter()

var sources = images.map(function (img) {
  if(img.src.split('.').pop() === "json") { // if extension is .json
    return null; // skip
  } else {
    return img.src;
  }
}).filter(Boolean);

这将从由.map()返回的数组中删除假值。

您可以进一步简化它,像这样:

var sources = images.map(function (img) {
  if(img.src.split('.').pop() !== "json") { // if extension is .json
    return img.src;
  }
}).filter(Boolean);

甚至可以使用箭头函数、对象解构和 && 运算符来编写一行代码:

var sources = images.map(({ src }) => src.split('.').pop() !== "json" && src).filter(Boolean);

6
谢谢,.filter(Boolean) 是个高明的解决方案! - kzaiwo

4

这里有一个实用方法(兼容ES5),它只映射非空值(隐藏了对reduce的调用):

function mapNonNull(arr, cb) {
    return arr.reduce(function (accumulator, value, index, arr) {
        var result = cb.call(null, value, index, arr);
        if (result != null) {
            accumulator.push(result);
        }

        return accumulator;
    }, []);
}

var result = mapNonNull(["a", "b", "c"], function (value) {
    return value === "b" ? null : value; // exclude "b"
});

console.log(result); // ["a", "c"]


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