eureka

スプレッド構文がシャローコピーだということを知らずにハマっていた話

1

スプレッド演算子でコピーしたら無敵だと思ってた自分を殴りたい

まずは概要から

Nuxtでコーポレートサイトを制作しています。

制作前にサイトマップの設計をして、それに従ってページ情報をjsonファイルとして管理していました。

このようなイメージです。

[
  {
    "id": 1,
    "name": "home",
    "enTitle": null,
    "path": "/",
    "routerName": "index",
    "contents": []
  },
  {
    "id": 2,
    "name": "会社情報",
    "enTitle": "Company",
    "path": null,
    "routerName": null,
    "contents": [
      {
        "id": 1,
        "name": "会社概要",
        "enTitle": "Company Profile",
        "path": "/company/",
        "routerName": "company",
        "contents": []
      }
    ]
  },
  ...
]

で、このjsonファイルを使ってヘッダー、フッター、キービジュアルのタイトルの出し分けなどを行っていたのですが、PCとレスポンシブで表示するメニュー項目を分けるという要件があり大いにハマりました。

ええ、もう底なし沼のように!

起こったこと

まず制作過程としてヘッダーを作成しました。
PCのヘッダーで表示する項目はこんな感じです。

– 会社情報
 - 会社概要
 - 代表挨拶
 - CSR
– よくあるご質問
– 事業紹介
 - 社員紹介
– お問い合わせ

モバイルにした場合はトグルにするので、ページが存在する事業紹介に飛べるように社員紹介の前に事業紹介をもう一つ追加します。(会社情報はページがないためタイトルとしての役割のみ)

– 会社情報
 - 会社概要
 - 代表挨拶
 - CSR
– よくあるご質問
– 事業紹介
 - 事業紹介(モバイルのときだけ追加)
 - 社員紹介
– お問い合わせ

UIのイメージはこんな感じです。

モバイルメニューのイメージ

ヘッダーが完成してフッターの作成に入りました。
フッターではサイトマップ用にメニュー一覧を使用するので、jsonデータをそのまま展開するだけです。

ところが、なんも処理をしていないのに事業紹介がダブっている・・・

PCでjsonデータを展開

え・・?なぜ・・・?

配列のコピーの仕方がわかってなかった

結論からいうとこれでした。

配列のコピーをするならスプレッド演算子だ!って信じて止まなかったのが今回の原因です。

以下がヘッダーで行っていた処理です。

import PAGES from '~/assets/json/samary.json'

export default {
  ...,
  computed: {
    getMenu() {
      // メニューを表示する順番
      const useMenuId = [2, 3, 4, 5]
      const businessTop = {
        id: 0,
        name: '事業紹介',
        enTitle: 'Business',
        path: '/business/',
        routerName: 'business',
      }

      const addMenuArr = [...this.PAGES].map((menu) => {
        if (menu.id === 4) {
          // 重複チェック
          if (menu.contents.some((item) => item.id === 0)) {
            return menu
          } else {
            menu.contents.unshift(businessTop)
          }
        }
        return menu
      })

      if (this.isMobile) {
        return useMenuId.map((menu) => {
          return addMenuArr.find((el) => el.id === menu)
        })
      }

      return useMenuId.map((item) => {
        return [...this.PAGES].find((el) => el.id === item)
      })
    },
  }
}

スプレッド演算子を使用して、ページ情報が載っているjsonファイルをコピーしてモバイル用のメニュー配列を作成しています。
問題ないと思っていたのですが、これがダメダメだったのです。

アメリカン風に表現することこうです。

👎

JavaScriptには浅いコピー(シャローコピー)と深いコピー(ディープコピー)があり、スプレッド演算子は浅いコピーにあたるので第一階層しかコピーされないとかなんとか(モゴモゴ
で、今回は深い階層の配列を更新したので、元のデータが更新されてしまったんですね。ふむ・・・。

ディープコピーしたら解決した

スプレッド演算子でコピーするのがだめだったのでそれをやめました。
たどり着いたのがこちらのJSON.parse(JSON.stringify(obj))を使用する方法です。

const menu = JSON.parse(JSON.stringify(this.PAGES));

JSON.stringifyで引数にとったものを文字列に一旦置き換えてからJSON.parseをするため、深いコピーになるようです。

なるほど・・。

この方法で関数に組み込んだら沼から出られました。配列難しい〜〜〜><。

終わりに

最近配列と仲良くなってきたと思った矢先にこれです。悲しくて仕方ありません。(そこまでじゃない)

今回は完全にスプレッド演算子を信用しきっていました。
恥ずかしい話(本当に恥ずかしいんですが)、computed内で処理を書いていたのでキャッシュか!?とか、もしやヘッダーとフッターのコンポーネント内の関数のスコープとかないのか!?とか(今考えたらそもそも関数名違うしこの思考にいたるのはおかしいんですが、なにせテンパっていたのでw)

いろいろコメントアウトしたり一通り検証しながら、配列のコピーの仕方が悪いのかとググってslice()使ってみたり、concat()使ってみたりしたのですが結果は変わらず。

ようやく出会った記事でシャローコピーとディープコピーを知りました。これは知らないと大変だ・・・

配列は配列でもオブジェクトの配列だとか多次元配列になってくると、処理が複雑で本当に難しいなぁと思いました。。

この話をした友人のエンジニアは、この手の複雑な配列を扱うならlodashが良いよーとおすすめしてくれました。

機会があったら使ってみようと思いますが、しばらくは配列の処理に慣れるためにも修行だと思ってこのままがんばります。笑

沼にハマった間の想いを綴ったら長くなりました。
本当にコードを書くのが一番お勉強になりますね。

参考サイト

1