【JavaScript】配列を非破壊的に扱うために必要だった知識

# TL;DR
文字列・数値・真偽値の配列なら`arr.slice()`でコピーして操作
プロパティがネストしないオブジェクトなら`arr.map(e => ({...e}))`でコピーして操作
(二次元配列なら`arr.map(e => [...e])`)
プロパティがネストするオブジェクトならライブラリを探す

# 何故非破壊的に扱いたいのか
JavaScriptの`Array`のメソッドには破壊的メソッドと非破壊的メソッドが混在している。
`reverse`だとか、`shift`だとかの破壊的なメソッドが関数に紛れ込んでいると副作用を生じさせてしまう。

```javascript
const fn = arr => arr.reverse().map(e => e + 1)

const arr = [1, 2, 3]
const arr2 = fn(arr)

console.log(arr2) //=> [4, 3, 2]
console.log(arr) //=> [3, 2, 1] 見かけ上arrには何もしていないはずなのに！
```

全部非破壊的に扱わせてくれと思った。

# `arr.slice()`を試す
調べてすぐ出てくるのは`arr.slice()`または`arr.concat()`。
一旦配列のコピーを作ってもとの配列への作用を回避する手法だ。

```javascript
const fn = arr => arr.slice().reverse().map(e => e + 1)

const arr = [1, 2, 3]
const arr2 = fn(arr)

console.log(arr2) //=> [4, 3, 2]
console.log(arr) //=> [1, 2, 3]
```
この場合は`arr.slice()`で問題ないが、[MDNのsliceのページ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)では下記のように解説されている。
>slice は元の配列を変更せず、元の配列から要素をシャローコピー (1 段階の深さのコピー) した新しい配列を返します。元の配列の要素は以下のように返される配列にコピーされます。

>(実際のオブジェクトではない) オブジェクトの参照については、slice はオブジェクトの参照を新しい配列にコピーします。元の配列も新しい配列も同じオブジェクトを参照します。参照されたオブジェクトが修正された場合、その変更は新しい配列と元の配列の両方に現れます。

>(String, Number, Boolean オブジェクトではなく) 文字列、数値、真偽値では、slice は値を新しい配列にコピーします。

>一方の配列の文字列や数値に変更を加えても、他の配列に影響はしません。

つまり、オブジェクトの配列に対して`arr.slice()`でコピーしても中身がオブジェクトの参照なので、中身を操作するともとの配列(の中身)にも影響してしまう。

```javascript
const arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]

const fn = arr => arr.slice().reverse().map(e => e.shift())
console.log(fn(arr)) // [ 7, 4, 1 ]

// reverseに関しては影響していないが、shiftに関しては影響してしまっている
console.log(arr) // [ [ 2, 3 ], [ 5, 6 ], [ 8, 9 ] ]
```

こんな真似を実際するかは不明だが、`arr.slice()`はオブジェクトに関しては別のオブジェクトとしてコピーしているわけではないということが実例できた。
これも非破壊的に扱いてえ。

# Spread syntaxを試す
`{...e}`でオブジェクトのコピーを、`[...e]`で配列のコピーを作成できる。
`arr.map(e => [...e])`や`arr.map(e => ({...e}))`で内部の要素をコピーした新しい配列を生成すれば、内部要素ごと新しい配列として、元の配列(とその内部要素)に対し影響なく扱える。

```javascript
const arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]

const fn = arr => arr.map(e => [...e]).reverse().map(e => e.shift())
console.log(fn(arr)) // [ 7, 4, 1 ]
console.log(arr) // [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]
```

ただし、上記の方法も完璧ではなく、[MDNのSpread syntaxの解説](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)によれば、
>メモ: コピーは 1 段階の深さで行われます。そのため、次の例のような多次元配列のようなオブジェクトをコピーする場合には適さないでしょう。(Object.assign() についても同じことが言えます。)

上記は多次元配列についての解説だが、オブジェクトがネストしたプロパティを持つオブジェクトに関しても同様のことが起こる。

```typescript
type Contact = {
  email: string,
  tel: string,
}

type User = {
  name: string,
  contact: Contact,
}
```

上記の`User`型のオブジェクトみたいなやつ。
`User.contact`みたいなネストしたプロパティが出る度にそのプロパティを新しくコピーするような関数を`map`して……という対応を取らなければいけない。
`arr.map(e => ({...e}))`のようなワンライナーで済むならばコストは低いが、プロパティがネストしてそのオブジェクトのプロパティもネストして……となると辛い。

そうなったら食い下がるのはやめて、ライブラリを使おうと思いました。（いかがでしたかブログ的な終わり）

# 補足
`arr.map(e => ({...e}))`は、

```javascript
arr.map(e => {
  return {...e}
})
```
をワンライナーで書くためにSpread syntaxを（）で括ってあげてるものです。

# 参考
[Array.prototype.slice() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)
[スプレッド構文 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)