本文へスキップ
ウェブエンジニア問題集
配列メソッド 4

配列の破壊的メソッド — sort・splice・push と非破壊の代替

約16分
この章の目次開く

配列メソッドには、元の配列をそのまま残して新しい値を返すものと、元の配列を直接変更するものがあります。 この章では、後者の破壊的メソッドをまとめます。

破壊的メソッド早見表

メソッド何をする戻り値非破壊の代替
push(...items)末尾に追加追加後の配列の長さ[...arr, value] / concat
pop()末尾を削除取り除いた要素slice(0, -1)
unshift(...items)先頭に追加追加後の配列の長さ[value, ...arr]
shift()先頭を削除取り除いた要素slice(1)
splice(start, deleteCount, ...items)途中の追加・削除・置換削除した要素の配列toSpliced()
sort(compareFn?)並び替え並び替えた元の配列(同じ参照)toSorted()
reverse()逆順にする逆順にした元の配列(同じ参照)toReversed()
fill(value, start?, end?)指定値で埋める埋めた元の配列(同じ参照)コピーしてから fill
copyWithin(target, start, end?)配列内で要素をコピー元の配列(同じ参照)目的に応じて slice / map
arr[i] = value要素を置き換える代入した値そのものwith(i, value)
学習者学習者

const newItems = items.push('C') って書いたら、newItems が 3 になってハマりました……。追加後の配列が返ってくると思ってたのに。

破壊的メソッドでいちばん事故が多いのが、この戻り値の勘違いです。破壊的メソッドは「変更後の新しい配列」を返しません。返るのは「長さ」「取り除いた要素」「削除した要素の配列」「元の配列そのもの」のいずれかです。以降の節では、メソッドごとに引数と戻り値を明示していきます。

push / pop — 末尾に追加・削除する

push は末尾に要素を追加し、pop は末尾の要素を取り除きます。 どちらも元の配列を変更します。

構文: array.push(...items) / array.pop()

メソッド引数戻り値
push(...items)末尾に入れる値(複数可)追加後の配列の長さ(number)
pop()なし取り除いた末尾要素。空配列なら undefined
const items = ['A', 'B'];
 
items.push('C');
console.log(items); // ["A", "B", "C"]
 
const last = items.pop();
console.log(last); // "C"
console.log(items); // ["A", "B"]
js

元の配列を変えたくない場合は、スプレッド構文や slice を使います。

const items = ['A', 'B'];
 
const added = [...items, 'C'];
const removedLast = items.slice(0, -1);
 
console.log(items); // ["A", "B"]
js

shift / unshift — 先頭に追加・削除する

unshift は先頭に要素を追加し、shift は先頭の要素を取り除きます。 どちらも元の配列を変更します。

構文: array.unshift(...items) / array.shift()

メソッド引数戻り値
unshift(...items)先頭に入れる値(複数可)追加後の配列の長さ
shift()なし取り除いた先頭要素。空配列なら undefined
const queue = ['B', 'C'];
 
queue.unshift('A');
console.log(queue); // ["A", "B", "C"]
 
const first = queue.shift();
console.log(first); // "A"
console.log(queue); // ["B", "C"]
js

shift 単体で見ると、先頭の1件を取り出して残りの要素が前に詰まります。 キューのように「来た順に1件ずつ処理する」ときは、オブジェクトの配列(レコード)でも同じです。

const tasks = [
  { id: 1, title: '設計' },
  { id: 2, title: '実装' },
  { id: 3, title: 'レビュー' },
];
 
const current = tasks.shift();
 
console.log(current); // { id: 1, title: "設計" }
console.log(tasks); // [{ id: 2, title: "実装" }, { id: 3, title: "レビュー" }]
js

空の配列に shift すると戻り値は undefined です。ループで使うときは、先に長さを確認します。

const pending = [
  { id: 1, title: '設計' },
  { id: 2, title: '実装' },
];
 
while (pending.length > 0) {
  const task = pending.shift();
  run(task);
}
js

非破壊で書くなら、追加は [value, ...arr]、削除は slice(1) です。

const queue = ['B', 'C'];
 
const added = ['A', ...queue];
const removedFirst = queue.slice(1);
js

splice — 途中の追加・削除・置換

splice は配列の途中に対して、削除・追加・置換を行うメソッドです。 柔軟ですが、元の配列を直接変更するため注意が必要です。

