配列のコピー・結合メソッド — slice・concat・浅いコピー
この章の目次開く
この章では、元の配列を壊さずに新しい配列を作る slice・concat・スプレッド構文を扱います。
どれも便利ですが、オブジェクトが入った配列では浅いコピーの理解が重要です。
slice — 配列の一部を取り出して新しい配列を作る
slice は開始位置と終了位置を指定して、配列の一部を新しい配列として返します。
元の配列は変更されません(非破壊)。
構文: array.slice(start, end?)
const languages = ['JavaScript', 'TypeScript', 'React', 'Node.js', 'SQL'];
const frontend = languages.slice(0, 3);
console.log(frontend); // ["JavaScript", "TypeScript", "React"]
console.log(languages); // ["JavaScript", "TypeScript", "React", "Node.js", "SQL"]end に指定した位置の要素は含まれないため、slice(0, 3) は「0番目から3番目の手前まで」を取り出します。
第2引数を省略する
第2引数を省略すると、start から配列の最後までを取り出します。
const tasks = ['設計', '実装', 'レビュー', 'リリース'];
tasks.slice(1);
// ["実装", "レビュー", "リリース"]マイナスのインデックス
slice では、末尾から数えるマイナスのインデックスも使えます。
最後の数件だけ取り出したいときに便利です。
const logs = ['10:00', '10:05', '10:10', '10:15', '10:20'];
const latest = logs.slice(-2);
// ["10:15", "10:20"]浅いコピーを作る
引数を指定せずに slice() を呼ぶと、配列全体のコピーを作れます。
const original = ['A', 'B', 'C'];
const copied = original.slice();
copied.push('D');
console.log(copied); // ["A", "B", "C", "D"]
console.log(original); // ["A", "B", "C"]コピーした側に要素を追加しても、元の配列はそのままです。 ただし、このコピーは**浅いコピー(シャローコピー)**です。
浅いコピーの落とし穴 — 中身がオブジェクトのとき
学習者コピーに「浅い」なんてあるんですか…?じゃあ「深いコピー」もあるってこと…?
先生いい勘だね。ポイントは配列の中身がオブジェクトかどうか。文字列や数値だけなら何も気にしなくていい。でもオブジェクトが入っていると、ちょっと不思議なことが起きるんだ。
const members = [{ name: '太郎' }, { name: '花子' }];
const copied = members.slice();
copied[0].name = '書き換えたよ';
console.log(copied[0].name); // "書き換えたよ"
console.log(members[0].name); // "書き換えたよ" ← 元の配列まで変わってる!?
学習者ええっ!?コピーした方だけ書き換えたのに、元の members まで変わってます…!
先生びっくりするよね。でも仕組みがわかれば怖くないよ。ポイントは、配列にはオブジェクトそのものではなく「参照」が入っているということなんだ。
整理すると、こうです。
- オブジェクトは、メモリ上のどこかに実体が1つ作られます
- 配列に入っているのは、その実体の場所を指し示す参照です
slice()でコピーされるのは、この参照だけです- つまりコピー後も、2つの配列は同じ実体を指したままです
学習者なるほど…!じゃあ最初の例('A', 'B', 'C')で何も起きなかったのはどうしてですか?
先生文字列や数値などのプリミティブは参照ではなく、値そのものが配列に入っているからだよ。コピーすれば値ごと複製されるので、片方を変えてももう片方には影響しない。参照が絡むのはオブジェクトや配列が要素のときだけ、と覚えておこう。
concat — 配列を結合して新しい配列を作る
concat は2つ以上の配列を結合し、新しい配列を返します。元の配列は変更されません。
const front = ['HTML', 'CSS'];
const back = ['Node.js', 'SQL'];
const all = front.concat(back);
console.log(all); // ["HTML", "CSS", "Node.js", "SQL"]
console.log(front); // ["HTML", "CSS"]複数の配列を一度に結合する
const a = [1, 2];
const b = [3, 4];
const c = [5, 6];
const merged = a.concat(b, c);
// [1, 2, 3, 4, 5, 6]配列でない値も追加できる
const arr = [1, 2, 3];
arr.concat(4, 5); // [1, 2, 3, 4, 5]
arr.concat([4, 5], 6); // [1, 2, 3, 4, 5, 6]ただし、ネストした配列は1段階しか展開されません。
[1].concat([[2, 3]]);
// [1, [2, 3]]スプレッド構文との比較
ES2015以降は、スプレッド構文 ... でも配列を結合できます。
const merged1 = a.concat(b);
const merged2 = [...a, ...b];
学習者concat とスプレッド構文、どっちを使えばいいの?
先生どちらでも結果は同じだから、プロジェクトのスタイルに合わせればOK。スプレッド構文の方が短く書けるから現在のコードベースではよく使われるけど、concat は「結合している」という意図が名前から明確に伝わる利点があるよ。
実務パターン: 条件付きで要素を追加
concat は非破壊なので、条件に応じて要素を追加したい場面で安全に使えます。
const baseMenu = ['ホーム', 'プロフィール'];
const menu = baseMenu.concat(
isAdmin ? ['管理画面'] : [],
isLoggedIn ? ['ログアウト'] : ['ログイン'],
);スプレッド構文で書く場合は、次のようになります。
const menu = [
...baseMenu,
...(isAdmin ? ['管理画面'] : []),
isLoggedIn ? 'ログアウト' : 'ログイン',
];ちゃんと使うためのポイント
sliceは部分取得と浅いコピーに使えるconcatとスプレッド構文は、元配列を壊さず結合できる- 非破壊メソッドでも、要素がオブジェクトなら中身は共有される
- ネストしたオブジェクトまでコピーしたい場合は
structuredCloneを検討する
次は、元の配列を書き換える 配列の破壊的メソッド を見ていきます。