【问题标题】:Comparing 2 JSON objects structure in JavaScript比较 JavaScript 中的 2 个 JSON 对象结构
【发布时间】:2016-01-15 12:35:30
【问题描述】:

我在一个大型应用程序中使用 angular-translate。有几个人提交代码+翻译,很多时候翻译对象不同步。

我正在构建一个 Grunt 插件来查看两个文件的结构并进行比较(只是键和整体结构,而不是值)。

主要目标是:

  • 查看每个文件,检查整个对象的结构 (或文件,在这种情况下)与翻译的完全相同;
  • 出错时,返回不匹配的键。

事实证明它比我预期的要复杂一些。所以我想我可以这样做:

  1. 对对象进行排序;
  2. 检查值包含的数据类型(因为它们是翻译,它将只有字符串或嵌套对象)并将其存储在另一个对象中,使键等于原始键,值将是string 'String',或者一个对象,如果它是一个对象。该对象包含子元素;
  3. 递归地重复步骤 1-2,直到整个对象被映射和排序;
  4. 对所有文件执行相同操作
  5. 对所有内容进行字符串化和比较。

一个小例子是以下对象:

{
  key1: 'cool',
  key2: 'cooler',
  keyWhatever: {
    anotherObject: {
      key1: 'better',
      keyX: 'awesome'
    },
    aObject: 'actually, it\'s a string'
  },
  aKey: 'more awesomeness'
}

将映射到:

{
  aKey: 'String',
  key1: 'String',
  key2: 'String',
  keyWhatever: {
    aObject: 'String',
    anotherObject: {
      key1: 'String',
      keyX: 'String'
    }
  }
}

在此之后,我会将所有对象字符串化并进行严格比较。

我的问题是,有没有更好的方法来执行此操作?无论是在简单性还是性能方面,因为有很多翻译文件而且它们都相当大。

我试图寻找已经可以执行此操作的库,但找不到。

谢谢

编辑感谢 Jared 指出对象无法排序。我很惭愧地说这样的话:D 另一种解决方案可能是迭代主翻译文件上的每个属性,如果它们是字符串,则将密钥与其他文件进行比较。如果它们是对象,请“输入”它们并执行相同操作。也许它比我的第一个猜测还要简单。应该怎么做?

【问题讨论】:

  • 尽管许多实现保持 insertion 顺序,但根据规范,javascript 对象是 unordered 键/值对。您将不得不使用 Object.keys 并使用数组,您不能“排序”对象。
  • 谢谢贾里德。我更正了我的问题详情!
  • 这是一个大问题,所以我不声称有解决方案,但有一件事您可能没有想到:您将如何处理这个问题? root = {myself: root }(也许你会抛出一个错误,但你不能永远崩溃页面递归)
  • @Katana314 请记住,JSON 不能表示循环结构。这对 OP 来说不是问题
  • @skeggse 不,它不能,但即使只是检测到它正在尝试处理一个事实,您也需要牢记这一点。否则,您的代码很可能会进入递归无限循环,让任何使用该函数的人感到困惑,而不是意识到他们给它提供了一个坏对象。

标签: javascript json


【解决方案1】:

假设您有两个 JSON 对象,jsonA 和 jsonB。

function compareValues(a, b) {

    //if a and b aren't the same type, they can't be equal
    if (typeof a !== typeof b) {
        return false;
    }
 
    // Need the truthy guard because
    // typeof null === 'object'
    if (a && typeof a === 'object') {
        var keysA = Object.keys(a).sort(),
            keysB = Object.keys(b).sort();

        //if a and b are objects with different no of keys, unequal
        if (keysA.length !== keysB.length) {
            return false;
        }

        //if keys aren't all the same, unequal
        if (!keysA.every(function(k, i) { return k === keysB[i];})) {
            return false;
        }

        //recurse on the values for each key
        return keysA.every(function(key) {
            //if we made it here, they have identical keys
            return compareValues(a[key], b[key]);
        });

    //for primitives just use a straight up check
    } else {
        return a === b;
    }
}

//true if their structure, values, and keys are identical    
var passed = compareValues(jsonA, jsonB); 

请注意,这可能会使深度嵌套的 JSON 对象的堆栈溢出。另请注意,这适用于 JSON,但不一定适用于常规 JS 对象,因为日期对象、正则表达式等需要特殊处理。