構文: array.splice(start, deleteCount, ...items)

引数渡せるもの説明
start(第1引数)数値変更を始める位置。負の数なら末尾から数える(-1 は最後の要素)
deleteCount(第2引数)数値・省略可start から削除する個数。省略すると start 以降を全部削除。0 なら削除なし
...items(第3引数以降)任意の値・省略可start の位置に挿入する要素。省略すると削除だけを行う

戻り値: 削除された要素の配列。1件だけ削除した場合も配列で返る。何も削除しなければ空配列 []

引数の組み合わせで「削除・挿入・置換」を1つのメソッドで賄うため、splice は何をしているのか読み取りにくくなりがちです。やりたいことから引くと次のようになります。

やりたいこと書き方戻り値
途中の2件を削除arr.splice(1, 2)削除した2件の配列
削除せず挿入だけarr.splice(1, 0, 'X')[](削除0件のため)
1件を別の値に置換arr.splice(1, 1, 'X')削除した1件の配列
その位置以降を全部削除arr.splice(1)削除した要素の配列
末尾の1件を削除arr.splice(-1, 1)削除した1件の配列

コードで確認します。

const tasks = ['設計', '実装', 'レビュー'];
 
tasks.splice(1, 1, 'テスト'); // 1番目を1件削除して 'テスト' を挿入 = 置換
 
console.log(tasks); // ["設計", "テスト", "レビュー"]
js
const items = ['A', 'B', 'C', 'D'];
 
console.log(items.splice(1, 0, 'X')); // [] ← 削除していないので空配列
console.log(items); // ["A", "X", "B", "C", "D"]
 
console.log(items.splice(2)); // ["B", "C", "D"] ← 第2引数を省略すると以降を全部削除
console.log(items); // ["A", "X"]
js

オブジェクトの id など、条件に合う最初の要素を1件削除する場合は、findIndex で位置を探してから splice する方法を使います。

ES2023以降は、非破壊の toSpliced が使えます。

const tasks = ['設計', '実装', 'レビュー'];
const updated = tasks.toSpliced(1, 1, 'テスト');
 
console.log(updated); // ["設計", "テスト", "レビュー"]
console.log(tasks); // ["設計", "実装", "レビュー"]
js
先生先生

splice と toSpliced は引数がまったく同じで、戻り値だけが違うと覚えると混乱しません。splice は「取り除いたもの」を、toSpliced は「作り直した配列」を返します。

sort — 並び替える

sort は配列を並び替えますが、元の配列を直接変更します。

構文: array.sort(compareFn?)

引数渡せるもの説明
compareFn(省略可)(a, b) => number2要素の並び順を決める関数。省略すると文字列に変換して比較する

戻り値: 並び替えたあとの配列。ただし新しい配列ではなく、元の配列と同じ参照が返る

const scores = [80, 100, 60];
 
const sorted = scores.sort((a, b) => a - b);
 
console.log(sorted); // [60, 80, 100]
console.log(scores); // [60, 80, 100] ← 元も並び替わっている
console.log(sorted === scores); // true ← コピーではなく同じ配列
js

比較関数の戻り値の符号で並び順が決まります。ここが sort のいちばん分かりにくい部分です。

compareFn(a, b) の戻り値並び順
負の数a を b より前に置く
0順序を変えない(ES2019以降は安定ソートを保証)
正の数b を a より前に置く
数値なら (a, b) => a - b で昇順、(a, b) => b - a で降順、と覚えておけば足ります。

比較関数を省略した sort() は、要素を文字列に変換してから比較します。数値の並び替えでは意図しない順番になるため、比較関数を必ず渡します。

[10, 9, 1].sort(); // [1, 10, 9] ← "10" < "9" として比較される
 
[10, 9, 1].sort((a, b) => a - b); // [1, 9, 10]
js

文字列の並び替えでも、大文字小文字が混ざると意図とズレます。コード単位の比較では大文字がすべて先に来るためです。こうした場合は localeCompare を比較関数に使います。

['banana', 'apple', 'Cherry'].sort(); // ["Cherry", "apple", "banana"] ← 大文字が先
['banana', 'apple', 'Cherry'].sort((a, b) => a.localeCompare(b)); // ["apple", "banana", "Cherry"]
js

