2023/02/03
【JavaScript】配列・オブジェクトの参照渡し&コピーをクイズで理解する【React】
JavaScriptの代入にはコピーと参照渡しの2種類があり、
曖昧に理解していると思わぬ障害にぶつかってしまうことがあります(私はつい最近ぶつかってしまいました…)。
そこで、この代入のときのコピーと参照渡しについて理解できるように、クイズを交えてご紹介いたします。
JavaScriptにおけるデータ型の代入についての知識
JavaScript(ECMA Script)にはデータ型の代入ときの動作として、
値がコピーされるプリミティブ型と、
参照(データが入った場所)だけを引き渡すオブジェクト型の2種類があります。
プリミティブ型は、文字列(String)、数値(number)、巨大整数(BigInt)、真偽値(Boolean)、undefined、シンボル(Symbol)が該当し、
オブジェクト型には、オブジェクト(Object)、配列(Array)、日付(Date)などプリミティブ型以外のデータ型が該当し、
関数もオブジェクト型に該当します。
ちなみに null はプリミティブ値でありながら、typeof null === 'object'
となる特殊なオブジェクト型です。
プリミティブ型の代入では
1 2 3 | let a = 1 let b = a a = 2 |
とすると、b の値には a の値がコピーされて入っているので、
bの値は1
となります。
一方、オブジェクト型の代入では、
1 2 3 | let a = [1, 2] let b = a a[0] = 2 |
とすると、b には a の参照が渡されており、a の変更がそのまま b にも反映されるので、
a の内容は、[2, 2]
となります 。
逆に、
1 2 3 | let a = [1, 2] let b = a b[0] = 2 |
のように、代入先を更新した場合も、a の参照先は b の参照先と同じなので、
a の内容は [2, 2]
となります。
オブジェクト型を参照ではなく、データとしてコピーし、別々に扱いたい場合は、[...<配列の変数名>]
のように ...
というスプレッド構文という書き方をすることで、
参照ではなく、データそのものを代入することができます。
1 2 3 | let a = [1, 2] let b = [...a] a[0] = 2 |
のように記述した場合、
a の内容は更新されて、[2, 2]
となりますが、
b の内容は a とは独立しているので、a からコピーしたときの内容である[1, 2]
となります。
ちなみに連想配列などのオブジェクトの場合は、
1 2 3 4 5 | const a = { name: 'LaunchCart', age: 10 } const b = {...a} |
のように記述することで、オブジェクトの内容をコピーすることができます。
では、ここからはクイズです
第1問
以下のような代入を行った場合、cの内容として正しいものはどれでしょうか。
1 2 3 4 5 | const a = [1, 2, 3] const b = [1, a] const c = [...b] a[0] = 2 b[0] = 2 |
A. [1, [1, 2, 3]]
B. [1, [2, 2, 3]]
C. [2, [2, 2, 3]]
↓↓↓ 正解は… ↓↓↓
B の [1, [2, 2, 3]]
でした。
解説
まず、b には a が参照として渡されているので、a の内容が変更されると、b の中の a が参照されている内容も変更されます。
そして、c には、スプレッド構文により、b の内容がコピーという形で渡されています。
b の内容がコピーされているので、a が参照されている内容もコピーされていそうですが、
スプレッド構文でコピーとして渡される内容は直下の内容だけなので、a の参照はそのまま c にも参照として渡されます。
そのため、b の内容をコピーとして c に渡しても、aはそのまま参照として渡されているので、a が更新されると c の a が参照されている箇所も更新されます。
しかし、c の最初の要素は、b のコピーであって、独立したデータなので、b で更新しても c には影響がなく、
結果として、[1, [2, 2, 3]]
となります。
第2問
以下のような代入を行った場合、cの内容として正しいものはどれでしょうか。
1 2 3 4 5 6 7 8 9 10 | const a = { name: 'LaunchCart', age: 10 } const b = {...a} const c = { ...b, name: a.name } a.name = 'Sterfield' |
A.
1 2 3 4 | { name: 'LaunchCart', age: 10 } |
B.
1 2 3 4 | { name: 'Sterfield', age: 10 } |
C.
1 2 3 4 5 | { name: 'Sterfield', name: 'LaunchCart', age: 10 } |
↓↓↓ 正解は… ↓↓↓
B の
1 2 3 4 | { name: 'LaunchCart', age: 10 } |
でした。
解説
まず、const b = {...a}
で b には a の内容がコピーされます。
つぎに、
1 2 3 4 | const c = { ...b, name: a.name } |
で c に ...b
で b の内容をコピーしつつ、続けて name: a.name
と記述されています。
オブジェクトの中で、スプレッド構文に続けて、スプレッド構文で渡しているデータと重複するプロパティのデータを並べると、
後から記述されたプロパティで内容が上書きされますので、c.name には a.name のデータが入ります。
a はオブジェクト型ですが、a.name は文字列型ですので、値がコピーされて渡されるため、
後から a.name の内容を差し替えても、c.name の内容は更新されず、コピーされたときの ‘LaunchCart’ の文字列のママとなります。
第3問
以下のような代入を行った場合、aの内容として正しいものはどれでしょうか。
1 2 3 4 5 6 7 8 9 | const initializedValue = { name: '', age: 0 } const a = [initializedValue, initializedValue] a[0].name = 'LaunchCart' a[0].age = 10 a[1].name = 'Sterfield' a[1].age = 15 |
A. [ { name: '', age: 0 }, { name: '', age: 0 } ]
B. [ { name: 'LaunchCart', age: 10 }, { name: 'Sterfield', age: 15 } ]
C. [ { name: 'Sterfield', age: 15 }, { name: 'Sterfield', age: 15 } ]
↓↓↓ 正解は… ↓↓↓
C の[ { name: 'Sterfield', age: 15 }, { name: 'Sterfield', age: 15 } ]
でした。
解説
まず、初期値となるオブジェクトを、initializedValue という変数で用意し、
a に [initializedValue, initializedValue]
と、2個代入しています。
ここでは、そのまま代入しているので、a には2回同じ参照を渡している状態になります。
1つ目の要素も2つ目の要素も同じ参照を渡しているので、
1 2 3 4 | a[0].name = 'LaunchCart' a[0].age = 10 a[1].name = 'Sterfield' a[1].age = 15 |
とすると、同じ参照に対して繰り返し値の更新を行っていることになるため、
より後での更新が反映され、2つの要素とも、{ name: 'Sterfield', age: 15 }
が入ることになります。
第4問(React応用編)
Reactのコンポーネント内では、コンポーネント内のどこかの値が変更されるたびに再描画を行う必要があるため、
通常の変数ではなく、useState()
などの Hooks を使い、状態管理として変数を扱うことで、
変数の変更をReactに知らせ、適切なタイミングでコンポーネントの再描画が行われるような仕組みになっています。
例えば、
Reactのコンポーネント内で、const a = [1, 2, 3]
のような変数を利用する場合、
1 | const [a, setA] = useState([1, 2, 3]) |
のように設定を行い、変数を変更する箇所で、
1 | setA([2, 2, 3]) |
というふうに変数の変更を行います。
では、問題です。
以下のようなコンポーネントで、
LaunchCart のボタン を2回、Sterfield のボタン を1回クリックすると、a の中身はどうなるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | const initializedValue = { name: '', age: 0 } export const ComponentA = () => { const [a, setA] = useState([]) const onClick = (e) => { const name = e.target.dataset.name const age = Number(e.target.dataset.age) initializedValue.name = name initializedValue.age = age setA([...a, initializedValue]) } return ( <> {/* LaunchCart のボタン */} <button data-name="LaunchCart" data-age={10} onClick={onClick}>LaunchCart</button> {/* Sterfield のボタン */} <button data-name="Sterfield" data-age={15} onClick={onClick}>Sterfield</button> {/* a の中身を表示 */} <pre>{JSON.stringify(a)}</pre> </> ) } |
↓↓↓ 正解は… ↓↓↓
↓実際に作ってみましたので、ここで実際に試してみてください!
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE