JavaScriptで配列の要素を削除するには、末尾なら pop、先頭なら shift、位置を指定するなら splice、条件で取り除くなら filter を使います。 元の配列を書き換えたくないときは、ES2023 で加わった toSpliced か filter を選びます。削除の方法は数が多く、「元の配列が変わるかどうか」と「戻り値が何か」がメソッドごとに違うため、ここを取り違えると思った結果になりません。

この記事では、先頭・末尾・位置指定・値指定・条件・重複・全削除という目的別に、破壊的か非破壊か・戻り値を並べた早見表を最初に置き、コピーしてそのまま使えるコードを Node.js v22.22.2 の実行結果つきで載せています。toSpliced などの非破壊メソッド、Map を使ったオブジェクト配列の重複削除、for ループの中で splice すると要素が1つ飛ぶ動きの再現と直し方、10万件での処理時間の実測まで扱います。

配列の要素を削除する方法の早見表

「破壊的」は元の配列そのものを書き換えること、「非破壊」は元の配列を残して新しい配列を返すことを指します。

目的 書き方 元の配列 戻り値
末尾を1つ削除 arr.pop() 変わる(破壊的) 削除した要素
先頭を1つ削除 arr.shift() 変わる(破壊的) 削除した要素
末尾を除いた配列を作る arr.slice(0, -1) 変わらない(非破壊) 新しい配列
先頭を除いた配列を作る arr.slice(1) 変わらない(非破壊) 新しい配列
位置を指定して削除 arr.splice(i, 1) 変わる(破壊的) 削除した要素の配列
位置を指定して削除(元を残す) arr.toSpliced(i, 1) 変わらない(非破壊) 削除後の新しい配列
値を指定して最初の1件を削除 arr.splice(arr.indexOf(v), 1)(-1 の確認が必要) 変わる(破壊的) 削除した要素の配列
値を指定してすべて削除 arr.filter(x => x !== v) 変わらない(非破壊) 新しい配列
条件に合う要素を削除 arr.filter(x => !条件) 変わらない(非破壊) 新しい配列
条件に合う最初の1件を削除 findIndex で位置を探して splice 変わる(破壊的) 削除した要素の配列
重複を削除(数値・文字列) [...new Set(arr)] 変わらない(非破壊) 新しい配列
重複を削除(オブジェクト) [...new Map(arr.map(o => [o.id, o])).values()] 変わらない(非破壊) 新しい配列
空・null などを削除 arr.filter(Boolean) 変わらない(非破壊) 新しい配列
すべて削除(空にする) arr.length = 0 変わる(破壊的) なし(代入文)

いちばん取り違えやすいのは、splice の戻り値が「削除後の配列」ではなく「削除した要素の配列」である点です。削除後の配列を戻り値で受け取りたいときは toSpliced か filter を使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript配列の要素削除|splice・filter・重複削除早見表」

検証した環境

項目 内容
実行環境 Node.js v22.22.2(Linux、2 vCPU のコンテナ)
実行方法 node ファイル名.mjs
表示 実行結果は console.log の出力そのまま
ブラウザ 今回はブラウザでの実行は未実行です。標準の配列メソッドだけを使っています

末尾・先頭を削除する:pop と shift

pop は末尾、shift は先頭を1つ取り除き、取り除いた要素を返します。 どちらも元の配列を書き換えます。

let a = ['a', 'b', 'c', 'd'];
const last = a.pop();
console.log(last, a);

let b = ['a', 'b', 'c', 'd'];
const first = b.shift();
console.log(first, b);

let empty = [];
console.log(empty.pop(), empty.shift(), empty);

実行結果:

d [ 'a', 'b', 'c' ]
a [ 'b', 'c', 'd' ]
undefined undefined []

空の配列に対して呼んでもエラーにはならず、undefined が返ります。

元の配列を残したいときは slice か分割代入を使います。

const a = ['a', 'b', 'c', 'd'];
console.log(a.slice(1));       // 先頭を除く
console.log(a.slice(0, -1));   // 末尾を除く
console.log(a.slice(2));       // 先頭2つを除く
console.log(a.slice(0, -2));   // 末尾2つを除く
console.log(a);                // 元の配列はそのまま

const [head, ...rest] = a;     // 分割代入で先頭と残りに分ける
console.log(head, rest);

実行結果:

