今回はJavaScriptのスプレッド演算子について学習したので、記事にしてみました。
スプレッド演算子使用時の注意点についてもみていきます。
スプレッド演算子
スプレッド演算子は、スプレッド構文(…)を用いて配列またはオブジェクトの要素を展開することができる演算子です。
以下に公式の定義も載せておきます。
スプレッド構文 (...) を使うと、配列式や文字列などの反復可能オブジェクトを、0 個以上の引数 (関数呼び出しの場合) や要素 (配列リテラルの場合) を期待された場所で展開したり、オブジェクト式を、0 個以上のキーと値の組 (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。
文面だとわかりづらいので、スプレッド演算子の処理についてみていきましょう。
オブジェクトでのスプレッド演算子
まずはオブジェクトを用いたスプレッド演算子についてみてみます。
スプレッド演算子を用いるとオブジェクトのプロパティを別の新たなオブジェクトのプロパティとして複製することができます。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
}
const obj2 = {..obj1}
console.log(obj2)
//出力
{
id: 1,
title: 'オブジェクトのスプレッド演算子',
blog: 'Amayz BLOG'
}
以下のように残りのキーを指定することで一部のプロパティのみを用いて、新たなオブジェクトを作成することも可能です。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
}
const {id, ...obj2} = obj1
console.log(obj2)
//出力
{
title: 'オブジェクトのスプレッド演算子',
blog: 'Amayz BLOG'
}
また、既存のオブジェクトに新たなプロパティを加えて、新たなオブジェクトを作成することもできます。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
}
const obj2 = {...obj1, date: '2023-04-01'}
console.log(obj2)
//出力
{
id: 1,
title: 'スプレッド演算子とその注意点',
blog: 'Amayz BLOG',
date: '2023-04-01'
}
次に、新たに作成したオブジェクトのプロパティを更新したときの動きついてみてみましょう。
以下のように新たに作成したオブジェクトの値を更新してもコピー元のオブジェクトは更新されません。
ただし、スプレッド演算子はオブジェクトをコピーするときにシャローコピーという方法でコピーをしており、この点について注意しなければなりません。その注意点については、後半で詳しく触れていきます。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
}
const obj2 = {...obj1}
obj2.id = 2
obj2.title = 'タイトル不明'
console.log(...obj1)
console.log(obj2)
//出力
{id: 1, title: 'スプレッド演算子とその注意点', blog: 'Amayz BLOG'}
{id: 2, title: 'タイトル不明', blog: 'Amayz BLOG'}
配列でのスプレッド演算子
配列を用いたスプレッド演算子についてもみてみましょう。
スプレッド演算子を用いることで配列の要素をひとつひとつの文字列として出力してくれます。
const list = ['sugar', 'honey']
console.log(...list)
//出力
sugar honey
コンソール出力には、配列としてではなく要素の文字列のして出力してくれます。
仮にスプレッド演算子を使用せずにコンソール出力した際には以下のように配列として出力されます。
const list = ['sugar', 'honey']
console.log(list)
//出力
['sugar', 'honey']
例えば、二つの配列を一つの配列にするときなどにconcatなどを用いますが、スプレッド演算子でも同様の処理が可能です。スプレッド演算子で一度それぞれの配列の要素が展開され、展開された要素を配列として代入し直しています。
let array1 = [0, 1, 2]
let array2 = [3, 4, 5]
array1 = [...list1, ...list2]
console.log(array1)
//出力
[0, 1, 2, 3, 4, 5]
スプレッド演算子の注意点
それでは上記で少し触れたスプレッド演算子の注意点について、みていこうと思います。
スプレッド演算子を用いることでオブジェクトのプロパティを、新たなオブジェクトのプロパティとして定義することができるということをご理解いただけたかと思います。
スプレッド演算子を用いた新たなオブジェクトの作成はシャローコピーというコピー方法が用いられています。このシャローコピーが用いられている構文や関数については、コピーしたプロパティの更新に注意点があります。
では、まずシャローコピーについてみていきましょう。
シャローコピー
シャローコピーとは、コピーで新たに作成したオブジェクトが、コピー元のオブジェクトと、プロパティの一部を、同じ参照で共有している場合がある状態のコピーのことを指します。
あわせて公式の定義を以下に引用しておきます。
オブジェクトのシャローコピーとは、コピーがコピー元のオブジェクトとプロパティにおいて同じ参照を共有する(同じ基礎値を指す)コピーのことを指します。その結果、コピー元とコピー先のどちらかを変更すると、もう一方のオブジェクトも変更される可能性があります。
ちょっとわかりづらいですが、簡単にいうとシャローコピーしたオブジェクトのプロパティの更新時に、コピー元のプロパティを更新してしまう場合と更新しない場合があるということです。
更新したいのは、コピーしたオブジェクトのプロパティだけなのに、コピー元のオブジェクトのプロパティまで変わってしまう・・・という事態が発生する場合があるということです。
上記「オブジェクトでのスプレッド演算子」でオブジェクトのプロパティを更新したパターンでは、コピー元のオブジェクト(obj1)のプロパティが更新されないパターンになります。
では、どのようなときにコピー元のオブジェクトのプロパティまで更新されてしまうのか、みていきましょう。
今回、オブジェクトの中にオブジェクトをもつオブジェクト(obj1)から、スプレッド演算子を用いて新たなオブジェクト(obj2)を作成します。以下の内容でコピーしたオブジェクト(obj2)のプロパティを更新してみるとどうなるでしょうか。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
date: {
created: '2023-04-01'
updated: '-'
}
}
const obj2 = {...obj1}
obj2.id = 2
obj2.title = 'タイトル不明'
obj2.date.updated = '2024-05-25'
console.log(...obj1)
console.log(obj2)
//出力
{id: 1, title: 'スプレッド演算子とその注意点', blog: 'Amayz BLOG', date: {create: '2023-04-01', updated: '2024-05-25'}}
{id: 2, title: 'タイトル不明', blog: 'Amayz BLOG', date: {create: '2023-04-01', updated: '2024-05-25'}}
コピーで作成したオブジェクト(obj2)のプロパティだけを更新したにもかかわらず、コピー元のオブジェクト(obj1)の一部のプロパティも更新されてしまっています。
※意図しない更新:obj1.date.updated
が’2024-05-25’
に更新されている。
このようにシャローコピーの構文やメソッドを用いたときには、コピー元のオブジェクト(obj1)のプロパティまで意図しない更新が実行されてしまう点に注意が必要となります。
では、コピー元のオブジェクト(obj1)に対して意図しない更新が実行される理由について、みてみましょう。
シャローコピーを用いた構文やメソッドは、一階層目(黄色)のプロパティについては実体としてコピーします。ただし、二階層目(緑色)以降のプロパティについては、コピー元のオブジェクト(obj1)と同じプロパティを参照した状態でオブジェクト(obj2)を作成されます。
そのため、オブジェクトの二階層目以降のプロパティに対して更新を実行すると、参照先のプロパティを更新してしまい、同じプロパティを参照しているすべてのオブジェクトのプロパティが更新されてしまいます。
つまり、コピーで作成したオブジェクト(obj2
)の二階層目以降のプロパティを更新すると、コピー元のオブジェクト(obj1
)のプロパティにも反映されます。
逆も同じで、コピー元のオブジェクト(obj1
)の二階層目以降のプロパティを更新した場合は、コピーで作成したオブジェクトの(obj2
)のプロパティにも反映されます。
このことから、コピーで作成したオブジェクト(obj2
)の二回層目以降のプロパティを更新する場合は、シャローコピーではなく、ディープコピーを用いてオブジェクトのコピー作成を実施することをおすすめします。
ディープコピーとは、階層に関係なくコピー元のオブジェクトのプロパティすべてを、新たなオブジェクトのプロパティとして作成するため、オブジェクトのプロパティを更新しても他のオブジェクトのプロパティへの影響はありません。
スプレッド演算子とは直接関係ありませんが、ディープコピーは以下のように書くことで処理されます。
const obj1 = {
id: 1,
title: 'スプレッド演算子とその注意点'
blog: 'Amayz BLOG'
date: {
created: '2023-04-01'
updated: '-'
}
}
const obj2 = JSON.parse(JSON.stringify(obj1))
まとめると
スプレッド演算子はシャローコピーで新たなオブジェクトを作成することができます。
ただし、シャローコピーを用いてコピーするため、オブジェクトの二階層目以降のプロパティに対して更新をすると、コピー元のオブジェクトの値まで更新してしまいます。
二回層目以降のプロパティ更新を行うときには、データ更新に対する影響を考慮した処理を書くか、ディープコピーを用いて新たにオブジェクトを作成することをおすすめします。
最後に・・・
シャローコピーで更新されたり更新されなかったりする理由がわかりづらく、混乱してしまうのは、そもそも”コピー”の意味合いが、オブジェクトの一階層目のプロパティを実体として複製するという前提について触れられておらず、二階層目以降のコピーがシャローコピーなのかディープコピーなのか、わかりづらいことにあるのかもしれませんね・・・