1. ホーム
  2. JavaScript

【JavaScript】push・pop・shift・unshift・splice の使い方|配列に要素を追加・削除する方法

Share

配列に値を足したり、いらなくなった要素を取り除いたりする処理は、JavaScript を書いていれば必ず出てきます。そのときに使うのが pushpopshiftunshiftsplice の5つのメソッドです。名前が似ていて「どれが先頭で、どれが末尾だったか」「戻り値は何だったか」で迷いやすいところでもあります。この記事では、5つの役割と戻り値を整理したうえで、splice の引数の使い分け、そして「元の配列を書き換えてしまう」という共通の性質と、その回避方法までを解説します。

5つのメソッドの役割と戻り値

まずは全体像を押さえておきましょう。追加・削除する位置と、返ってくる値が何かをまとめると次のようになります。「追加系は長さ、削除系は取り除いたもの」を返す、と覚えると整理しやすいです。

メソッド動作戻り値
push(...items)末尾に要素を追加する追加後の length(数値)
pop()末尾の要素を取り除く取り除いた末尾の要素。空配列なら undefined
shift()先頭の要素を取り除く取り除いた先頭の要素。空配列なら undefined
unshift(...items)先頭に要素を追加する追加後の length(数値)
splice(start, deleteCount, ...items)任意の位置で削除・挿入する取り除いた要素の配列。何も削除しなければ空配列

戻り値の型が「数値」「要素そのもの」「配列」とバラバラなので、ここを取り違えるとバグにつながります。特に splice は、削除した要素が1つだけでも配列で返る点に注意してください。

push と pop で末尾を操作する

push は配列の末尾に要素を追加するメソッドです。戻り値は追加した要素ではなく、追加したあとの配列の長さである点に注意しましょう。引数はいくつでも渡せるので、複数の値をまとめて追加できます。

main.js
const fruits = ['りんご'];

// 末尾に1つ追加する
fruits.push('みかん');
console.log(fruits); // => [ 'りんご', 'みかん' ]

// 複数の値をまとめて追加できる
const length = fruits.push('ぶどう', 'もも');

console.log(length); // => 4(追加後の length が返る)
console.log(fruits); // => [ 'りんご', 'みかん', 'ぶどう', 'もも' ]

pop は逆に、末尾の要素を1つ取り除きます。こちらの戻り値は取り除いた要素そのものです。取り除いた値をそのまま使いたい場面が多いため、この仕様は理にかなっています。

main.js
const stack = ['a', 'b', 'c'];

const last = stack.pop();

console.log(last);  // => 'c'(取り除いた要素)
console.log(stack); // => [ 'a', 'b' ]

// 空の配列に対して呼ぶと undefined が返る
const empty = [];
console.log(empty.pop()); // => undefined

空配列に pop を呼んでもエラーにはならず、undefined が返るだけです。ループで取り出し続けるようなコードでは、undefined が返ったかどうかではなく length を条件にするほうが安全です(要素として undefined が入っている可能性があるためです)。

shift と unshift で先頭を操作する

shift は先頭の要素を取り除いて、その要素を返します。unshift は先頭に要素を追加し、push と同じく追加後の length を返します。名前からは方向が分かりにくいので、「un が付くほうが追加」と覚えておくとよいでしょう。

main.js
const queue = ['a', 'b', 'c'];

// 先頭を取り除く
const first = queue.shift();

console.log(first); // => 'a'(取り除いた要素)
console.log(queue); // => [ 'b', 'c' ]

// 先頭に複数まとめて追加する
const length = queue.unshift('x', 'y');

console.log(length); // => 4(追加後の length)
console.log(queue);  // => [ 'x', 'y', 'b', 'c' ]

unshift('x', 'y') のように複数渡した場合、渡した順番のまま先頭に並びます。1つずつ unshift を2回呼ぶと順序が逆になってしまうので、まとめて渡すほうが意図どおりになります。

なお shiftunshift は、先頭を変更したあとに残りの要素のインデックスをすべてずらす必要があります。要素数が非常に多い配列で毎フレーム呼ぶような使い方では、末尾を操作する push / pop に比べて処理が重くなりやすい点は頭の片隅に置いておくとよいでしょう。

splice で好きな位置を削除・挿入する

先頭・末尾以外の任意の位置を操作したいときに使うのが splice です。書式は array.splice(start, deleteCount, ...items) で、引数の意味は次のとおりです。

引数意味
start操作を始める位置(0 から数えるインデックス)。負の数を渡すと末尾から数えた位置になる
deleteCountstart から削除する個数。0 を渡すと削除せず挿入だけ行う。省略すると start から末尾まですべて削除される
...items削除した位置に挿入する要素。省略すると削除だけを行う

削除と挿入をひとつのメソッドで兼ねているため、渡す引数によって「削除」「挿入」「置き換え」の3つの使い方ができます。順番に見ていきましょう。

削除する(start と deleteCount)

