.map()の要素をスキップする方法は?


418

配列要素をスキップするにはどうすればよいですか .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"]

19
できませんが、後ですべてのnull値を除外できます。
Felix Kling 2014

1
何故なの?continueを使用して機能しないことはわかっていますが、その理由(ダブルループを回避することもできます)を知っておくとよいでしょうimg.src。 = json?
GrayedFox 2018

@GrayedFoxその後、暗黙でundefinedはなく、配列に配置されnullます。それほど良くない...
FZ

回答:


639

ただ、.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()。通常.map()、次の点で表現できます.reduce

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()コールバックの一部です。画像ソースは、フィルター操作によって保持された場合にのみ、結果配列にプッシュされます。


21
これには、配列全体を2回ループする必要がありますか?それを回避する方法はありますか?
Alex McMillan

7
@AlexMcMillanを使用する.reduce()と、すべてを1つのパスで実行できますが、パフォーマンスの点では大きな違いがあるとは思えません。
2015年

9
これらすべてのマイナスで、スタイルの値(「空」nullundefinedNaN我々は内部の1つを利用することができれば、それは良いでしょうなど)map()このオブジェクトが何にマッピングし、スキップされるべきであることを指標として。私は98%をマップしたい配列に出くわします(たとえばString.split()、最後に単一の空の文字列を残しますが、これは気にしません)。答えてくれてありがとう:)
Alex McMillan

6
@AlexMcMillan .reduce()は、戻り値を完全に制御できるため、ベースラインの「やりたいことを何でもできる」関数です。トランスデューサーの概念に関するClojureのRich Hickeyによる優れた研究に興味があるかもしれません。
2015年

3
@vsyncでは、要素をスキップできません.map()。ただし、.reduce()代わりに使用できるので、追加します。
2016年

25

配列から一部の要素をスキップする最も簡単な方法は、filter()メソッドを使用することだと思います。

このメソッド(ES5)とES6構文を使用すると、コードを1行で記述でき、これにより必要なものが返されます

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()ために作られたものです
雪崩1

2
これforEachは2パスではなく1パスで完了するよりも優れていますか?
ウリウォン

1
あなたが望むように@wuliwong。しかし、これはまだだろうことを考慮してくださいO(n)複雑meassureに、あまりにもこれら二つの記事、少なくとも見てみてください。frontendcollisionblog.com/javascript/2015/08/15/...coderwall.com/p/kvzbpa/don-t-使用アレイ-foreachの使用-ために、代わりに すべてのベスト!
simhumileco

1
@simhumilecoありがとうございます!まさにそのために、私はここにいます(そしておそらく他にもたくさんいます)。問題はおそらく、一度だけ繰り返すことによって.filterと.mapを組み合わせる方法です。
ジャックブラック

21

2019年以降、Array.prototype.flatMapは適切なオプションです。

images.flatMap(({src}) => src.endsWith('.json') ? [] : src);

MDNから

flatMapマップ中にアイテムを追加および削除する(アイテム数を変更する)方法として使用できます。つまり、常に1対1ではなく、(各入力項目を個別に処理することにより)多くの項目を多くの項目にマップできます。この意味で、フィルターの逆のように機能します。1要素の配列を返すだけでアイテムが保持され、複数要素の配列がアイテムを追加するか、0要素の配列がアイテムを削除するだけです。



1
これは本当に答えです。シンプルで十分です。これは、フィルターと削減よりも優れていることがわかります。
シャチを防御

19

TLDR:最初に配列をフィルタリングしてからマップを実行できますが、これには配列に2つのパスが必要になります(フィルターはマップする配列を返します)。このアレイは小さいため、パフォーマンスコストは非常に小さくなります。単純な削減も行うことができます。ただし、配列(または任意のデータ型)を1回パスするだけでこれがどのように行われるかを再考したい場合は、Rich Hickeyによって人気を博した「トランスデューサ」と呼ばれるアイデアを使用できます。

回答:

[].map(fn1).filter(f2)...この方法では、すべてのreducing関数のメモリに中間配列が作成されるため、配列のドットチェーンと操作を増やす必要はありません。

