1.はじめに
スプレッド構文って、オブジェクトや配列を簡単に一括で表示する程度の認識しか持っていませんでした。
@uhyo さんの「プロを目指す人のための TypeScript 入門」を元にキャッチアップしました。
スプレッド構文について、知らなかったことがあったので、アウトプットします。
少し長い記事ですが、スプレッド構文についてまとめるとこのボリュームになりました。
気になるところだけでも見て頂ければなと思います。
間違って解釈している所ありましたら、ご指摘いただけますと幸いです。
2.目次
1. はじめに
2. 目次
3. この記事でわかること
4. 環境
5. スプレッド構文とは
5.1. スプレッド構文とは
5.2. スプレッド構文の使いかた
5.3. 別のオブジェクトや配列からコピー
5.4. スプレッド構文は、オブジェクトや配列の拡張に便利
5.5. スプレッド構文は複数使用できる
5.6. ネストしているものは、コピーされない
6. おわりに
7. 参考
3.この記事でわかること
スプレッド構文は、オブジェクトや配列を展開するだけという認識から脱却できる
- 別のオブジェクトや配列から コピーする
- スプレッド構文でコピーしたオブジェクトや配列は、別の オブジェクトや配列となる
- スプレッド構文を使用する時は、
{}
、[]
で囲む - スプレッド構文でコピーしたものには 変更が反映されない ※ ネスト除く
- スプレッド構文よりも 前に プロパティーの変更を行っても変更できない
- 1つのオブジェクトや配列の中で 複数使用できる
- 同じプロパティがあった時は、後の方 が採用される
- ネスト しているものは、コピーされない
4.環境
- Node.js: 16.15.1
5.スプレッド構文とは
5.1.スプレッド構文とは
スプレッド構文とは、何かを調べてみると、
配列式や文字列などの反復可能オブジェクトを、0 個以上の引数 (関数呼び出しの場合) や要素 (配列リテラルの場合) を期待された場所で展開したり、オブジェクト式を、0 個以上のキーと値の組 (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。
// 一部省略
スプレッド構文の真価は、オブジェクトや配列などに含まれる要素の数に関係なく、同じ値で動作することにあります
とドキュメントに記載がありました。
つまり、
別のオブジェクトや配列を コピー して、展開する
僕は、コピーする ことを理解していませんでした。
5.2.スプレッド構文の使いかた
スプレッド構文は、オブジェクトや配列の中に ...〇〇〇
と記述して使います。
5.2.1.オブジェクトのとき
const daishiInfo = {
name: 'daishi',
age: 35
};
// オブジェクトのスプレッド構文
const whoAreYou = { ...daishiInfo };
console.log(whoAreYou); // { name: 'daishi', age: 35 }
daishiInfo
オブジェクトの中身がすべて whoAreYou
に入っています。
5.2.2.配列のとき
const daishiSkills = ['JavaScript', 'TypeScript'];
// 配列のスプレッド構文
const whatSkills = [...daishiSkills];
console.log(whatSkills); // [ 'JavaScript', 'TypeScript' ]
オブジェクト同様に、daishiSkills
の配列の中身がすべて whatSkills
に入っています。
スプレッド構文は、オブジェクトや配列の 展開 なので、
使用する時は、{}
、 []
で囲む必要があります。
const whoAreYou = ...daishiInfo;
const whatSkills = ...daishiSkills;
とすると、エラーになります。
スプレッド構文は、簡単にオブジェクトや配列をコピーすることができますが、
挙動について理解していないと、意図しない バグの原因になりかねないので記事にしました。
5.3.別のオブジェクトや配列からコピー
まずは、コピー とは、どういうことか見ていきます。
5.3.1.オブジェクトのとき
const daishiInfo = {
name: 'daishi',
age: 35
};
// オブジェクトのスプレッド構文
const whoAreYou = { ...daishiInfo };
// name に 'manju' を再代入
daishiInfo.name = 'manju';
console.log(daishiInfo); // { name: 'manju', age: 35 }
console.log(whoAreYou); // { name: 'daishi', age: 35 }
daishiInfo
オブジェクトの name
プロパティに manju
を再代入しました。
コピー元 の daishiInfo
は、
{ name: 'daishi', age: 35 }
→ { name: 'manju', age: 35 }
に変化しましたが、
コピー先 の whoAreYou
は、
{ name: 'daishi', age: 35 }
のままです。
これが コピーする ということです。
スプレッド構文でコピーしたものには コピー元の変更が反映されない
コピー元の変更が反映されないと言うことは、別のオブジェクトということになります。
const daishiInfo = {
name: 'daishi',
age: 35
};
// オブジェクトのスプレッド構文
const whoAreYou = { ...daishiInfo };
// 別のオブジェクトかを確認
console.log(daishiInfo === whoAreYou); // false
daishiInfo
と whoAreYou
は、一致していないことがわかりました。
コピーするということは、別のオブジェクトを作るということ
5.3.2.配列のとき
const daishiSkills = ['JavaScript', 'TypeScript', 'PHP', 'Ruby'];
// 配列のスプレッド構文
const whatSkills = [...daishiSkills];
// 2 要素目に 'Java' を再代入
daishiSkills[2] = 'Java';
console.log(daishiSkills); // [ 'JavaScript', 'TypeScript', 'Java', 'Ruby' ]
console.log(whatSkills); // [ 'JavaScript', 'TypeScript', 'PHP', 'Ruby' ]
こちらも、オブジェクトのときと同様に、
daishiSkills[2]
要素に Java
を再代入しました。
コピー元 の daishiSkills
は、
[ 'JavaScript', 'TypeScript', 'PHP', 'Ruby' ]
→ [ 'JavaScript', 'TypeScript', 'Java', 'Ruby' ]
と変化していますが、
コピー先 の whatSkills
は、
[ 'JavaScript', 'TypeScript', 'PHP', 'Ruby' ]
のままです。
コピー元の変更が反映されないと言うことは、別の配列ということになります。
const daishiSkills = ['JavaScript', 'TypeScript', 'PHP', 'Ruby'];
// 配列のスプレッド構文
const whatSkills = [...daishiSkills];
// 配列が一致しているかを確認
console.log(daishiSkills === whatSkills); // false
daishiSkills
と whatSkills
は、一致していないことがわかりました。
※ オブジェクト同様、ネスト した配列はコピーされません。
5.4スプレッド構文は、オブジェクトや配列の拡張に便利
スプレッド構文は、オブジェクトや配列の拡張や書き換え、新しいものを作るのに便利です。
ただし、スプレッド構文の記述位置によっては、データが書き換えが反映されない箇所もあるので、注意が必要になります。
細かく見ていきたいと思います。
5.4.1.オブジェクトのとき
const daishiInfo = {
name: 'daishi',
age: 35,
email: 'daishi@example.com',
};
// オブジェクトのスプレッド構文
const manjuInfo = {
name: 'manju',
...daishiInfo,
email: 'manju@example.com',
sex: 'male',
};
console.log(manjuInfo); // { name: 'daishi', age: 35, email: 'manju@example.com', sex: 'male' }
オブジェクトの name
、email
プロパティの値を書き換え、sex
プロパティの値を追加しようとしましたが、
name
プロバティの値のみ書き換えが反映できてません。
これは、...daishiInfo
のスプレッド構文よりも 前に記述する か 後ろに記述する かで反映されるデータが変わるからです。
name: 'manju'
は、スプレッド構文の ...daishiInfo
よりも 前に記述しています。
コピー元の daishiInfo
の name: 'daishi'
が、name: 'manju'
を書き換えています。
なので、name: 'manju'
は 反映されていません 。
次に、
email: 'manju@example.com'
は、スプレッド構文の ...daishiInfo
よりも 後ろに記述しています。
email: 'manju@example.com'
が、コピー元の daishiInfo
の email: 'daishi@example.com'
を書き換えています。
なので、email: 'manju@example.com'
は反映されています。
最後に、
sex: 'male'
は、daishiInfo
には存在しなかったので、sex: 'male'
が反映されています。
ロジックは、前から後ろに読んでいくので、当たり前と言えば当たり前ですが、僕はこれを理解できていませんでした…
コピー先の値の変更は、スプレッド構文の後ろ に記述する
この記述方法であれば、コピー元には反映されません。
5.4.2.配列のとき
const daishiSkills = ['JavaScript', 'TypeScript', 'PHP', 'Ruby'];
// 配列のスプレッド構文
const manjuSkills = ['React', ...daishiSkills, 'Java'];
console.log(daishiSkills); // [ 'JavaScript', 'TypeScript', 'PHP', 'Ruby' ]
console.log(manjuSkills); // [ 'React', 'JavaScript', 'TypeScript', 'PHP', 'Ruby', 'Java' ]
配列のときは、オブジェクトとは挙動が異なります。
配列のスプレッド構文は、その配列の要素すべて が、その位置にコピーされる
なので manjuSkills
は、[ 'React', 'JavaScript', 'TypeScript', 'PHP', 'Ruby', 'Java' ]
の通り、
-
0
要素目と5
要素目は、値が追加 -
1
要素目から4
要素目までは、daishiSkills
の配列がコピー
となっているのが、わかるかと思います。
5.5.スプレッド構文は複数使用できる
スプレッド構文は、1つのオブジェクトや配列に対して、複数できます。
ただしこちらも、オブジェクトと配列では挙動が異なります。
5.5.1.オブジェクトのとき
const daishiInfo = {
name: 'daishi',
age: 35,
email: 'daishi@example.com',
};
const manjuInfo = {
name: 'manju',
age: 132,
sex: 'female',
};
// オブジェクトの複数のスプレッド構文
const whoAreYou: UserInfo = {
...daishiInfo,
...manjuInfo,
};
console.log(whoAreYou); // { name: 'manju', age: 132, email: 'daishi@example.com', sex: 'female' }
この2つのオブジェクトをスプレッド構文でつなげてみます。
ご覧の通り、複数のスプレッド構文を問題なく使用できます。
ただ、注意点があり、
-
name
プロバティは、manjuInfo
の値 -
age
プロバティは、manjuInfo
の値 -
email
プロバティは、daishiInfo
の値 -
sex
プロバティは、manjuInfo
の値
となっています。
これは、
name
とage
プロバティは、daishiInfo
の値が、manjuInfo
に上書きされる-
email
プロバティは、daishiInfo
にしかないので、daishiInfo
の値 -
sex
プロバティは、manjuInfo
にしかないので、manjuInfo
の値
という理由からです。
同じプロパティは、後の方が採用される
定義されていないプロパティに関しては、定義されている方が採用されます。
5.5.2.配列のとき
配列に関しても、複数のスプレッド構文を使用できます。
const daishiSkills = ['JavaScript', 'TypeScript', 'PHP', 'Ruby'];
const manjuSkills = ['JavaScript', 'TypeScript', 'React', 'PHP'];
// 配列の複数のスプレッド構文
const programmingSkills = [
...daishiSkills,
...manjuSkills
];
console.log(programmingSkills); // ['JavaScript', 'TypeScript', 'PHP', 'Ruby', 'JavaScript', 'TypeScript', 'React', 'PHP']
ご覧の通り、オブジェクトと違い、
同じ要素番号に同じ値が入っていても両方の値が採用される
5.6.ネストしているものは、コピーされない
ネストしているオブジェクトや配列は、コピーされません。
思わぬところで値が変わるおそれがあるので、ネストしたスプレッド構文は、取り扱いに注意が必要です。
5.6.1.オブジェクトのとき
const daishiProfile =
{
name: 'daishi',
age: 35,
programmingSkills: {
frontEnd: ['JavaScript', 'TypeScript'],
}
};
// オブジェクトのスプレッド構文
const whoAreYou = {
...daishiProfile
};
// daishiProfile に再代入
daishiProfile.name = 'manju';
daishiProfile.programmingSkills.frontEnd = ['PHP', 'Ruby'];
console.log(daishiProfile); // { name: 'manju', age: 35, programmingSkills: { frontEnd: ['PHP', 'Ruby'] } }
console.log(whoAreYou); // { name: 'daishi', age: 35, programmingSkills: { frontEnd: ['PHP', 'Ruby'] } }
上記は、
whoAreYou
オブジェクトに daishiProfile
オブジェクトをスプレッド構文でコピーした後に、
daishiProfile
の name
プロバティとprogrammingSkills.frontEnd
プロパティに再代入しました。
ここで、思いも寄らない、値が返ってきます。
whoAreYou
は、
{ name: 'daishi', age: 35, programmingSkills: { frontEnd: ['PHP', 'Ruby'] } }
となってほしいところ、
{ name: 'daishi', age: 35, programmingSkills: { frontEnd: ['JavaScript', 'TypeScript'] } }
となっています。
つまり、
コピー先の whoAreYou
の name
プロバティは再代入 前 の 'daishi'
が入っていますが、
programmingSkills.frontEnd
プロパティは再代入 後 の、{ frontEnd: ['PHP', 'Ruby'] }
が入っています。
このことから、
ネストしたオブジェクトはコピーされない
ことがわかります。
では、ネストした値もコピーしたいときはどうするかと言うと、
ネストした部分もスプレッド構文 で囲ってあげます。
const daishiProfile =
{
name: 'daishi',
age: 35,
programmingSkills: {
frontEnd: ['JavaScript', 'TypeScript'],
}
};
// ネストしたオブジェクトのスプレッド構文
const whoArtYou = {
...daishiProfile,
programmingSkills: { ...daishiProfile.programmingSkills }
;
// daishiProfile に再代入
daishiProfile.name = 'manju';
daishiProfile.programmingSkills.frontEnd = ['PHP', 'Ruby'];
console.log(daishiProfile); // { name: 'manju', age: 35, programmingSkills: { frontEnd: ['PHP', 'Ruby'] } }
console.log(whoArtYou); // { name: 'daishi', age: 35, programmingSkills: { frontEnd: ['JavaScript', 'TypeScript'] } }
ネストした部分もスプレッド構文で囲ってあげると、
コピー元に再代入しても、コピー先は変わっていない のがわかるかと思います。
(書き方まどろっこしいな…)
こころの声が…
ネストしたオブジェクトをコピーする際は、ネスト部分もスプレッド構文で囲む必要がある
5.6.2.配列のとき
配列も、オブジェクト同様に、ネストしているとコピーされません。
const daishiSkills = [['JavaScript', 'TypeScript'],['PHP', 'Ruby']];
// 配列のスプレッド構文
const whatSkills = [...daishiSkills];
// daishiSkills に再代入
daishiSkills[1][0] = 'Java';
console.log(daishiSkills); // [ [ 'JavaScript', 'TypeScript' ], [ 'Java', 'Ruby' ] ]
console.log(whatSkills); // [ [ 'JavaScript', 'TypeScript' ], [ 'Java', 'Ruby' ] ]
コピー元の daishiSkills
の PHP
を Java
に再代入で変更しました。
変更しないで欲しいコピー先の whatSkills
も Java
に変化してます。
オブジェクトのとき同様にネストした配列をコピーするときは、
ネストした部分もスプレッド構文 で囲ってあげます。
const daishiSkills = [['JavaScript', 'TypeScript'],['PHP', 'Ruby']];
// // ネストした配列のスプレッド構文
const whatSkills = [[...daishiSkills[0]], [...daishiSkills[1]]];
// オブジェクトのスプレッド構文
daishiSkills[1][0] = 'Java';
console.log(daishiSkills); // [ [ 'JavaScript', 'TypeScript' ], [ 'Java', 'Ruby' ] ]
console.log(whatSkills); // [ [ 'JavaScript', 'TypeScript' ], [ 'PHP', 'Ruby' ] ]
ネストした部分をスプレッド構文で囲ってあげると、
コピー元に再代入しても、コピー先は変わっていない のがわかるかと思います。
[ ↓ 追記 ↓ ]
5.6.3 ネスト部分もコピーできるライブラリ
@RyoTa_0222 さんが、このまどろっこしい書き方を解決するライブラリを教えてくださりました。
ありがとうございます。
簡単にネスト部分もコピーできるんですね。便利!!
[ ↑ 追記 ↑ ]
6.おわりに
スプレッド構文は、オブジェクトや配列を展開するだけと思っていましたが、
調べてみると奥が深いものだなと感じました。
特に、コピーしているのか、同じものを呼んでいるのかについての認識をもっていなかったです。
思いも寄らないところでバグを起こしかねないので意識しなければと言う思いと、今までのコード大丈夫かなという恐怖でいっぱいです。
問題あれば、早急に対応します…
もし、この記事で間違った認識してるよってところがあれば、ご指摘頂けますと幸いです。
併せて他の記事も読んでいただけると嬉しいです🙇♂️