LoginSignup
0
0

More than 3 years have passed since last update.

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

Posted at

TL;DR

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

何故非破壊的に扱いたいのか

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

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()
一旦配列のコピーを作ってもとの配列への作用を回避する手法だ。

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のページでは下記のように解説されている。

slice は元の配列を変更せず、元の配列から要素をシャローコピー (1 段階の深さのコピー) した新しい配列を返します。元の配列の要素は以下のように返される配列にコピーされます。

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

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

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

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

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}))で内部の要素をコピーした新しい配列を生成すれば、内部要素ごと新しい配列として、元の配列(とその内部要素)に対し影響なく扱える。

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の解説によれば、

メモ: コピーは 1 段階の深さで行われます。そのため、次の例のような多次元配列のようなオブジェクトをコピーする場合には適さないでしょう。(Object.assign() についても同じことが言えます。)

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

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

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

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

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

補足

arr.map(e => ({...e}))は、

arr.map(e => {
  return {...e}
})

をワンライナーで書くためにSpread syntaxを()で括ってあげてるものです。

参考

Array.prototype.slice() - JavaScript | MDN
スプレッド構文 - JavaScript | MDN

0
0
0

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
0
0