質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.83%

ネストされたオブジェクト間の差分を取得する方法

解決済

回答 2

投稿

  • 評価
  • クリップ 3
  • VIEW 6,026
退会済みユーザー

退会済みユーザー

JSでネストされたオブジェクト間の差分を取得したい場合、どのように実装するのがベターなのでしょうか?
調べながらなんとかLodashというライブラリを使ってほしい結果を取得できましたが、中身がブラックボックなので、なんか良くわからないけど出来たという感覚です。

有識者の方も、その処理は面倒だからライブラリを使うことが多いとおっしゃるのなら、なるほどとなるのですが、周りに相談できる人もいないためTeratailを頼りにご質問させていただきました。

 背景

こちら側の商品情報を外部サービスと同期(API連携する)することになりました
こちら側で商品情報を更新した場合、更新APIのパラメータに更新されたオブジェクトをそのまま渡せれば良いのですが、更新されたプロパティのみを渡す必要性がでてきました。

 目的

更新されたプロパティのみを元の階層のまま抽出することです。

 試したこと

const _ = require('lodash');

// 更新前のオブジェクト
const objectA = {
  "id": "1",
  "description": "製品Aです。",
  "images": [
    "1.jpg",
    "2.jpg",
    "3.jpg",
  ],
  "metadata": {
    "product_id": "6735",
    "options": [
      "color",
      "size",
    ]
  },
  "name": "製品A",
  "active": false,
  "created": 1511420673,
  "updated": 1528145536,
}

// 更新後のオブジェクト
const objectB = {
  "id": "1",
  "description": "製品Bになりました。",
  "images": [
    "5.jpg",
  ],
  "metadata": {
    "product_id": "6735",
    "options": [
      "size",
    ]
  },
  "name": "製品B",
  "active": true,
  "created": 1511420673,
  "updated": 1528145599,
}

console.log(_.omitBy(objectB, (v, k) => objectA[k] === v))

 試した結果

{
  "description": "製品Bになりました。",
  "images": [
    "5.jpg",
  ],
  "metadata": {
    "product_id": "6735",
    "options": [
      "size",
    ]
  },
  "name": "製品B",
  "active": true,
  "updated": 1528145599,
}

 補足情報(FW/ツールのバージョンなど)

Node.js

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+7

こんにちは。

オブジェクトや配列に対してちょっと込み入った操作をするときに Lodash を利用するという選択自体は、間違いではありません。実際、私も同じ課題に直面したら、まずは、Lodash に便利なメソッドはないか?と探すと思います。

ただし、ご質問に挙げられている以下の _.omitBy を使ったコード

_.omitBy(objectB, (v, k) => objectA[k] === v);

だと、objectB から更新されたプロパティだけを残して他を除去(omit)したオブジェクトを得るという、期待した動作を満たせないのでは?という懸念があります。
たとえば以下の例

const objectA = {
  id: 1,
  description: "製品Aです。",
  images: [
    "1.jpg",
    "2.jpg",
    "3.jpg",
  ],
};

const objectB = {
  id: 1,
  description: "製品Aです。",
  images: [
    "1.jpg",
    "2.jpg",
    "3.jpg",
  ],
};

console.log(_.omitBy(objectB, (v, k) => objectA[k] === v));

では、 objectA と objectB とは内容が同じなので、更新のあったプロパティを持つオブジェクトとしては空オブジェクト {} が返ってきて欲しいところですが、上記を実行すると images が等しくないプロパティとして残ることになり、結果として_.omitByの返すオブジェクトは、以下

{
  images: [
    "1.jpg",
    "2.jpg",
    "3.jpg",
  ]
}

であることが、console.logの出力で確認できます。
これの検証用のサンプルを以下に上げましたので、ご確認ください。

上記を勘案しまして、オブジェクトの差分を得るのにLodashを使ってみたこと、および 
_.omitBy(objectB, (v, k) => objectA[k] === v) 
というコードを拝読して、お伝えしたいこととしては、以下の3点です。

  1. Lodashを使った修正案の見つけ方
  2. Lodashを使わずに最小限の要求を満たすものを書いてみる
  3. 基礎を見直すことと応用的なものを使うことのバランスについて

これらについて以下順を追って回答します。

 1. Lodashを使った修正案の見つけ方

ご質問に

調べながらなんとかLodashというライブラリを使って

