LoginSignup
32
15

More than 1 year has passed since last update.

【JS / TS】スプレッド構文は、オブジェクトや配列を展開するだけじゃない

Last updated at Posted at 2022-08-08

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 個以上のキーと値の組 (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。

// 一部省略

スプレッド構文の真価は、オブジェクトや配列などに含まれる要素の数に関係なく、同じ値で動作することにあります

とドキュメントに記載がありました。

参照: MDN Web Docs

つまり、

別のオブジェクトや配列を コピー して、展開する

僕は、コピーする ことを理解していませんでした。

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

daishiInfowhoAreYou は、一致していないことがわかりました。

コピーするということは、別のオブジェクトを作るということ

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

daishiSkillswhatSkills は、一致していないことがわかりました。

※ オブジェクト同様、ネスト した配列はコピーされません。

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' }

オブジェクトの nameemail プロパティの値を書き換え、sex プロパティの値を追加しようとしましたが、
name プロバティの値のみ書き換えが反映できてません。

これは、...daishiInfo のスプレッド構文よりも 前に記述する後ろに記述する かで反映されるデータが変わるからです。

name: 'manju' は、スプレッド構文の ...daishiInfo よりも 前に記述しています。
コピー元の daishiInfoname: 'daishi' が、name: 'manju' を書き換えています。
なので、name: 'manju'反映されていません

次に、
email: 'manju@example.com' は、スプレッド構文の ...daishiInfo よりも 後ろに記述しています。
email: 'manju@example.com' が、コピー元の daishiInfoemail: '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 の値

となっています。

これは、

  • nameage プロバティは、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 オブジェクトをスプレッド構文でコピーした後に、
daishiProfilename プロバティとprogrammingSkills.frontEnd プロパティに再代入しました。

ここで、思いも寄らない、値が返ってきます。

whoAreYou は、  
{ name: 'daishi', age: 35, programmingSkills: { frontEnd: ['PHP', 'Ruby'] } } となってほしいところ、
{ name: 'daishi', age: 35, programmingSkills: { frontEnd: ['JavaScript', 'TypeScript'] } } となっています。

つまり、
コピー先の whoAreYouname プロバティは再代入 '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' ] ]

コピー元の daishiSkillsPHPJava に再代入で変更しました。

変更しないで欲しいコピー先の whatSkillsJava に変化してます。

オブジェクトのとき同様にネストした配列をコピーするときは、
ネストした部分もスプレッド構文 で囲ってあげます。

配列のオブジェクト(ネスト 正しい使いかた)
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.おわりに

スプレッド構文は、オブジェクトや配列を展開するだけと思っていましたが、
調べてみると奥が深いものだなと感じました。

特に、コピーしているのか、同じものを呼んでいるのかについての認識をもっていなかったです。

思いも寄らないところでバグを起こしかねないので意識しなければと言う思いと、今までのコード大丈夫かなという恐怖でいっぱいです。

問題あれば、早急に対応します…

もし、この記事で間違った認識してるよってところがあれば、ご指摘頂けますと幸いです。

併せて他の記事も読んでいただけると嬉しいです🙇‍♂️

7.参考

書籍:プロを目指す人のためのTypeScript入門 鈴木僚太[著] @uhyo
MDN Web Docs

32
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
15