最良のアプローチは実際のレデューシング関数で動作するため、データのパスは1つだけで、余分な配列はありません。

削減関数は、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!!!']

リソース:リッチヒッキートランスデューサーの投稿


17

ここに楽しい解決策があります:

/**
 * Filter-map. Like map, but skips undefined values.
 *
 * @param callback
 */
function fmap(callback) {
    return this.reduce((accum, ...args) => {
        let 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
このメソッドを使用するとmap、個別にfilterとをconcat呼び出す必要がなくなりました。
LogicalBranch

11

余分なエッジケースではなく、回答してください。

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

10

なぜ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

マップを使用して上記に直接アクセスする方法はなく、すべてのソリューションが別の方法を使用していることを考えると、十分に公平です。
Alex

8
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値を除外しますnull


3

次に、null以外の値のみをマップするユーティリティメソッド(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"]


1

私は.forEachを反復して使用し、結果をresults配列にプッシュしてから使用します。このソリューションでは、配列を2回ループしません


1

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);

0

@theprtkが提供するコードの更新版を次に示します。例を挙げながら一般化されたバージョンを表示するために少し整理されています。

注:私はこれを彼の投稿へのコメントとして追加しますが、まだ十分な評判がありません

/**
 * @see http://clojure.com/blog/2012/05/15/anatomy-of-reducer.html
 * @description functions that transform reducing functions
 */
const transduce = {
  /** a generic map() that can take a reducing() & return another reducing() */
  map: changeInput => reducing => (acc, input) =>
    reducing(acc, changeInput(input)),
  /** a generic filter() that can take a reducing() & return */
  filter: predicate => reducing => (acc, input) =>
    predicate(input) ? reducing(acc, input) : acc,
  /**
   * a composing() that can take an infinite # transducers to operate on
   *  reducing functions to compose a computed accumulator without ever creating
   *  that intermediate array
   */
  compose: (...args) => x => {
    const fns = args;
    var i = fns.length;
    while (i--) x = fns[i].call(this, x);
    return x;
  },
};

const example = {
  data: [{ src: 'file.html' }, { src: 'file.txt' }, { src: 'file.json' }],
  /** note: `[1,2,3].reduce(concat, [])` -> `[1,2,3]` */
  concat: (acc, input) => acc.concat([input]),
  getSrc: x => x.src,
  filterJson: x => x.src.split('.').pop() !== 'json',
};

/** step 1: create a reducing() that can be passed into `reduce` */
const reduceFn = example.concat;
/** step 2: transforming your reducing function by mapping */
const mapFn = transduce.map(example.getSrc);
/** step 3: create your filter() that operates on an input */
const filterFn = transduce.filter(example.filterJson);
/** step 4: aggregate your transformations */
const composeFn = transduce.compose(
  filterFn,
  mapFn,
  transduce.map(x => x.toUpperCase() + '!'), // new mapping()
);

/**
 * Expected example output
 *  Note: each is wrapped in `example.data.reduce(x, [])`
 *  1: ['file.html', 'file.txt', 'file.json']
 *  2:  ['file.html', 'file.txt']
 *  3: ['FILE.HTML!', 'FILE.TXT!']
 */
const exampleFns = {
  transducers: [
    mapFn(reduceFn),
    filterFn(mapFn(reduceFn)),
    composeFn(reduceFn),
  ],
  raw: [
    (acc, x) => acc.concat([x.src]),
    (acc, x) => acc.concat(x.src.split('.').pop() !== 'json' ? [x.src] : []),
    (acc, x) => acc.concat(x.src.split('.').pop() !== 'json' ? [x.src.toUpperCase() + '!'] : []),
  ],
};
const execExample = (currentValue, index) =>
  console.log('Example ' + index, example.data.reduce(currentValue, []));

exampleFns.raw.forEach(execExample);
exampleFns.transducers.forEach(execExample);

0

あなたはあなたの後の方法を使用することができますmap()。方法filter()あなたのケースでは例えば:

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

メソッドフィルター:

const sourceFiltered = sources.filter(item => item)

次に、既存の項目のみが新しい配列に含まれsourceFilteredます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.