挿入する要素を渡さなければ、単純な削除になります。戻り値は取り除いた要素の配列です。

main.js
const items = ['a', 'b', 'c', 'd', 'e'];

// インデックス 1 から 2 個削除する
const removed = items.splice(1, 2);

console.log(removed); // => [ 'b', 'c' ](取り除いた要素の配列)
console.log(items);   // => [ 'a', 'd', 'e' ]

deleteCount を省略すると、start の位置から末尾までがすべて削除されます。「1個だけ消したいのに全部消えてしまった」というミスは、この省略時の挙動を知らないことが原因であることが多いです。1個だけ消したいときは必ず 1 を明示します。

main.js
const items = ['a', 'b', 'c', 'd'];

// deleteCount を省略 → インデックス 2 から末尾まで削除される
const removed = items.splice(2);

console.log(removed); // => [ 'c', 'd' ]
console.log(items);   // => [ 'a', 'b' ]

挿入する(deleteCount に 0 を渡す)

deleteCount0 を渡すと、1つも削除せずに start の位置へ要素を挿入できます。何も削除していないので、戻り値は空配列です。

main.js
const items = ['a', 'b', 'c'];

// インデックス 2 の位置に、削除せず 2 つ挿入する
const removed = items.splice(2, 0, 'x', 'y');

console.log(removed); // => [](削除していないので空配列)
console.log(items);   // => [ 'a', 'b', 'x', 'y', 'c' ]

挿入された要素は、もともと start の位置にあった要素(この例では 'c')の手前に入ります。

置き換える(削除と挿入を同時に行う)

削除する個数と挿入する要素の両方を指定すれば、置き換えになります。削除数と挿入数が一致している必要はないので、2個消して1個入れる、といった操作もできます。

main.js
const items = ['a', 'b', 'c', 'd'];

// インデックス 1 から 2 個削除して、代わりに 'X' を挿入する
const removed = items.splice(1, 2, 'X');

console.log(removed); // => [ 'b', 'c' ]
console.log(items);   // => [ 'a', 'X', 'd' ]

start に負の数を渡す

start に負の数を渡すと、末尾から数えた位置として扱われます。-1 が最後の要素、-2 が最後から2番目です。要素数が分からない配列の後ろのほうを操作したいときに便利です。

main.js
const items = ['a', 'b', 'c', 'd'];

// -2 は末尾から2番目('c')の位置。そこから1個削除する
const removed = items.splice(-2, 1);

console.log(removed); // => [ 'c' ]
console.log(items);   // => [ 'a', 'b', 'd' ]

なお、負の数を渡しても削除は右方向(末尾に向かって)進みますsplice(-2, 1) は「末尾から2番目を起点に、そこから1個」という意味であり、後ろから数えて削除するわけではない点に注意してください。

5つとも元の配列を書き換える

ここまで見てきた5つのメソッドには、共通する重要な性質があります。それはすべて元の配列そのものを変更する(破壊的メソッド)ということです。mapfilter のように新しい配列を返すのではなく、呼び出した配列の中身が書き換わります。

main.js
const original = ['a', 'b', 'c'];
const alias = original; // 同じ配列を参照しているだけ

alias.push('d');

// 別名で操作しても、参照先は同じ配列なので両方変わる
console.log(alias);    // => [ 'a', 'b', 'c', 'd' ]
console.log(original); // => [ 'a', 'b', 'c', 'd' ]

関数の引数として受け取った配列に pushsplice を使うと、呼び出し元の配列まで変わってしまいます。これは意図した設計であればよいのですが、そうでなければ原因の分かりにくいバグになります。

コピーしてから操作する

元の配列を残したい場合の一番手軽な方法は、先に複製を作ってから破壊的メソッドを使うことです。複製にはスプレッド構文(...)や slice() を使います。slice は名前が splice と似ていますが、こちらは元の配列を変更せず、切り出した部分を新しい配列として返すメソッドです。

main.js
const original = ['a', 'b', 'c'];

// スプレッド構文で複製してから操作する
const copy = [...original];
copy.splice(1, 1);

console.log(copy);     // => [ 'a', 'c' ]
console.log(original); // => [ 'a', 'b', 'c' ](元のまま)

// 引数なしの slice() でも複製できる
const copy2 = original.slice();
copy2.push('d');
console.log(original); // => [ 'a', 'b', 'c' ]

末尾や先頭への追加だけであれば、そもそも push を使わずに [...original, 'd']['x', ...original] と書くほうが簡潔です。削除も、条件で絞り込めるなら filter で新しい配列を作れます。

非破壊版のメソッドを使う

比較的新しい JavaScript には、破壊的メソッドの非破壊版が用意されています。toSpliced()splice() と同じ引数を取りつつ元の配列を変更せず、変更後の新しい配列を返します。with() は指定インデックスの値だけを差し替えた新しい配列を返します。ほかに toSorted()toReversed() も同様の位置づけです。