【讨论】:

    【解决方案2】:

    实际上,您确实需要对键进行排序,因为它们不需要以任何特定顺序吐出。写一个函数,

    function getComparableForObject(obj) {
        var keys = Object.keys(obj);
        keys.sort(a, b => a > b ? 1 : -1);
    
        var comparable = keys.map(
                key => key + ":" + getValueRepresentation(obj[key])
            ).join(",");
        return "{" + comparable + "}";
    }
    

    其中getValueRepresentation 是一个返回“字符串”或调用getComparableForObject 的函数。如果您担心循环引用,请将Symbol 添加到外部作用域repr,在上面的函数中分配obj[repr] = comparable,并在getValueRepresentation 中检查每个对象是否已定义obj[repr] 并返回它而不是递归处理它。

    【讨论】:

      【解决方案3】:

      对对象中的键数组进行排序是可行的。但是,排序的平均 time complexityO(n⋅log(n))。我们可以做得更好。保证两个集合AB等价的快速通用算法如下:

      for item in B
        if item in A
          remove item from A
        else
          sets are not equivalent
      sets are equivalent iff A is empty
      

      为了解决@Katana31,我们可以通过维护一组已访问对象并确保该对象的所有后代都不在列表中来检测循环引用:

      # poorly written pseudo-code
      fn detectCycles(A, found = {})
        if A in found
          there is a cycle
        else
          found = clone(found)
          add A to found
          for child in A
            detectCycles(child, found)
      

      这是一个完整的实现(你可以找到一个简化版本,假设 JSON/非循环输入here):

      var hasOwn = Object.prototype.hasOwnProperty;
      var indexOf = Array.prototype.indexOf;
      
      function isObjectEmpty(obj) {
        for (var key in obj) {
          return false;
        }
      
        return true;
      }
      
      function copyKeys(obj) {
        var newObj = {};
      
        for (var key in obj) {
          newObj[key] = undefined;
        }
      
        return newObj;
      }
      
      // compares the structure of arbitrary values
      function compareObjectStructure(a, b) {
        return function innerCompare(a, b, pathA, pathB) {
          if (typeof a !== typeof b) {
            return false;
          }
      
          if (typeof a === 'object') {
            // both or neither, but not mismatched
            if (Array.isArray(a) !== Array.isArray(b)) {
              return false;
            }
      
            if (indexOf.call(pathA, a) !== -1 || indexOf.call(pathB, b) !== -1) {
              return false;
            }
      
            pathA = pathA.slice();
            pathA.push(a);
      
            pathB = pathB.slice();
            pathB.push(b);
      
            if (Array.isArray(a)) {
              // can't compare structure in array if we don't have items in both
              if (!a.length || !b.length) {
                return true;
              }
      
              for (var i = 1; i < a.length; i++) {
                if (!innerCompare(a[0], a[i], pathA, pathA)) {
                  return false;
                }
              }
      
              for (var i = 0; i < b.length; i++) {
                if (!innerCompare(a[0], b[i], pathA, pathB)) {
                  return false;
                }
              }
      
              return true;
            }
      
            var map = copyKeys(a), keys = Object.keys(b);
      
            for (var i = 0; i < keys.length; i++) {
              var key = keys[i];
      
              if (!hasOwn.call(map, key) || !innerCompare(a[key], b[key], pathA,
                  pathB)) {
                return false;
              }
      
              delete map[key];
            }
      
            // we should've found all the keys in the map
            return isObjectEmpty(map);
          }
      
          return true;
        }(a, b, [], []);
      }
      

      请注意,此实现直接比较两个对象的结构等效性,但不会将对象简化为可直接比较的值(如字符串)。我没有进行任何性能测试,但我怀疑它不会增加显着的价值,尽管它会消除重复确保对象是非循环的需要。因此,您可以轻松地将 compareObjectStructure 拆分为两个函数 - 一个用于比较结构,另一个用于检查循环。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-05-25
        • 2023-03-26
        • 2018-03-23
        • 2011-05-26
        • 1970-01-01
        • 2014-07-12
        • 1970-01-01
        • 2011-12-27
        相关资源
        最近更新 更多