とありました。 また、

周りに相談できる人もいない

ともありました。そのような状況の中で Lodashを見つけ出して、それを使ってみようと思い、実際コードを書いてみたというチャレンジはとてもよいと思います。ですので、まずは Lodashを使って、主題の「ネストされたオブジェクト間の差分を取得する方法」を探ってみましょう。

ご質問から業務でコードを書いている中での課題と思われますが、仕事の中でこの種の問題に遭遇した場合、すでに(優れたプログラマーである)誰かが作ってくれた、より確からしい実装に早くたどりつく必要がありますね。そういうときに、あくまで私の場合は、という限られた話にはなりますが、まずは適切な(日本語を含まない)英文でググってからの stackoverflow または、使うモジュールの発信元であるGitHubレポジトリのissueに拠り所を求めることが多いです。

このご質問の解決策を見つけるときに、Lodashを使うというのは筋として悪くないので、

(1) まずは "lodash difference between two objects" でグーグル検索 し、

(2) 検索結果で上位に出てきたものをざっと読み、

(3) stackoverflowの投稿が出てきたときは、いくつかの回答の中でどれが使えそうか当たりをつけます。
 
(4) 今回のご質問の場合、Lo-Dash Essentials という著書も書いている、 Adam Boduch さんによる回答: 

を拝借するのがよいと思えました。
(ただし検索結果として出てきた、どの投稿あるいは回答が一番使えるか?を選ぶのは、個人の判断に委ねられるところなので、別の投稿や回答のほうが使えると判断される方もいらっしゃることでしょう。)

上記を経て作成したものが以下です。

// 更新前のオブジェクト
const objectA = {
  id: 1,
  description: "製品Aです。",
  images: [
    "1.jpg",
    "2.jpg",
    "3.jpg",
  ],
  metadata: {
    product_id: 6735,
    options: [
      "color",
      "size",
    ]
  },
  name: "製品A",
  active: false,
  created: 1511420673,
  updated: 1528145536,
};

// 更新後のオブジェクト
const objectB = {
  id: 1,
  description: "製品Bになりました。",
  images: [
    "5.jpg",
  ],
  metadata: {
    product_id: 6735,
    options: [
      "size",
    ]
  },
  name: "製品B",
  active: true,
  created: 1511420673,
  updated: 1528145599,
};

// 更新されたプロパティの配列を得る。(https://stackoverflow.com/a/31686152)
const diffProps = _.reduce(objectA, function(result, value, key) {
    return _.isEqual(value, objectB[key]) ?
        result : result.concat(key);
}, []);

console.log(diffProps);

// 値が更新されたプロパティと、それらのobjectBでの値を持つオブジェクトを作成
const diffObj = diffProps.reduce((obj, prop) => { 
  obj[prop] = objectB[prop];
  return obj;
}, {});

console.log(diffObj);

上記のコードを以下に上げましたのでお試しください。

この差分抽出コードだと、objectAとobjectBの内容が同じときは、差分のオブジェクトは以下

のように、期待どおり空オブジェクトになります。

先ほど、
「業務上でこの種の問題に遭遇した場合、すでに(優れたプログラマーである)誰かが作ってくれた、より確からしい実装に早くたどりつく必要がありますね。」
と書きました。その観点でいうと、(1)から(4)のステップで大事なのは、 まずは(1)でいかに適切な英語で検索できるかだと思ってます。ですので、なるべく、stackoverflow のタイトルや、使いたいモジュールの発信元である GitHubレポジトリの issueタイトルにヒットしそうな検索ワードをあれこれと試します。

 2. Lodashを使わずに最小限の要求を満たすものを書いてみる

二点目としまして、あらためてterataillerさんのご質問を再読して(老婆心ながら)思うところとしては、ご質問にあるコード

_.omitBy(objectB, (v, k) => objectA[k] === v)

によって、差分のあるプロパティだけを取り出せると考えたのでしたら、 厳密な比較演算子=== についての理解を点検する必要があるのでは? ということです。ですので、

中身がブラックボックスなので、なんか良くわからないけど出来たという感覚

のモヤモヤの原因の一端は、=== が true になる条件の理解不足にあるかもしれません。モヤモヤにも良いモヤモヤと悪いモヤモヤとがあって、 ===の理解不足によるモヤモヤは悪いモヤモヤなので、すぐに解消すべきと思います。

