🤿

【Javascript】オブジェクト(連想配列じゃない)とかをディープコピーする前に

2022/01/03に公開
6

はじまり

この前、コーディングしている時にふと気になったのですが、ディープコピーの要否って一体どうなんだろう?と思って、書いた記事になります。

追記分(2022/01/10)

  • 不必要に配列に入れる表現をなくしました。
    ex. displayList.push([partOfDataList]);displayList.push(partOfDataList);
  • スプレッド構文を用いていた処理の呼称をディープコピーではなく、シャローコピー(スプレッド構文)に修正。
  • ディープコピーの方法も追加検証。
  • 「連想配列」を「オブジェクト」に修正。(console.logで出力された記事名は修正前に取れたものなので無視。)

まず、このコードを。

失敗コード

まず、このコードを実行すると、displayListにちゃんと値が入っていないことが確認できます。どうやらシャローコピーになっている参照をコピーしただけなのが原因のようです。

import fetch from 'node-fetch';
import _ from 'lodash';

const endpoint = 'https://api.rss2json.com/v1/api.json';
const feedUrl = 'https://zenn.dev/kinkinbeer135ml/feed';

const main = async () => {
    let res = await fetch(`${endpoint}?rss_url=${feedUrl}`);
    let dataList = await res.json();
    let partOfDataList = {};
    let displayList = [];

    // declare for count
    let i = 0;
    const number_of_display = 5;

    const start = performance.now();
    while(i < number_of_display){
        // let partOfDataList = {};
        partOfDataList['title']   = dataList.items[i%5].title;
        partOfDataList['pubDate'] = dataList.items[i%5].pubDate;
        console.log(partOfDataList);
        displayList.push(partOfDataList); // fix on 20220110
        // displayList.push({...partOfDataList}) // fix on 20220110
        // displayList.push(_.cloneDeep(partOfDataList)) // fix on 20220110
        i++;
    }
    const end = performance.now();
    console.log(displayList);

    console.log(end - start);
}

main()

console.log(displayList);の結果(失敗コード)

{
  title: '【Javascript】連想配列とかをディープコピーする前に',
  pubDate: '2022-01-03 06:16:42'
}
{
  title: '【Docker】ファイル実行できるNode.js環境を作る',
  pubDate: '2022-01-02 14:00:39'
}
{ title: 'Web上の画像をGoogleドライブに保存する', pubDate: '2021-12-26 10:06:05' }
{
  title: '【Javascript】Kindleの蔵書のタイトルだけを一覧で取得するツールを作りました',
  pubDate: '2021-12-25 20:43:42'
}
{ title: '【Python】ジェネレータとyieldでちと遊んだ', pubDate: '2021-12-19 13:41:03' }
[
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  },
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  },
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  },
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  },
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  }
]

そこで、以下の方法で解消を試みました。
いずれの方法でも解消できています。

解消法その1(変数を毎回宣言)

調査用コード

まずは、while句の外で宣言していたpartOfDataListをwhileの中で都度宣言してみました。参照コピーですが実現可能です。

import fetch from 'node-fetch';
import _ from 'lodash';

const endpoint = 'https://api.rss2json.com/v1/api.json';
const feedUrl = 'https://zenn.dev/kinkinbeer135ml/feed';

const main = async () => {
    let res = await fetch(`${endpoint}?rss_url=${feedUrl}`);
    let dataList = await res.json();
    // let partOfDataList = {};
    let displayList = [];

    // declare for count
    let i = 0;
    const number_of_display = 5;

    const start = performance.now();
    while(i < number_of_display){
        let partOfDataList = {};
        partOfDataList['title']   = dataList.items[i%5].title;
        partOfDataList['pubDate'] = dataList.items[i%5].pubDate;
        console.log(partOfDataList);
        displayList.push(partOfDataList); // fix on 20220110
        // displayList.push({...partOfDataList}) // fix on 20220110
        // displayList.push(_.cloneDeep(partOfDataList)) // fix on 20220110
        i++;
    }
    const end = performance.now();
    console.log(displayList);

    console.log(end - start);
}

main()

解消法その2(ディープコピーシャローコピー(スプレッド構文))

調査用コード

次は、pushする際にディープシャローコピーするスプレッド構文を使う方法です。

import fetch from 'node-fetch';
import _ from 'lodash';

const endpoint = 'https://api.rss2json.com/v1/api.json';
const feedUrl = 'https://zenn.dev/kinkinbeer135ml/feed';

const main = async () => {
    let res = await fetch(`${endpoint}?rss_url=${feedUrl}`);
    let dataList = await res.json();
    let partOfDataList = {};
    let displayList = [];

    // declare for count
    let i = 0;
    const number_of_display = 5;

    const start = performance.now();
    while(i < number_of_display){
        // let partOfDataList = {};
        partOfDataList['title']   = dataList.items[i%5].title;
        partOfDataList['pubDate'] = dataList.items[i%5].pubDate;
        console.log(partOfDataList);
        // displayList.push(partOfDataList); // fix on 20220110
        displayList.push({...partOfDataList}) // fix on 20220110
        // displayList.push(_.cloneDeep(partOfDataList)) // fix on 20220110
        i++;
    }
    const end = performance.now();
    console.log(displayList);

    console.log(end - start);
}

