ウェブエンジニア問題集
配列メソッド 3

配列のコピー・結合メソッド — slice・concat・浅いコピー

7
この章の目次開く

この章では、元の配列を壊さずに新しい配列を作る sliceconcat・スプレッド構文を扱います。 どれも便利ですが、オブジェクトが入った配列では浅いコピーの理解が重要です。

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"]
js

end に指定した位置の要素は含まれないため、slice(0, 3) は「0番目から3番目の手前まで」を取り出します。

第2引数を省略する

第2引数を省略すると、start から配列の最後までを取り出します。

const tasks = ['設計', '実装', 'レビュー', 'リリース'];
 
tasks.slice(1);
// ["実装", "レビュー", "リリース"]
js

マイナスのインデックス

slice では、末尾から数えるマイナスのインデックスも使えます。 最後の数件だけ取り出したいときに便利です。

const logs = ['10:00', '10:05', '10:10', '10:15', '10:20'];
 
const latest = logs.slice(-2);
// ["10:15", "10:20"]
js

浅いコピーを作る

引数を指定せずに 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"]
js

コピーした側に要素を追加しても、元の配列はそのままです。 ただし、このコピーは**浅いコピー(シャローコピー)**です。

浅いコピーの落とし穴 — 中身がオブジェクトのとき

学習者学習者

コピーに「浅い」なんてあるんですか…?じゃあ「深いコピー」もあるってこと…?

先生先生

いい勘だね。ポイントは配列の中身がオブジェクトかどうか。文字列や数値だけなら何も気にしなくていい。でもオブジェクトが入っていると、ちょっと不思議なことが起きるんだ。

const members = [{ name: '太郎' }, { name: '花子' }];
const copied = members.slice();
 
copied[0].name = '書き換えたよ';
 
console.log(copied[0].name);  // "書き換えたよ"
console.log(members[0].name); // "書き換えたよ" ← 元の配列まで変わってる!?
js
コピーしたはずの配列を変更したら元の配列まで変わって驚く様子
学習者学習者

ええっ!?コピーした方だけ書き換えたのに、元の members まで変わってます…!

先生先生

びっくりするよね。でも仕組みがわかれば怖くないよ。ポイントは、配列にはオブジェクトそのものではなく「参照」が入っているということなんだ。

整理すると、こうです。

  1. オブジェクトは、メモリ上のどこかに実体が1つ作られます
  2. 配列に入っているのは、その実体の場所を指し示す参照です
  3. slice() でコピーされるのは、この参照だけです
  4. つまりコピー後も、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"]
js

複数の配列を一度に結合する

const a = [1, 2];
const b = [3, 4];
const c = [5, 6];
 
const merged = a.concat(b, c);
// [1, 2, 3, 4, 5, 6]
js

配列でない値も追加できる

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]
js

ただし、ネストした配列は1段階しか展開されません

[1].concat([[2, 3]]);
// [1, [2, 3]]
js

スプレッド構文との比較

ES2015以降は、スプレッド構文 ... でも配列を結合できます。

const merged1 = a.concat(b);
const merged2 = [...a, ...b];
js
学習者学習者

concat とスプレッド構文、どっちを使えばいいの?

先生先生

どちらでも結果は同じだから、プロジェクトのスタイルに合わせればOK。スプレッド構文の方が短く書けるから現在のコードベースではよく使われるけど、concat は「結合している」という意図が名前から明確に伝わる利点があるよ。

実務パターン: 条件付きで要素を追加

concat は非破壊なので、条件に応じて要素を追加したい場面で安全に使えます。

const baseMenu = ['ホーム', 'プロフィール'];
 
const menu = baseMenu.concat(
  isAdmin ? ['管理画面'] : [],
  isLoggedIn ? ['ログアウト'] : ['ログイン'],
);
js

スプレッド構文で書く場合は、次のようになります。

const menu = [
  ...baseMenu,
  ...(isAdmin ? ['管理画面'] : []),
  isLoggedIn ? 'ログアウト' : 'ログイン',
];
js

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

  • slice は部分取得と浅いコピーに使える
  • concat とスプレッド構文は、元配列を壊さず結合できる
  • 非破壊メソッドでも、要素がオブジェクトなら中身は共有される
  • ネストしたオブジェクトまでコピーしたい場合は structuredClone を検討する

次は、元の配列を書き換える 配列の破壊的メソッド を見ていきます。

参考リンク