Q1const backup = order; と書いたあと backup.total = 0; を実行すると、order.total はどうなりますか?
参照とコピー — 配列・オブジェクトの落とし穴
代入したつもりが同じものを指していて、片方を直すともう片方も変わる仕組みを扱います。スプレッド構文による浅いコピー、structuredClone による深いコピー、Object.freeze まで進みます。
注文内容を書き換える前にバックアップを取っておこうとして const backup = order; と書くと、あとから order を直したときに backup まで同じ値に変わります。数値や文字列ではこうしたことは起きません。
この記事では、配列やオブジェクトの代入で何が渡っているのか、そして安全にコピーを作る書き方を扱います。
代入で渡るもの — プリミティブと参照
この食い違いを追いかけるのが難しいのは、代入したときに渡るものが値の種類によって違うからです。何が代入でそのまま写され、何が写されないのかを、ここで整理します。
数値・文字列・真偽値・null・undefined の 5 種類(プリミティブと呼びます。オブジェクトでも配列でもない値です)は、代入すると値そのものが写されます。
写したあとの 2 つの変数は無関係なので、片方を書き換えても、もう片方は変わりません。
配列とオブジェクトは 参照型(値そのものではなく、値が置かれている場所を指す情報を持つ型)です。代入で渡るのは場所を指す情報だけなので、2 つの変数が同じ 1 つの配列やオブジェクトを指します。
どちらの名前から書き換えても、指している先は同じ 1 つです。
// プリミティブは値が写される
let price = 3980;
let salePrice = price;
salePrice = 2980;
console.log(price); // 3980(影響を受けない)
console.log(salePrice); // 2980
// 配列は同じ 1 つを指す
const cart = ["A4 ノート"];
const savedCart = cart;
savedCart.push("油性ペン");
console.log(cart.length); // 2(cart 側も増えている)
console.log(cart === savedCart); // true(同じものを指している)
const でも中身は書き換えられる
const が固定するのは、変数がどこを指すかだけです。const cart = []; のあとに cart.push("油性ペン") と書けば中身は増えますし、const order = {} のあとに order.total = 0 も通ります。
禁止されるのは cart = [...] のように別のものを指し直す代入だけです。
コピーを作る — スプレッド構文と === の見ているもの
ここまで見たとおり、代入だけでは元と切り離された安全なコピーになりません。元の注文を残したまま金額だけを変えた別の注文を作りたいときは、{ ...order } のようにスプレッド構文で中身を並べた新しいオブジェクトを用意します。ここで確かめたいのは、そうして作ったものが元とどういう関係にあるのかです。
const copiedOrder = { ...order }; で作られるのは別のオブジェクトなので、order === copiedOrder は false です。=== は配列やオブジェクトに対して中身が同じかどうかではなく、同じ 1 つを指しているかどうかを判定します。
中身を比べたいときは、キーの数や個々の値のように、比べたい観点を自分で書きます。
true、別々に作ったものなら中身が同じでも false です。const cart = ["A4 ノート"];
const copiedCart = [...cart];
// 別々のものなので === は false
console.log(cart === copiedCart); // false
// 中身が同じでも false になる
console.log(["A"] === ["A"]); // false
// 中身どうしを比べれば true
console.log(cart[0] === copiedCart[0]); // true
// 同じものを指しているときだけ true
const savedCart = cart;
console.log(cart === savedCart); // true
入れ子の内側は写らない — 浅いコピーと深いコピー
ここまでのスプレッド構文は、金額のようなプリミティブな値については安全なコピーを作れていました。ただし顧客情報のように内側にオブジェクトを持つ注文をコピーすると、外側は別になっても内側は元と同じものを指したままです。コピーしたはずの側で住所を直したら、元の注文の住所まで変わっていた、という食い違いが起きます。
スプレッド構文が作るのは 浅いコピー(1 段目のプロパティだけを写し、内側のオブジェクトは同じものを指したままにするコピー)です。1 段目の値がプリミティブなら独立しますが、内側がオブジェクトや配列のときは、場所を指す情報(参照)がそのまま写ります。
内側まで作り直したいときは 深いコピー(入れ子の内側まで新しく作り直すコピー)を使います。structuredClone(order) と書くと、入れ子の何段目でも別のオブジェクトとして作り直されます。
ブラウザに用意されている関数なので、本サイトのコンソールでもそのまま使えます。ただし関数が入っているオブジェクトには使えません。
const order = {
orderId: "ORD-1342",
customer: { name: "森田 圭", city: "大阪市" },
};
// 浅いコピー: 1 段目は別、内側は同じものを指す
const shallow = { ...order };
shallow.orderId = "ORD-9999";
console.log(order.orderId); // ORD-1342(独立している)
shallow.customer.city = "京都市";
console.log(order.customer.city); // 京都市(元も変わった)
console.log(order.customer === shallow.customer); // true
// 深いコピー: 内側まで別になる
const deep = structuredClone(order);
deep.customer.city = "神戸市";
console.log(order.customer.city); // 京都市(元は無事)
console.log(order.customer === deep.customer); // false
書き換えを止める — Object.freeze
ここまで見てきたとおり、配列やオブジェクトは参照を共有しているだけで、意図せず書き換わってしまうことがあります。既定の設定や税率のように、そもそも後から書き換わってほしくない値には、const とは別の対策が要ります。const で宣言しても中身の書き換えは止められないので、オブジェクト自体を変更不可にする方法が必要です。
Object.freeze(settings) と書くと、そのオブジェクトはプロパティの変更・追加・削除を受け付けなくなります。凍結したあとに代入・追加・削除を書くと、その場で TypeError が発生して実行が止まります。
凍結済みかどうかは Object.isFrozen(settings) で調べられます。凍結は 1 段目にだけかかるので、内側のオブジェクトは別に凍結する必要があります。
const settings = { theme: "dark" };
// 凍結する前は書き換えられる
settings.theme = "light";
console.log(settings.theme); // light
Object.freeze(settings);
console.log(Object.isFrozen(settings)); // true
// 凍結後に代入すると、その行で止まる
// settings.theme = "dark";
// TypeError: Cannot assign to read only property 'theme' of object '#<Object>'
// 凍結は 1 段目だけ。内側は止まらない
const nested = Object.freeze({ display: { theme: "dark" } });
nested.display.theme = "light";
console.log(nested.display.theme); // light
// 変えたいときは新しいオブジェクトを作る
const updated = { ...settings, theme: "dark" };
console.log(updated.theme); // dark
console.log(settings.theme); // light(元は無事)
変えたいときは新しいオブジェクトを作る
凍結したオブジェクトに代入・追加・削除をすると TypeError が発生し、そこから先の行は実行されません。凍結したオブジェクトを変えたいときは、書き換えるのではなく { ...settings, theme: "light" } のように新しいオブジェクトを作る形にします。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2const a = ["A"]; const b = ["A"]; のとき、a === b は何を返しますか?
Q3内側にオブジェクトを持つデータを { ...order } でコピーしたとき、内側のオブジェクトはどうなりますか?