[ 'b', 'c', 'd' ]
[ 'a', 'b', 'c' ]
[ 'c', 'd' ]
[ 'a', 'b' ]
[ 'a', 'b', 'c', 'd' ]
a [ 'b', 'c', 'd' ]

末尾から複数まとめて削ってよいなら、length に小さい数を代入する方法もあります。

let a = ['a', 'b', 'c', 'd'];
a.length = 2;
console.log(a);

実行結果:

[ 'a', 'b' ]

位置を指定して削除する:splice

splice(開始位置, 削除する個数) は、指定した位置から要素を取り除き、取り除いた要素を配列で返します。 元の配列が書き換わります。

let a;
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(1, 1), a);   // 位置1から1個
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(1, 2), a);   // 位置1から2個
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(2), a);      // 位置2から最後まで
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(-1, 1), a);  // 末尾の1個
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(-2), a);     // 末尾の2個
a = ['a', 'b', 'c', 'd', 'e']; console.log(a.splice(1, 2, 'X'), a); // 削除して置き換え

実行結果:

[ 'b' ] [ 'a', 'c', 'd', 'e' ]
[ 'b', 'c' ] [ 'a', 'd', 'e' ]
[ 'c', 'd', 'e' ] [ 'a', 'b' ]
[ 'e' ] [ 'a', 'b', 'c', 'd' ]
[ 'd', 'e' ] [ 'a', 'b', 'c' ]
[ 'b', 'c' ] [ 'a', 'X', 'd', 'e' ]

左が戻り値(削除した要素)、右が実行後の配列です。引数の指定による違いを表にまとめます。

呼び方 動き 戻り値(a が5要素のとき)
a.splice(1, 1) 位置1から1個削除 [ 'b' ]
a.splice(2) 位置2から最後まで削除 [ 'c', 'd', 'e' ]
a.splice(-1, 1) 末尾から数えた位置で削除 [ 'e' ]
a.splice(1, 0) 個数0なので何も削除しない []
a.splice(10, 1) 範囲外なので何も削除しない []
a.splice() 引数なしは何も削除しない []
a.splice(undefined) undefined は 0 として扱われ、全部削除 [ 'a', 'b', 'c', 'd', 'e' ]

最後の2行は実行して確かめた結果です。引数なしと undefined を渡した場合で動きが違うため、位置を変数で渡すときは値が入っているかを確認しておくと安心です。

splice でよくある取り違え

戻り値を同じ変数に代入すると、削除した要素だけが残ります。

let a = ['a', 'b', 'c', 'd'];
a = a.splice(1, 1);
console.log(a);

実行結果:

[ 'b' ]

a.splice(1, 1); とだけ書けば、a は [ 'a', 'c', 'd' ] になります。また、名前の似た slice は「切り出して返す」メソッドで、元の配列は変わりません。

const a = ['a', 'b', 'c', 'd'];
console.log(a.slice(1, 2), a);

実行結果:

[ 'b' ] [ 'a', 'b', 'c', 'd' ]
メソッド 第2引数の意味 元の配列 戻り値
splice(start, count) 削除する個数 変わる 削除した要素の配列
slice(start, end) 終了位置(その手前まで) 変わらない 切り出した部分の配列
toSpliced(start, count) 削除する個数 変わらない 削除後の配列

元の配列を残して削除する:toSpliced(ES2023)

toSpliced は splice の非破壊版で、削除後の新しい配列を返します。 引数の意味は splice と同じです。

const a = ['a', 'b', 'c', 'd', 'e'];
console.log(a.toSpliced(1, 1));        // 位置1を削除
console.log(a.toSpliced(1, 2, 'X'));   // 削除して置き換え
console.log(a.toSpliced(-1));          // 末尾を削除
console.log(a);                        // 元の配列はそのまま

実行結果:

[ 'a', 'c', 'd', 'e' ]
[ 'a', 'X', 'd', 'e' ]
[ 'a', 'b', 'c', 'd' ]
[ 'a', 'b', 'c', 'd', 'e' ]

React の state のように「元の配列を書き換えず、新しい配列を渡す」ことが求められる場面や、Object.freeze で固定した配列を扱う場面で役立ちます。

const a = Object.freeze(['a', 'b', 'c']);
try {
  a.splice(0, 1);
} catch (e) {
  console.log(e.name + ': ' + e.message);
}
console.log(a.toSpliced(0, 1));

実行結果:

TypeError: Cannot assign to read only property '0' of object '[object Array]'
[ 'b', 'c' ]

ES2023 では、同じ考え方の非破壊メソッドがそろって加わりました。

const a = [1, 2, 3];
console.log(a.with(1, 99), a.toReversed(), a.toSorted((x, y) => y - x), a);

実行結果:

[ 1, 99, 3 ] [ 3, 2, 1 ] [ 3, 2, 1 ] [ 1, 2, 3 ]
破壊的(元を書き換える) 非破壊(新しい配列を返す) 用途
splice() toSpliced() 削除・挿入・置き換え
arr[i] = v with(i, v) 1要素の置き換え
reverse() toReversed() 逆順
sort() toSorted() 並べ替え

対応状況と、使えない環境での書き方

MDN の toSpliced のページを今回開いて確認したところ、Baseline(主要ブラウザでそろって使える目安)は 2023年7月からと記載されていました。Node.js では v22.22.2 で動くことを実行して確認しています。対応が始まった版は、一般に Chrome 110、Firefox 115、Safari 16、Node.js 20 と知られています(個々の版番号は今回は未確認です)。

それより前の環境も対象にする場合は、スプレッド構文と slice、または filter で同じ結果を作れます。

const a = ['a', 'b', 'c', 'd', 'e'];
const i = 2;
console.log([...a.slice(0, i), ...a.slice(i + 1)]);
console.log(a.filter((_, idx) => idx !== i));

実行結果:

[ 'a', 'b', 'd', 'e' ]
[ 'a', 'b', 'd', 'e' ]

値を指定して削除する

最初に見つかった1件だけ削除する

indexOf で位置を調べ、splice で取り除きます。見つからないとき indexOf は -1 を返すので、必ず確認を入れます。

let a = ['red', 'green', 'blue', 'green'];
const i = a.indexOf('green');
if (i !== -1) a.splice(i, 1);
console.log(i, a);

実行結果:

1 [ 'red', 'blue', 'green' ]

確認を省くとどうなるかを見てみます。

let a = ['red', 'green', 'blue'];
const i = a.indexOf('pink');   // 見つからないので -1
a.splice(i, 1);                // splice(-1, 1) は「末尾を1個削除」
console.log(i, a);

実行結果:

-1 [ 'red', 'green' ]

存在しない値を指定したのに、末尾の blue が消えました。エラーが出ないので気づきにくい動きです。if (i !== -1) を付けるだけで防げます。

非破壊で書くなら次のとおりです。最後に見つかった1件を消したいときは lastIndexOf を使います。

const a = ['red', 'green', 'blue', 'green'];
const i = a.indexOf('green');
console.log(i === -1 ? a : a.toSpliced(i, 1));

const j = a.lastIndexOf('green');
console.log(a.toSpliced(j, 1));

実行結果:

[ 'red', 'blue', 'green' ]
[ 'red', 'green', 'blue' ]

同じ値をすべて削除する・複数の値をまとめて削除する

const a = ['red', 'green', 'blue', 'green'];
console.log(a.filter(v => v !== 'green'));
console.log(a);   // 元の配列はそのまま

const b = ['a', 'b', 'c', 'd'];
const del = new Set(['b', 'd']);          // 削除したい値の集まり
console.log(b.filter(v => !del.has(v)));

// Set の difference(Node.js 22 で動作)
console.log([...new Set([1, 2, 3, 4]).difference(new Set([2, 4]))]);

実行結果:

[ 'red', 'blue' ]
[ 'red', 'green', 'blue', 'green' ]
[ 'a', 'c' ]
[ 1, 3 ]

filter は結果を返すだけなので、戻り値を受け取らないと何も変わりません。

const a = ['a', 'b', 'c', 'd'];
a.filter(v => v !== 'b');     // 戻り値を使っていない
console.log(a);

実行結果:

[ 'a', 'b', 'c', 'd' ]

const result = a.filter(...) と受け取るか、let で宣言して a = a.filter(...) と代入し直します。

NaN とオブジェクトは indexOf で見つからない

const a = [1, NaN, 3];
console.log(a.indexOf(NaN), a.includes(NaN));
console.log(a.filter(v => !Number.isNaN(v)));

const o = { id: 1 };
const list = [o, { id: 2 }];
console.log(list.indexOf({ id: 1 }), list.indexOf(o), list.includes({ id: 1 }));

実行結果:

-1 true
[ 1, 3 ]
-1 0 false

