7.

JavaScript の配列|破壊的メソッドと sort の落とし穴

編集
この記事の要点
  • 配列は [] で作る。キーは常に数値。文字列キーが要るならオブジェクトか Map
  • 元を書き換えるメソッドと新しい配列を返すメソッドがある。取り違えが最も多い事故
  • sort()既定で文字列として比較する。数値は比較関数が必須
  • コピーは [...arr]入れ子の中身までは複製されない
  • 存在確認は includes()indexOf() !== -1 より読みやすい

作り方と基本操作

const fruits = ["りんご", "みかん", "ぶどう"];

console.log(fruits[0]);        // りんご
console.log(fruits.at(-1));    // ぶどう   末尾は at(-1)
console.log(fruits.length);    // 3

fruits[1] = "レモン";           // 更新
fruits.push("いちご");          // 末尾に追加
fruits.pop();                  // 末尾を取り出す
fruits.unshift("桃");           // 先頭に追加
fruits.shift();                // 先頭を取り出す

// 型は混在できる(推奨はしない)
const mixed = [1, "a", true, null, { id: 1 }, [1, 2]];

// 長さを指定して作る
const zeros = new Array(3).fill(0);          // [0, 0, 0]
const range = Array.from({ length: 3 }, (_, i) => i);   // [0, 1, 2]

// 存在しない添字は undefined(エラーにならない)
console.log(fruits[99]);       // undefined

length は書き換えられる

const a = [1, 2, 3];

a.length = 1;
console.log(a);        // [1]        ← 切り詰められる

a[5] = 9;
console.log(a);        // [1, 空 ×4, 9]
console.log(a.length); // 6
console.log(a[2]);     // undefined  ← 穴が空く(疎な配列)

// 穴があると挙動が分かれる
[, , 1].forEach((v) => console.log(v));   // 1 だけ(穴は飛ばす)
[, , 1].map((v) => 0);                    // [空, 空, 0](穴は残る)
Array.from([, , 1]);                       // [undefined, undefined, 1]

添字を飛ばして代入すると「穴」ができ、メソッドごとに扱いが変わります。末尾への追加は必ず push() を使ってください。

元を変えるか、新しく作るか

元を書き換える(破壊的)新しい配列を返す(非破壊)
push pop shift unshiftconcat slice [...a]
splicetoSpliced(新しい環境)
sorttoSorted
reversetoReversed
fill copyWithinmap filter flat
const nums = [3, 1, 2];

const sorted1 = nums.sort();       // nums 自体も並べ替わる
console.log(nums);                 // [1, 2, 3]

const nums2 = [3, 1, 2];
const sorted2 = [...nums2].sort(); // コピーしてから並べ替える
console.log(nums2);                // [3, 1, 2]  元は無事

// splice は「削除して返す」。戻り値は削除した要素
const a = [1, 2, 3, 4];
const removed = a.splice(1, 2);    // 添字 1 から 2 件削除
console.log(a, removed);           // [1, 4]  [2, 3]

// slice は「切り出して返す」。元は変わらない
const b = [1, 2, 3, 4];
console.log(b.slice(1, 3), b);     // [2, 3]  [1, 2, 3, 4]

spliceslice は名前が似ていて挙動が正反対です。React などで状態を扱うときに sortsplice を直接使うと、元の状態を壊して再描画されない原因になります。

sort の落とし穴

console.log([10, 9, 100].sort());                 // [10, 100, 9]  ← 文字列として比較
console.log([10, 9, 100].sort((a, b) => a - b));  // [9, 10, 100]  昇順
console.log([10, 9, 100].sort((a, b) => b - a));  // [100, 10, 9]  降順

// 文字列は localeCompare
const names = ["さくら", "あおい", "かえで"];
console.log([...names].sort((a, b) => a.localeCompare(b, "ja")));

// オブジェクトの配列
const users = [{ name: "田中", age: 30 }, { name: "鈴木", age: 25 }];
users.sort((a, b) => a.age - b.age);

// 複数キー: 年齢の降順、同じなら名前の昇順
users.sort((a, b) => b.age - a.age || a.name.localeCompare(b.name, "ja"));

sort() を引数なしで数値に使ってはいけません。要素を文字列に変換してから辞書順で並べるため、[10, 9][10, 9] のままになります。

よく使うメソッド

const nums = [1, 2, 3, 4, 5];

nums.map((n) => n * 2);                    // [2,4,6,8,10]   変換
nums.filter((n) => n % 2 === 0);           // [2,4]          絞り込み
nums.reduce((acc, n) => acc + n, 0);       // 15             集計
nums.find((n) => n > 3);                   // 4              最初の 1 件
nums.findIndex((n) => n > 3);              // 3              その添字
nums.findLast((n) => n < 4);               // 3              後ろから
nums.some((n) => n > 4);                   // true           1 つでも
nums.every((n) => n > 0);                  // true           すべて
nums.includes(3);                          // true           含まれるか
nums.indexOf(3);                           // 2              位置(無ければ -1)
nums.join(", ");                           // "1, 2, 3, 4, 5"
nums.flat();                               // 入れ子を平らに
nums.flatMap((n) => [n, n]);               // map してから平らに

// 連結とコピー
const merged = [...nums, ...[6, 7]];       // スプレッド(推奨)
const merged2 = nums.concat([6, 7]);