元の配列を変えたくない場合は、ES2023の toSorted を使います。

const scores = [80, 100, 60];
const sorted = scores.toSorted((a, b) => a - b);
 
console.log(sorted); // [60, 80, 100]
console.log(scores); // [80, 100, 60]
js

toSorted が使えない環境では、コピーしてから sort します。

const sorted = [...scores].sort((a, b) => a - b);
js

reverse — 逆順にする

reverse は配列の順番を逆にします。 これも元の配列を直接変更します。

構文: array.reverse()(引数なし)

戻り値: 逆順にしたあとの配列。sort と同じく、元の配列と同じ参照が返る

const steps = ['入力', '確認', '完了'];
 
steps.reverse();
console.log(steps); // ["完了", "確認", "入力"]
js

非破壊で書くなら toReversed を使います。

const steps = ['入力', '確認', '完了'];
const reversed = steps.toReversed();
 
console.log(reversed); // ["完了", "確認", "入力"]
console.log(steps); // ["入力", "確認", "完了"]
js

with — 要素の置き換えを非破壊で行う

配列の要素を arr[i] = value で書き換えるのも、元の配列を直接変更する操作です。

const users = ['太郎', '花子', '次郎'];
 
users[1] = '佐藤';
console.log(users); // ["太郎", "佐藤", "次郎"]
js

ES2023の with を使うと、指定した位置だけ置き換えた新しい配列を作れます。

構文: array.with(index, value)

引数渡せるもの説明
index数値置き換える位置。負の数なら末尾から数える(-1 は最後)
value任意の値その位置に入れる新しい値

戻り値: 1要素だけ置き換えた新しい配列。元の配列は変わらない。範囲外の index を渡すと RangeError

const users = ['太郎', '花子', '次郎'];
const updated = users.with(1, '佐藤');
 
console.log(updated); // ["太郎", "佐藤", "次郎"]
console.log(users); // ["太郎", "花子", "次郎"]
 
console.log(users.with(-1, '田中')); // ["太郎", "花子", "田中"]
users.with(10, '山本'); // RangeError: Invalid index : 10
js

ES2023の非破壊メソッド早見表

破壊的メソッドと引数は同じで、戻り値だけが「新しい配列」に変わるのがこの4つです。

メソッド引数戻り値
toSorted(compareFn?)sort と同じ並び替えた新しい配列
toReversed()なし逆順にした新しい配列
toSpliced(start, deleteCount?, ...items)splice と同じ変更後の新しい配列(削除要素の配列ではない)
with(index, value)位置と新しい値1要素だけ置き換えた新しい配列

実務パターン: 状態更新では非破壊にする

Reactのstateのように、変更検知が参照の違いに依存する場面では、元配列を直接変更しない書き方が基本です。

破壊的メソッドと非破壊の書き方の対比
state更新では「元の配列を触る」か「新しい配列を作る」かが分かれ目
// ❌ 元の配列を直接変更している
items.push(newItem);
setItems(items);
 
// ⭕ 新しい配列を作る
setItems([...items, newItem]);
js

削除・並び替え・置き換えも同じです。

// 削除
setItems(items.filter((item) => item.id !== targetId));
 
// 並び替え
setItems(items.toSorted((a, b) => a.order - b.order));
 
// 置き換え
setItems(items.map((item) => (item.id === targetId ? { ...item, done: true } : item)));
js

ちゃんと使うためのポイント

  • push / pop / shift / unshift / splice / sort / reverse / fill は元配列を変更する
  • 破壊的メソッドの戻り値は「変更後の新しい配列」ではない。push / unshift は長さ、pop / shift は取り除いた要素、splice は削除した要素の配列、sort / reverse は元の配列と同じ参照
  • splice(start, deleteCount, ...items) は第2引数を省略すると以降を全部削除し、0 を渡すと挿入だけを行う
  • sort は比較関数を渡す。戻り値が負なら a が前、正なら b が前。数値は (a, b) => a - b で昇順
  • ES2023では toSorted / toReversed / toSpliced / with が非破壊の代替になり、こちらは戻り値が新しい配列になる
  • UIのstate更新では、元配列を直接変更せず新しい配列を作る

次は、初期化や平坦化で使う 配列の初期化・平坦化メソッド を見ていきます。

参考リンク