indexOf は === で比べるため、NaN は見つかりません。また、オブジェクトは中身が同じでも別々に作ったものは別物として扱われます。オブジェクトの配列は、次の章のように id などのプロパティで条件を書いて削除します。

条件に合う要素を削除する:filter と findIndex

filter は「残したい条件」を書くメソッドです。 削除したい条件があるときは、それを反転させて書きます。

const users = [
  { id: 1, name: 'sato',   active: true },
  { id: 2, name: 'suzuki', active: false },
  { id: 3, name: 'tanaka', active: true },
];

// active が false の人を取り除く(= true の人を残す)
console.log(users.filter(u => u.active));

// id が 2 の人を取り除く
console.log(users.filter(u => u.id !== 2).map(u => u.id));

// 元の配列を書き換えて、条件に合う最初の1件だけ削除する
const copy = [...users];
const i = copy.findIndex(u => u.id === 2);
if (i !== -1) copy.splice(i, 1);
console.log(i, copy.map(u => u.id));

console.log(users.findIndex(u => u.id === 99));     // 見つからないときは -1
console.log(users.findLastIndex(u => u.active));    // 後ろから探す

実行結果:

[
  { id: 1, name: 'sato', active: true },
  { id: 3, name: 'tanaka', active: true }
]
[ 1, 3 ]
1 [ 1, 3 ]
-1
2

空・null・undefined を取り除く

const a = [0, 1, '', null, 'a', undefined, NaN, false, [], '0'];
console.log(a.filter(Boolean));            // 偽と判定される値をすべて除く
console.log(a.filter(v => v != null));     // null と undefined だけ除く
console.log(['a', '', 'b', ' ', 'c'].filter(s => s.trim() !== ''));  // 空白だけの文字列を除く

実行結果:

[ 1, 'a', [], '0' ]
[
  0,   1,   '',
  'a', NaN, false,
  [],  '0'
]
[ 'a', 'b', 'c' ]

filter(Boolean) は短く書けますが、数値の 0 や空文字列、false も取り除きます。 0 を残したいデータでは、v != null のように条件を明示するのが確実です。

二次元配列から列や行を削除する

const grid = [[1, 2, 3], [4, 5, 6]];
console.log(grid.map(row => row.filter((_, c) => c !== 1)));  // 2列目を削除
console.log(grid.toSpliced(0, 1));                             // 1行目を削除

実行結果:

[ [ 1, 3 ], [ 4, 6 ] ]
[ [ 4, 5, 6 ] ]

forループ中にspliceすると要素が飛ぶ:再現と直し方

配列から偶数を取り除くつもりで、次のように書いたとします。

const a = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < a.length; i++) {
  if (a[i] % 2 === 0) a.splice(i, 1);
}
console.log(a);

実行結果:

[ 1, 2, 3, 4, 5 ]

偶数が残っています。ループの各回で「i と、そのとき見ていた値」を記録すると、理由が見えてきます。

const a = [1, 2, 2, 3, 4, 4, 5];
const log = [];
for (let i = 0; i < a.length; i++) {
  log.push(`i=${i}:${a[i]}`);
  if (a[i] % 2 === 0) a.splice(i, 1);
}
console.log(log.join(' '));

実行結果:

i=0:1 i=1:2 i=2:3 i=3:4 i=4:5

7個の要素があったのに、調べたのは5回だけです。splice で1つ削除すると後ろの要素が1つ前に詰まり、次の要素が今の位置 i に来ます。そこで i++ されるので、その要素は調べられずに通り過ぎます。

flowchart LR
    A["i=1 で 2 を削除"] --> B["後ろの要素が1つ前に詰まる<br>位置1 に次の 2 が来る"]
    B --> C["i++ で i=2 へ進む"]
    C --> D["位置1 の 2 は<br>調べられないまま残る"]

forEach や for...of でも同じ結果になります。

const a = [1, 2, 2, 3, 4, 4, 5];
a.forEach((v, i) => { if (v % 2 === 0) a.splice(i, 1); });
console.log(a);

const b = [1, 2, 2, 3, 4, 4, 5];
for (const v of b) { if (v % 2 === 0) b.splice(b.indexOf(v), 1); }
console.log(b);

実行結果:

[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3, 4, 5 ]

直し方4通り