main()

解消法その3(ディープコピー)

調査用コード

次は、pushする際にディープコピーするを使う方法です。lodashを使いました。

解消法その3(ディープコピー).js
import fetch from 'node-fetch';
import _ from 'lodash';

const endpoint = 'https://api.rss2json.com/v1/api.json';
const feedUrl = 'https://zenn.dev/kinkinbeer135ml/feed';

const main = async () => {
    let res = await fetch(`${endpoint}?rss_url=${feedUrl}`);
    let dataList = await res.json();
    let partOfDataList = {};
    let displayList = [];

    // declare for count
    let i = 0;
    const number_of_display = 5;

    const start = performance.now();
    while(i < number_of_display){
        // let partOfDataList = {};
        partOfDataList['title']   = dataList.items[i%5].title;
        partOfDataList['pubDate'] = dataList.items[i%5].pubDate;
        console.log(partOfDataList);
        // displayList.push(partOfDataList); // fix on 20220110
        // displayList.push({...partOfDataList}) // fix on 20220110
        displayList.push(_.cloneDeep(partOfDataList)) // fix on 20220110
        i++;
    }
    const end = performance.now();
    console.log(displayList);

    console.log(end - start);
}

main()
console.log(end - start);

console.log(displayList);の結果(解消法その1~3)

{
  title: '【Javascript】連想配列とかをディープコピーする前に',
  pubDate: '2022-01-03 06:16:42'
}
{
  title: '【Docker】ファイル実行できるNode.js環境を作る',
  pubDate: '2022-01-02 14:00:39'
}
{ title: 'Web上の画像をGoogleドライブに保存する', pubDate: '2021-12-26 10:06:05' }
{
  title: '【Javascript】Kindleの蔵書のタイトルだけを一覧で取得するツールを作りました',
  pubDate: '2021-12-25 20:43:42'
}
{ title: '【Python】ジェネレータとyieldでちと遊んだ', pubDate: '2021-12-19 13:41:03' }
[
  {
    title: '【Javascript】連想配列とかをディープコピーする前に',
    pubDate: '2022-01-03 06:16:42'
  },
  {
    title: '【Docker】ファイル実行できるNode.js環境を作る',
    pubDate: '2022-01-02 14:00:39'
  },
  { title: 'Web上の画像をGoogleドライブに保存する', pubDate: '2021-12-26 10:06:05' },
  {
    title: '【Javascript】Kindleの蔵書のタイトルだけを一覧で取得するツールを作りました',
    pubDate: '2021-12-25 20:43:42'
  },
  {
    title: '【Python】ジェネレータとyieldでちと遊んだ',
    pubDate: '2021-12-19 13:41:03'
  }
]

どれが良いのか?

3通りの方法で出来ましたが、果たしてどれを使っていきましょうか?
僕としては、「変数を毎回宣言」する方法の方が可読性としては勝っていて良さげだなと感じました。
→ 追記:standard softwareさんからの指摘で、以前は書き方に問題がありました。しっかり書いたら特に可読性に違いは無さそうですね。
しかし、毎回宣言するのは、速度的に遅そうじゃない? どうなんだろう?
次は、実行速度を調べてみました。

実行速度の調査

調査用コード

次の調査に使うコードはこんな感じ。ざっと、30000件のレコードが入った連想配列オブジェクトの処理速度を計測しました。何回か実行して計測しました。
例えば、その1は以下の感じです。

解消法その1(変数を毎回宣言).js
import fetch from 'node-fetch';
import _ from 'lodash';

const endpoint = 'https://api.rss2json.com/v1/api.json';
const feedUrl = 'https://zenn.dev/kinkinbeer135ml/feed';

const main = async () => {
    let res = await fetch(`${endpoint}?rss_url=${feedUrl}`);
    let dataList = await res.json();
    // let partOfDataList = {};
    let displayList = [];

    // declare for count
    let i = 0;
    const number_of_display = 30000;

    const start = performance.now();
    while(i < number_of_display){
        let partOfDataList = {};
        partOfDataList['title']   = dataList.items[i%5].title;
        partOfDataList['pubDate'] = dataList.items[i%5].pubDate;
        console.log(partOfDataList);
        displayList.push(partOfDataList); // fix on 20220110
        // displayList.push({...partOfDataList}) // fix on 20220110
        // displayList.push(_.cloneDeep(partOfDataList)) // fix on 20220110
        i++;
    }
    const end = performance.now();
    console.log(displayList);

    console.log(end - start);
}

main()

測定結果