言い換えると、

  • Lodash (なり、他の便利な何か)を採用することにし、それを使うために必要な情報に効率よくリーチして、ときには他の優れたプログラマーの成果物を土台として使ったりもしながら、どんなオブジェクトにも使える「オブジェクト一般の差分取得」というような、(いわば)大きなテーマのコードを持ってきて、締め切りに追われがちな限られた開発時間の中で、必要あれば修正も加えて、自分の目の前にある課題解決に使うことができる。

というのは応用編のスキル、しかもプログラミングの技術力は半分であとの半分は仕事をさばく器用さに近いものですが、それよりも

  • === で true になる条件という基礎知識を正しく活用して、自分の開発業務の範囲内にある、ある特定の形式のオブジェクトを比較するコードを、Lodashのような便利グッズを使わず、スクラッチから難なく書ける。

というスキルのほうが、獲得すべき順序として優先度が(かなり)高い

ということです。

そこで、以下に挙げたいくつかのページ

で説明している基礎知識を確認するための実践として、オブジェクト一般について比較するコードではなく、ご質問で検討の対象としている、以下の形式

{
  id: 整数,
  description: 文字列,
  images: 文字列を要素とする配列,
  metadata: {
    product_id: 整数,
    options: 文字列を要素とする配列
  },
  name: 文字列,
  active: ブール値,
  created: 整数(タイムスタンプ),
  updated: 整数(タイムスタンプ),
}


であることは分かっている2つのオブジェクト objectA と objectB とを比較して差分のプロパティだけを持つオブジェクトを返す関数なりメソッドなりを書いてみるのはいかがでしょうか? これをスクラッチから、Lodashのような便利なものを使わずに書いてみるとよいかもしれません。それがterataillerさんにとっての(次の一歩となる、)

ベターな

コードになると思いますし、それが書けてからLodashを使った汎用的なコードを採用することにしても遅くはありません。
また、ご質問にある、

中身がブラックボックスなので、なんか良くわからないけど出来たという感覚

と書かれている、誰かが作った中身の分からないものに乗ってしまっていることでのモヤモヤ(これは良いほうのモヤモヤです)を感じることのできるセンスを維持することも大事ですので、そのための練習問題として、(上記の、ある特定の形式のオブジェクトのプロパティ比較と差分オブジェクトの作成が書けた後に、) 一般的なオブジェクトの比較という大きいテーマに、Lodash を使わないで取り組むのもよい修練になると思います。それに取り組むと、おそらく

についている回答のようなコードを書くことになり、JavaScriptの基礎確認に役立つと思います。

 3. 基礎を見直すことと応用的なものを使うことのバランスについて

ここは意見の分かれるところだと思いますが、自分の考えを書いておきます。

私は「基礎がしっかり身についてないうちに、誰かが作ってくれた便利なものに頼ろうとしてはいけません。」というお説教を言うつもりは全くありません。むしろ逆で、基礎があやふやなうちから Lodash (に限らず JQuery でも、React でもいいのですが。) のような、言わば巷で流行の道具に目が行って、それに飛びつくことができるというのは、(これからもキープすべき)良質なセンスだと思います。それで飛びついてはみたものの、それを使ってみようとしたらうまくいかなくて、どうしてうまくいかないのかと試行錯誤した結果、「基礎力が足りない」と自分でちゃんと納得して結論することができ、その対策を(通常の業務時間とは別の時間を使うことになるかもしれませんが。)さっさと始めることが大事です。そのようにして、何か応用的なことをやろうとして、理解不足を感じた基礎項目に立ち返り、基礎を点検できたらまた応用に取り組むということを繰り返すうちに、基礎を見直すことと応用的な取り組みとの間の、自分なりのバランスが出来ていきます。

  
以上、参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/10 04:18

    前回に引き続き、大変ご丁寧な回答をありがとうございます。
    具体例を交え問題解決のプロセスまで、期待以上のご回答をいただけて嬉しい限りです。
    何度も読み返して次への糧とさせていただきます!

    キャンセル

+1

 ベターな書き方

JSでネストされたオブジェクト間の差分を取得したい場合、どのように実装するのがベターなのでしょうか?

あなたが最も理解している書き方がベストです。

  • var を使うべきではない。let, const を使うべき。
  • for 文を使うべきではない。forEach, for-of を使うべき。
  • XMLHttpRequest を使うべきではない。Fetchを使うべき。