// 重複を除く
const uniq = [...new Set([1, 1, 2, 2, 3])];   // [1, 2, 3]

// グループ化(新しい環境)
Object.groupBy(nums, (n) => (n % 2 ? "odd" : "even"));

コピーは浅い

const a = [{ id: 1 }];
const b = [...a];        // 配列は新しくなるが、中のオブジェクトは共有される
b[0].id = 99;
console.log(a[0].id);    // 99

// 中身まで複製する
const c = structuredClone(a);      // 標準の深いコピー
c[0].id = 1;
console.log(a[0].id);              // 99(影響しない)

JSON.parse(JSON.stringify(a)) でも深いコピーになりますが、Date が文字列になり undefined や関数が消えます。structuredClone() のほうが安全です。

ループ中に配列を変更しない

const nums = [1, 2, 3, 4];

// 危険: 削除すると後ろがずれて飛ばされる
nums.forEach((n, i) => {
  if (n % 2 === 0) nums.splice(i, 1);
});

// 正しい: 新しい配列を作る
const odds = [1, 2, 3, 4].filter((n) => n % 2 !== 0);

// その場で置き換えたいとき
nums.length = 0;
nums.push(...odds);

配列かどうかの判定

console.log(typeof []);            // "object"   ← 判定に使えない
console.log(Array.isArray([]));    // true       これを使う

// 配列風オブジェクトを配列にする
const nodes = document.querySelectorAll("li");   // NodeList(配列ではない)
const arr = Array.from(nodes);                   // 配列になる
// nodes.map(...) は使えないが Array.from すれば使える

多次元配列

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
];

console.log(matrix[1][2]);        // 6
console.log(matrix.length);       // 2   行数
console.log(matrix[0].length);    // 3   列数

// 平らにする
console.log(matrix.flat());       // [1,2,3,4,5,6]
console.log([[1,[2]]].flat(2));   // [1,2]   深さを指定

// 行と列を入れ替える
const t = matrix[0].map((_, i) => matrix.map((row) => row[i]));
console.log(t);                   // [[1,4],[2,5],[3,6]]

// 初期化: fill だけだと同じ配列を共有してしまう
const bad = new Array(2).fill([]);
bad[0].push(1);
console.log(bad);                 // [[1], [1]]   ← 両方に入る

const good = Array.from({ length: 2 }, () => []);
good[0].push(1);
console.log(good);                // [[1], []]

fill([])fill({}) は「同じ 1 個」を全要素に入れます。行ごとに別の配列が要る場合は Array.from() で毎回作ってください。

配列風オブジェクト

function f() {
  console.log(arguments);          // 配列風(length はあるが map は無い)
  console.log([...arguments]);     // 配列にする
}

// DOM のコレクション
const nodes = document.querySelectorAll("li");   // NodeList
nodes.forEach((n) => {});                        // forEach は使える
// nodes.map(...)                                 // TypeError
[...nodes].map((n) => n.textContent);            // 配列にしてから

// 文字列も 1 文字ずつ取り出せる
[..."あいう"];                    // ["あ","い","う"]
Array.from("abc");                // ["a","b","c"]

// length を持つオブジェクトから作る
Array.from({ length: 3 }, (_, i) => i * 2);   // [0, 2, 4]

[...x] が使えるのは反復可能なもの(配列・文字列・Set Map NodeList)です。{ length: 3 } のような単なる配列風オブジェクトには Array.from() を使います。

要素の検索と削除

const users = [
  { id: 1, name: "田中" },
  { id: 2, name: "鈴木" },
];

// 検索
const user = users.find((u) => u.id === 2);
const index = users.findIndex((u) => u.id === 2);

// オブジェクトの includes は「同じ実体か」で判定する
console.log(users.includes({ id: 1, name: "田中" }));   // false
console.log(users.some((u) => u.id === 1));            // true

// 削除(元を変えない)
const removed = users.filter((u) => u.id !== 2);

// 更新(元を変えない)
const updated = users.map((u) => (u.id === 2 ? { ...u, name: "佐藤" } : u));

// 追加(元を変えない)
const added = [...users, { id: 3, name: "高橋" }];

オブジェクトの比較は参照で行われます。中身が同じでも別のインスタンスなら includes()indexOf() は見つけられません。

性能の目安

操作計算量補足
push / pop / arr[i]O(1)速い
shift / unshift / spliceO(n)全要素をずらす
includes / indexOf / findO(n)件数が多いなら Set / Map
sortO(n log n)比較関数が重いと効く
スプレッドで結合をループ内で繰り返すO(n²)要注意
// 遅い: 毎回すべてコピーしている
let result = [];
for (const item of items) result = [...result, item];

// 速い
const result2 = [];
for (const item of items) result2.push(item);

// 大量データの存在確認は Set にする
const ids = new Set(users.map((u) => u.id));
console.log(ids.has(2));      // O(1)

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 記述方法
  2. コメント
  3. 変数の宣言
  4. 関数
  5. 演算子
  6. 条件文
  7. 配列
  8. 連想配列
  9. ループ処理
  10. 非同期処理
  11. 同期処理
  12. 確認ウィンドウを表示する方法
  13. 文字の置換
  14. base urlを取得する方法
  15. formのsubmit前にjavascriptを呼び出す方法
  16. undefinedのイコール判定
  17. Javascript のみで form を post で submit する方法

最近更新/作成されたページ