// 1. filter で新しい配列を作る(いちばん簡潔)
const a1 = [1, 2, 2, 3, 4, 4, 5];
console.log(a1.filter(v => v % 2 !== 0));

// 2. 後ろから回す(詰まっても、これから調べる側に影響しない)
const a2 = [1, 2, 2, 3, 4, 4, 5];
for (let i = a2.length - 1; i >= 0; i--) {
  if (a2[i] % 2 === 0) a2.splice(i, 1);
}
console.log(a2);

// 3. 削除したら i を1つ戻す
const a3 = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < a3.length; i++) {
  if (a3[i] % 2 === 0) { a3.splice(i, 1); i--; }
}
console.log(a3);

// 4. while で、削除しなかったときだけ進める
const a4 = [1, 2, 2, 3, 4, 4, 5];
let i = 0;
while (i < a4.length) {
  if (a4[i] % 2 === 0) a4.splice(i, 1); else i++;
}
console.log(a4);

実行結果:

[ 1, 3, 5 ]
[ 1, 3, 5 ]
[ 1, 3, 5 ]
[ 1, 3, 5 ]
直し方 元の配列 向いている場面
filter 変わらない ほとんどの場面。読みやすく、速い
後ろから for 変わる 同じ配列を書き換える必要があるとき
削除後に i-- 変わる 前から順に処理する必要があるとき
while 変わる 進める条件を明示したいとき

同じ配列(同じ参照)を保ったまま filter の結果に入れ替えたいときは、次の書き方が使えます。

const a = [1, 2, 2, 3, 4, 4, 5];
const ref = a;
a.splice(0, a.length, ...a.filter(v => v % 2 !== 0));
console.log(a, ref === a);

実行結果:

[ 1, 3, 5 ] true

重複を削除する

数値・文字列の重複は Set

Set は同じ値を1つしか持てない入れ物なので、配列を Set にしてから配列に戻すと重複がなくなります。 最初に出てきた順番は保たれます。

const a = [3, 1, 3, 2, 1, '1', NaN, NaN];
console.log([...new Set(a)]);
console.log(Array.from(new Set(a)));
console.log(a.filter((v, i, self) => self.indexOf(v) === i));

実行結果:

[ 3, 1, 2, '1', NaN ]
[ 3, 1, 2, '1', NaN ]
[ 3, 1, 2, '1' ]

数値の 1 と文字列の '1' は別の値として残ります。3行目の filter と indexOf を使う書き方では、indexOf が NaN を見つけられないため、NaN が結果から消えています。NaN を含む可能性があるデータでは Set を使うと結果がそろいます。

大文字・小文字を区別せずに重複を除く例と、重複している値だけを取り出す例です。

const names = ['Apple', 'apple', 'APPLE', 'Banana'];
const seen = new Set();
console.log(names.filter(s => {
  const k = s.toLowerCase();
  return seen.has(k) ? false : (seen.add(k), true);
}));

const n = [3, 1, 3, 2, 1];
console.log([...new Set(n.filter((v, i, self) => self.indexOf(v) !== i))]);

実行結果:

[ 'Apple', 'Banana' ]
[ 3, 1 ]

オブジェクト配列の重複は Map

オブジェクトは中身が同じでも別々のものとして扱われるため、Set では重複を除けません。

const a = [{ id: 1, n: 'a' }, { id: 2, n: 'b' }, { id: 1, n: 'c' }];
console.log([...new Set(a)].length);

実行結果:

3

「どのプロパティが同じなら重複とみなすか」をキーにして、Map に入れます。 Map は同じキーに対して値を1つだけ持つので、キーごとに1件にまとまります。

const a = [{ id: 1, n: 'a' }, { id: 2, n: 'b' }, { id: 1, n: 'c' }];

// 後に出てきたものを残す(同じキーは上書きされる)
console.log([...new Map(a.map(o => [o.id, o])).values()]);

// 先に出てきたものを残す
const m = new Map();
for (const o of a) if (!m.has(o.id)) m.set(o.id, o);
console.log([...m.values()]);

// Set と filter で、先に出てきたものを残す
const seen = new Set();
console.log(a.filter(o => !seen.has(o.id) && seen.add(o.id)));

実行結果:

[ { id: 1, n: 'c' }, { id: 2, n: 'b' } ]
[ { id: 1, n: 'a' }, { id: 2, n: 'b' } ]
[ { id: 1, n: 'a' }, { id: 2, n: 'b' } ]