30000件での測定結果は以下のようになりました。毎回宣言のほうが少し速そうな感じはしますね。
→ (2022/01/10追記)これは、執筆当初、開発者ツールで動くスクリプトで動かした結果になります。

Sessions ディープコピーシャローコピー 変数を毎回宣言
1st 2076.10 1967.60
2nd 2061.19 1974
3rd 2050.19 2024.59
4th 2100.39 2092
5th 2130 1961.10
Avg. 2083.57 2003.86

測定結果その2(2022/01/10追記分)

追加で、ディープコピーを含めた3通りの方法をnode.js環境で試しました。(ファイル出力で試したので先程より格段に速くなっているのはご了承下さい。)
ディープコピーが1~2割くらい遅く、変数を毎回宣言する方法が最も速いかなという結果になりました。

Sessions 変数を毎回宣言 シャローコピー ディープコピー
1st 215.71 222.50 257.43
2nd 225.32 227.12 269.18
3rd 216.66 218.44 255.03
4th 215.89 217.68 248.55
5th 214.94 219.55 248.06
Avg. 217.70 221.06 255.65

おしまい

ディープコピーする前に、変数を毎回宣言する方法も検討してみては?

参考

https://qiita.com/_kt15_/items/0ae5085d61fa5598c76e
https://qiita.com/BlueSilverCat/items/d27a551eb4498d69f7d0

↓ディープコピーの種類は他にもこんなにある。(2022/01/10追記分)

https://qiita.com/suin/items/80e687dd1789b9d9d2fd

Discussion

standard softwarestandard software

たぶんディープコピーという用語を誤解されているので、シャローコピーとディープコピーで調べてみてください。

また、下記行で要素1つの配列に対してmapしているのはかなり謎です。

displayList.push([partOfDataList].map(list => ({...list})))

下記でよいと思われます。

displayList.push([{...partOfDataList}])

なぜdisplayListという配列に対して要素1つだけの配列を加えるのかは、理由がよくわからないのですが、やりたいことは下記でよいのではないでしょうか?

displayList.push({...partOfDataList})

{...Object} は、シャローコピーになります。

https://jsbin.com/dozojicece/edit?html,js,console

kinkinbeer135mlkinkinbeer135ml

ご指摘ありがとうございます。
ディープコピーはオブジェクトを別人としてコピーして、シャローコピーはオブジェクトの同一人物をコピーするようなイメージを持っています。
その点は概ね合っていると思うのですが、実は僕は文字列型をプリミティブな型ではないのだと思っていました。文字列は、文字の配列だから・・・と今まで思っていました。
そのため、
partOfDataList['title'] = dataList.items[i%5].title;
で入れた文字列を...でpushする処理を何度か行っても、直前に行った文字列が変化しなかった動作を見て
[partOfDataList].map(list => ({...list})
の部分はディープコピーが働いているんだなと思った次第です。
おっしゃる通り、ディープコピーとかで再度調べたらより理解が深まりました。「ディープコピー スプレット構文」で調べたら、プリミティブ型もすぐ分かりました。理解が甘かったです。

displayList.push([partOfDataList].map(list => ({...list})))
の部分はおっしゃる通り謎ですね。自分でもなぜこう書いたのか分からず・・・。
配列にしていた点もあまり覚えていませんが、開発者ツールでデバッグしてて配列がダブっていることに気づきませんでした・・・。
displayList.push({...partOfDataList})で正しいです!

ありがとうございます。勉強になります。JS Binというツールも便利ですね。自分が書いたコードをコンソール付きで手軽に見れる環境を共有できるのですね。

standard softwarestandard software

そういえば細かい指摘ばかりで申し訳ないのですがJSではオブジェクトを「連想配列」と呼ぶのは間違いの元なのでやめておいた方がよいです。JSのオブジェクトはオブジェクトであって連想配列ではないです。

例:console.log({}['toString']);

連想配列というならSet の方が適切ですが、Setを連想配列とは呼ばないので、JSには連想配列という用語そのものがない(はず)です。

多数のWebページでJSで「連想配列」という単語が書かれているのを見かけるので誤解する人が多いですが、そういう記事は正確ではないWebページなので学ぶ時には適切ではないです。

正確ではなく間違った情報があふれる世の中なのでなかなか正しい学習の道を見抜くのは難しいとは思うのですが、なるべく正しい定義で学習をしていくと効率よいのではないかと思います。

kinkinbeer135mlkinkinbeer135ml

いえいえ! 目からうろこです。
オブジェクトって、値だけじゃなくて関数も入れられるんですね。知らなかった。
これはこれでまた便利ですね! とても奥が深い・・・。
色々ご指摘ありがとうございます。連想配列とオブジェクトをごっちゃにしている記事が検索結果の上位を占めていて気付くのはなかなか難しそうですね・・・。ネットサーフィンするだけでなく、体系的に学習することも必要だなと思いました。
記事は更新しました。