main.js
const items = ['a', 'b', 'c', 'd'];

// toSpliced: splice と同じ引数で、新しい配列を返す
const spliced = items.toSpliced(1, 2);
console.log(spliced); // => [ 'a', 'd' ]
console.log(items);   // => [ 'a', 'b', 'c', 'd' ](元のまま)

// with: 指定インデックスだけ差し替えた新しい配列を返す
const replaced = items.with(1, 'X');
console.log(replaced); // => [ 'a', 'X', 'c', 'd' ]
console.log(items);    // => [ 'a', 'b', 'c', 'd' ](元のまま)

ただし toSplicedwithtoSortedtoReversed後から追加された比較的新しい APIです。主要なモダンブラウザや近年の Node.js では利用できますが、古い環境をサポートする必要がある場合は動作しないことがあります。導入前に MDN のブラウザー互換性の表や、プロジェクトのターゲット環境(browserslist の設定など)を確認しておくと安心です。使えない環境が対象に含まれるなら、スプレッド構文と slice の組み合わせで同じことが実現できます。

push したのに画面が更新されないとき

React などのフレームワークで配列を state として持っているとき、「push したはずなのにリストが増えない」という現象に出会うことがあります。これは push の破壊的な性質が原因です。

参照が変わらないと再レンダリングされない

React は state が更新されたかどうかを、値の中身ではなく参照が変わったかで判定します。push は同じ配列の中身を書き換えるだけなので、更新前と更新後で参照が同じままです。その結果、React は「変わっていない」と判断し、再レンダリングが起きません。

TodoList.jsx
const [items, setItems] = useState(['りんご', 'みかん']);

// NG: 同じ配列を書き換えて渡している
function addItemNg(name) {
  items.push(name); // 中身は増えるが、参照は変わらない
  setItems(items);  // 前回と同じ参照なので更新とみなされない
}

新しい配列を作って渡す

解決策は単純で、毎回新しい配列を作って state にセットすることです。追加はスプレッド構文、削除は filtertoSpliced、1件だけの差し替えは mapwith で書けます。

TodoList.jsx
// OK: 新しい配列を作って渡す
function addItem(name) {
  setItems((prev) => [...prev, name]);
}

// 削除も、splice ではなく filter で新しい配列を作る
function removeItem(index) {
  setItems((prev) => prev.filter((_, i) => i !== index));
}

同じ考え方は Vue の一部の場面や、状態を比較して差分を検出するライブラリ全般にも当てはまります。状態として扱う配列には破壊的メソッドを使わない、と決めておくと事故を防ぎやすくなります。

delete で要素を消してはいけない理由

配列から要素を削除する方法として delete arr[0] を思いつくかもしれませんが、これは配列に対しては使うべきではありません。delete はオブジェクトのプロパティを削除する演算子で、配列に使うとその位置が「空スロット」になるだけで、要素は詰められず length も変わらないからです。

main.js
const items = ['a', 'b', 'c'];

delete items[0];

console.log(items);        // => [ <1 empty item>, 'b', 'c' ]
console.log(items.length); // => 3(減らない)
console.log(items[0]);     // => undefined
console.log(0 in items);   // => false(キー自体が存在しない)

// 空スロットは forEach や map でスキップされる
items.forEach((value) => console.log(value));
// => 'b'
// => 'c'

厄介なのは、items[0] を読むと undefined が返るのに、forEachmap では「そこに値がない」として飛ばされる、という一貫しない振る舞いをすることです。length を基準にしたループでは undefined として処理され、forEach では処理されない、といったずれが起きます。

要素を本当に取り除いて後ろを詰めたいなら splice を使います。元の配列を残したいなら filtertoSpliced で新しい配列を作りましょう。

main.js
const items = ['a', 'b', 'c'];

// 正しい削除:後ろの要素が詰められ、length も減る
items.splice(0, 1);

console.log(items);        // => [ 'b', 'c' ]
console.log(items.length); // => 2

まとめ

push は末尾に、unshift は先頭に要素を追加し、どちらも複数の値をまとめて渡せて追加後の length を返します。pop は末尾、shift は先頭の要素を取り除き、取り除いた要素(空配列なら undefined)を返します。splice(start, deleteCount, ...items) は任意の位置で削除・挿入・置き換えができ、取り除いた要素の配列を返します。deleteCount を省略すると末尾まで削除される点と、0 を渡せば挿入だけできる点は覚えておきましょう。

そして5つに共通するのは、いずれも元の配列を書き換える破壊的メソッドだということです。元の配列を残したいときはスプレッド構文や slice() で複製してから操作するか、toSpliced()with() といった非破壊版のメソッド(対応環境の確認は必要です)を使います。React の state のように参照の変化で更新を判定する場面では、破壊的メソッドを避けて新しい配列を作るのが基本です。また、要素の削除に delete を使うと空スロットが残って length も変わらないので、削除には必ず splicefilter を使ってください。

参考ページ