1つ目は id が 1 の要素として、後に出てきた n が 'c' のほうが残っています。並び順は最初に出てきた位置のままです。「先勝ち」か「後勝ち」かで書き方を選んでください。

複数のプロパティの組み合わせで判定したいときは、キーを文字列にまとめます。

const b = [{ x: 1, y: 2 }, { x: 1, y: 2 }, { x: 1, y: 3 }];
console.log([...new Map(b.map(o => [`${o.x}|${o.y}`, o])).values()]);
console.log([...new Map(b.map(o => [JSON.stringify(o), o])).values()]);

実行結果:

[ { x: 1, y: 2 }, { x: 1, y: 3 } ]
[ { x: 1, y: 2 }, { x: 1, y: 3 } ]

JSON.stringify をキーにする方法は手軽ですが、プロパティの並び順が違うと別のものとして扱われます。プロパティを指定してキーを作る書き方のほうが確実です。

方法 対象 残るもの 補足
[...new Set(arr)] 数値・文字列 最初に出てきたもの NaN も1つにまとまる
filter + indexOf 数値・文字列 最初に出てきたもの NaN は消える。件数が多いと時間がかかる
new Map(arr.map(o => [o.id, o])) オブジェクト 後に出てきたもの 1行で書ける
Map と has で確認 オブジェクト 最初に出てきたもの for...of で書く
filter + findIndex オブジェクト 最初に出てきたもの 件数が多いと時間がかかる

配列を空にする(全削除)

方法は3つあり、「同じ配列を参照している別の変数にも反映されるか」が違います。

let a = [1, 2, 3]; const b = a;
a = [];                       // 新しい空配列を代入
console.log(a, b);

let c = [1, 2, 3]; const d = c;
c.length = 0;                 // 同じ配列を空にする
console.log(c, d);

let e = [1, 2, 3]; const f = e;
const removed = e.splice(0);  // 同じ配列を空にし、中身を受け取る
console.log(e, f, removed);

実行結果:

[] [ 1, 2, 3 ]
[] []
[] [] [ 1, 2, 3 ]
書き方 別の変数から見た配列 const の配列 向いている場面
arr = [] 元の中身が残る 使えない その変数だけ空にしたい
arr.length = 0 一緒に空になる 使える 共有している配列をまとめて空にしたい
arr.splice(0) 一緒に空になる 使える 削除した中身も受け取りたい

const で宣言した配列に [] を代入すると、次のエラーになります。中身の変更(length の代入や splice)は const でも行えます。

const a = [1, 2, 3];
try { a = []; } catch (e) { console.log(e.name + ': ' + e.message); }
a.length = 0;
console.log(a);

実行結果:

TypeError: Assignment to constant variable.
[]

delete演算子は「穴」が残る

delete で要素を消すと、長さは変わらず、その位置が空(empty)になります。

const a = ['a', 'b', 'c'];
const r = delete a[1];
console.log(r, a, a.length, 1 in a);
a.forEach((v, i) => console.log(' ', i, v));
console.log(JSON.stringify(a));

実行結果:

true [ 'a', <1 empty item>, 'c' ] 3 false
  0 a
  2 c
["a",null,"c"]

forEach では飛ばされ、JSON にすると null になる、というように、処理によって扱いが変わります。要素を詰めて削除したいときは splice か filter を使います。 すでに穴がある配列は、次のように詰められます。

const a = [1, , 3, undefined];
console.log(a.flat());
console.log(a.filter(() => true));

実行結果:

[ 1, 3, undefined ]
[ 1, 3, undefined ]

穴は取り除かれ、値として入っている undefined は残ります。なお toSpliced は穴を undefined として埋めます([1, , 3].toSpliced(0, 0) は [ 1, undefined, 3 ] になりました)。

速度の実測:10万件で比べる

要素数10万の配列で、各方法の時間を測りました(5回実行した最小値。配列を作る時間を含みます。数値は検証環境でのもので、マシンによって変わります)。

処理 時間
pop() を10万回(末尾から全部) 6.57 ms
shift() を10万回(先頭から全部) 896.40 ms
splice(0, 1) を10万回 2293.81 ms
filter で偶数を除く(1回) 5.76 ms
後ろから splice で偶数を除く 238.63 ms
splice(500, 1) を1000回 23.63 ms
toSpliced(500, 1) を1000回 358.38 ms
重複削除 [...new Set(arr)] 2.10 ms
重複削除 filter + indexOf 291.75 ms
オブジェクト重複削除 Map 7.94 ms
オブジェクト重複削除 filter + findIndex(2回実行の最小値) 278.58 ms