この手の「推奨する書き方」のようなものが主張される事は確かにありますが、極論をいえば、どれも同じです。
たとえ話ですが、どこかで主張された「推奨する書き方」を良くわからないままに使って、バグを発生させるのでは本末転倒です。
バグを作りこまない為に、「あなたが最も理解している for 文を使う」のはありだと私は思います。
他の機能についても並行して学習し、「あなたが最も理解している for 文」と同等の理解度になった時に、ケース別に機能の取捨選択をすればいいでしょう。
そこまで理解すれば、自分の中に「どれを選ぶべきか」の判断基準を持っているはずです。

初心者にありがちなのは「推奨する機能を教えて下さい。それを覚えます。」というものですが、最終的には全てを覚えなければなりません
それなら、いろいろ試して貰って、その人が一番理解しやすい機能から覚えて貰った方が効率が良いでしょう。

 「わからない」の具体化

調べながらなんとかLodashというライブラリを使ってほしい結果を取得できましたが、中身がブラックボックなので、なんか良くわからないけど出来たという感覚です。

これはライブラリに「期待する動作」をよく考えないままに「適当に試したらたまたま出来た」のが原因です。
ライブラリの良いところでも悪いところでもありますが、ブラックボックスのまま使うのは正常動作しているように見えても、意図しないバグが潜んでいる可能性がある為、お勧めしません。
理想的には、そのライブラリのコードを解読し、動作を理解する事です。
次点で公式ドキュメントを読みながら、テストコードを書いて、動作を理解できるまでトライアンドエラーを繰り返す事。
それが出来ない内は、ライブラリを使わず、素のJavaScriptでコードを書く方が習得速度が速いと私は思います。

コードを作る前にアルゴリズムを考えるのが重要です。
はっきりいえば、アルゴリズムが出来ていないうちはコードなんて書いても無駄です。

  • 頭の中で動作を考えながら、頭の中で動かしてみる
  • 紙に書きだしてみる
  • フローチャートを書いてみる
  • consoleデバッグ、BreakPointで書きだしたフロー図と答え合わせ

こうした作業を実行していますか。
もし、やっているなら、質問文にそれを書きましょう。
現在の「なんかよくわからない」から「~のように考え、~をやってみたが、~のようになってしまった」のように、「分からない」を具体化して下さい。

現在の私の状況は「あなたがどこまで理解して、どこが理解できないのか分からない」ので、「何を教えていいかわからない」です。
(こういう場合、「何が分からないのか分からないので、とりあえず、要件を達成したコードを出しておくか」となりがちで、それが狙いなのかもしれませんが、そこは私の暇つぶしに付き合ってくれた対価として提供しましょう。)

 コード

アルゴリズムとしては、teratailler さんの過去質問に近いものがあります。
(あえていえば、再帰処理を理解している必要があります)

'use strcit';
function deepCopy (object) {
  return Object(object) === object && JSON.parse(JSON.stringify(object)) || object;
}

function diffObject (beforeObject, afterObject) {
  const add = Object.create(null), remove = Object.create(null), hasOwnProperty = Object.prototype.hasOwnProperty;

  for (let key of new Set(Object.keys(beforeObject).concat(Object.keys(afterObject)))) {
    const beforeValue = beforeObject[key], afterValue = afterObject[key];

    if (!hasOwnProperty.call(beforeObject, key)) {
      add[key] = deepCopy(afterValue);
    } else if (!hasOwnProperty.call(afterObject, key)) {
      remove[key] = deepCopy(beforeValue);
    } else if (beforeValue !== afterValue) {
      if (Object(beforeValue) === beforeValue && Object(afterValue) === afterValue) {
        const child = diffObject(beforeValue, afterValue);

        remove[key] = deepCopy(child.beforeValue);
        add[key] = deepCopy(child.afterValue);
      } else {
        remove[key] = deepCopy(beforeValue);
        add[key] = deepCopy(afterValue);
      }
    }
  }

  return {add: add, remove: remove};
}

Re: teratailler さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/10 04:18

    前回に引き続き、大変ご丁寧な回答をありがとうございます。
    今回はライブラリに頼ってしまいましたが、このような問題にも素のJavaScriptで書けるように精進します。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.83%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る