読み取れるポイントは次の4つです。

  • たくさんの要素を条件で取り除くなら filter が速いという結果でした。splice は削除のたびに後ろの要素を詰め直すため、回数が増えるほど時間がかかります。
  • 先頭からの削除(shift)は末尾(pop)より時間がかかります。 先頭から順に取り出す処理を大量に行うなら、位置を表す変数を進める方法や、逆順にして pop する方法があります。
  • toSpliced は毎回配列全体をコピーするので、ループの中で何度も呼ぶ用途には向きません。1回の更新で使うのが合っています。
  • 重複削除は Set と Map が速く、filter と indexOf(または findIndex)の組み合わせより100倍以上短い時間でした。

数百件程度の配列ではどれも一瞬なので、まずは読みやすさで選び、件数が増えたらこの表を参考にしてください。

どの方法を使うかの判断表

状況 おすすめ 理由
React の state など、新しい配列を渡したい filter / toSpliced 元の配列を書き換えない
条件に合うものをまとめて取り除く filter 簡潔で速い
位置がわかっている1件を取り除く(元を残す) toSpliced(i, 1) 削除後の配列が戻り値になる
位置がわかっている1件を取り除く(書き換える) splice(i, 1) コピーが発生しない
取り除いた要素を使いたい pop / shift / splice 戻り値で受け取れる
後から追加した順に取り出す(スタック) push + pop 末尾の操作は速い
先に追加した順に取り出す(キュー) push + shift 件数が多いときは位置の変数で管理
重複を除く Set / Map 件数が増えても速い
共有している配列を空にする arr.length = 0 同じ配列のまま空になる
flowchart TD
    A[配列から削除したい] --> B{元の配列を残す?}
    B -- 残す --> C{何で指定する?}
    C -- 位置 --> D["toSpliced(i, 1)"]
    C -- 値や条件 --> E[filter]
    C -- 重複 --> F["Set または Map"]
    B -- 書き換える --> G{どこを?}
    G -- 末尾 --> H[pop]
    G -- 先頭 --> I[shift]
    G -- 位置 --> J["splice(i, 1)"]
    G -- 全部 --> K["length = 0"]

よくある質問

Q. splice と filter はどちらを使えばよいですか? A. 元の配列を書き換えたい、または削除した要素を受け取りたいときは splice、元の配列を残して条件で取り除きたいときは filter です。複数の要素を条件で取り除く場合は、filter のほうが短く書けて、今回の測定でも速い結果でした。

Q. toSpliced が使えない環境ではどう書きますか? A. [...arr.slice(0, i), ...arr.slice(i + 1)] か arr.filter((_, idx) => idx !== i) で同じ結果になります。どちらも元の配列を書き換えません。

Q. 配列から特定の値を1つだけ削除するには? A. const i = arr.indexOf(value); if (i !== -1) arr.splice(i, 1); と書きます。indexOf が -1 を返したまま splice に渡すと末尾の要素が削除されるので、-1 の確認を入れておきます。

Q. オブジェクトの配列から id が重複するものを削除するには? A. [...new Map(arr.map(o => [o.id, o])).values()] で、id ごとに1件にまとめられます。この書き方は後に出てきた要素が残ります。先に出てきた要素を残したいときは、Map の has で確認してから set する書き方を使います。

まとめ

  • 末尾は pop、先頭は shift、位置指定は splice、条件は filter が基本の4つです。
  • splice の戻り値は「削除した要素の配列」です。削除後の配列を戻り値でほしいときは toSpliced か filter を使います。
  • 元の配列を残したいときは toSpliced(ES2023)・filter・slice を選びます。
  • indexOf の結果が -1 のまま splice に渡すと末尾が削除されます。 if (i !== -1) を入れておきます。
  • for ループの中で splice すると次の要素が1つ飛びます。 filter に置き換えるか、後ろから回します。
  • 重複削除は、数値・文字列なら Set、オブジェクトなら Map が簡潔で速い方法です。
  • 空にする方法は arr = [] と arr.length = 0 で、別の変数への影響が違います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript配列の要素削除|splice・filter・重複削除早見表」

株式会社RJCでは、JavaScriptの配列操作のような基礎の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?