【问题标题】:Javascript - deepEqual ComparisonJavascript - deepEqual 比较
【发布时间】:2014-10-16 20:07:54
【问题描述】:

问题(来自 Eloquent Javascript 第 2 版,第 4 章,练习 4):

编写一个函数 deepEqual,它接受两个值并仅在它们满足时返回 true 是相同的值,或者是具有相同属性的对象,其值也是 与对 deepEqual 的递归调用相比时相等。

测试用例:

var obj = {here: {is: "an"}, object: 2};
console.log(deepEqual(obj, obj));
// → true
console.log(deepEqual(obj, {here: 1, object: 2}));
// → false
console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
// → true

我的代码:

var deepEqual = function (x, y) {
  if ((typeof x == "object" && x != null) && (typeof y == "object" && y != null)) {
    if (Object.keys(x).length != Object.keys(y).length)
      return false;
    for (var prop in x) {
      if (y.hasOwnProperty(prop))
        return deepEqual(x[prop], y[prop]);
    /*This is most likely where my error is. The question states that all the values
    should be checked via recursion; however, with the current setup, only the first
    set of properties will be checked. It passes the test cases, but I would like
    to solve the problem correctly!*/
      }
    }
  else if (x !== y)
    return false;
  else
    return true;
}

我想我已经大致了解了;但是,就像我在评论中所说的那样,程序不会检查对象中的第二个属性。我觉得我有结构/逻辑问题,只是以错误的方式使用递归,因为我最初打算循环遍历属性,使用递归比较第一个属性的值,然后继续循环到下一个属性并再次比较。虽然,我不确定这是否可能?

我已经深思熟虑并尝试了几种不同的方法,但这是迄今为止我得出的最正确的答案。有什么提示可以为我指明正确的方向吗?

【问题讨论】:

    标签: javascript recursion


    【解决方案1】:

    您可以在 for 循环之外使用变量来跟踪比较:

    var allPropertiesEqual = true;
    for (var prop in x) {
        if (y.hasOwnProperty(prop)) {
            allPropertiesEqual = deepEqual(x[prop], y[prop]) && allPropertiesEqual;
        } else {
            allPropertiesEqual = false;
        }
    }
    return allPropertiesEqual;
    

    前面的例子不是故意优化的。因为您在比较对象,所以您知道一旦发现不等式就可以return false,并且可以在之前检查的所有属性都相等时继续循环:

    for (var prop in x) {
        if (y.hasOwnProperty(prop)) {
            if (! deepEqual(x[prop], y[prop]) )
                return false; //first inequality found, return false
        } else {
            return false; //different properties, so inequality, so return false
        }
    }
    return true;
    

    【讨论】:

    • { c : 3, d: 4 } 相比,{ a: 1, b: 2 } 将返回 true,因为 hasOwnProperty() 将始终为 false,并且将跳过所有测试。
    • 感谢您的回答!我希望我能给你们两个开绿支票,因为你们都有很好的详细答案。 :( 不过我还是给了你一个赞成票!
    【解决方案2】:

    正如您所怀疑的,您返回的是第一个看到的属性的匹配项。如果该属性不匹配,则应返回 false,否则请继续查找。

    此外,如果在 y 上找不到 prop 属性(即计数匹配,但实际属性不匹配),则返回 false

    如果所有属性都匹配,则返回true

    var deepEqual = function (x, y) {
      if (x === y) {
        return true;
      }
      else if ((typeof x == "object" && x != null) && (typeof y == "object" && y != null)) {
        if (Object.keys(x).length != Object.keys(y).length)
          return false;
    
        for (var prop in x) {
          if (y.hasOwnProperty(prop))
          {  
            if (! deepEqual(x[prop], y[prop]))
              return false;
          }
          else
            return false;
        }
        
        return true;
      }
      else 
        return false;
    }
    

    var deepEqual = function (x, y) {
      if (x === y) {
        return true;
      }
      else if ((typeof x == "object" && x != null) && (typeof y == "object" && y != null)) {
        if (Object.keys(x).length != Object.keys(y).length)
          return false;
    
        for (var prop in x) {
          if (y.hasOwnProperty(prop))
          {  
            if (! deepEqual(x[prop], y[prop]))
              return false;
          }
          else
            return false;
        }
    
        return true;
      }
      else 
        return false;
    }
    
    var obj = {here: {is: "an", other: "3"}, object: 2};
    console.log(deepEqual(obj, obj));
    // → true
    console.log(deepEqual(obj, {here: 1, object: 2}));
    // → false
    console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
    // → false
    console.log(deepEqual(obj, {here: {is: "an", other: "2"}, object: 2}));
    // → false
    console.log(deepEqual(obj, {here: {is: "an", other: "3"}, object: 2}));
    // → true

    【讨论】:

    • 对“x”的属性进行.hasOwnProperty() 检查可能也是一个好主意,或者,更好的是,使用通过调用Object.keys() 已经获得的返回值进行迭代,因为那些已经被限制为“自己的”属性。
    • 非常感谢!我对递归仍然很陌生,但是您的更改说明了有关该主题的一个很好的教训。我知道为什么我的代码不起作用,并且您的解决方案很简单。再次感谢,并享受投票!
    • 为什么不在函数顶部加上 if(x === y)?这样,如果返回 true,则不必通过第一个大 if 块
    • 绝对值得一试(和分析,看看它是否真的有所作为)。在这种情况下,我试图尽可能接近问题的结构,以明确差异。
    • @kharish 对象比较处理了这个问题。将数组视为其键为0, 1, 2... 的对象。 for... in 循环遍历这些“键”并比较值(在本例中为数组中的整数)。没有必要——也没有明智的方法——以不同的方式对待数组。示例:codepen.io/paulroub/pen/NWxvprK?editors=0011
    【解决方案3】:

    我对 JS 很陌生,但这是我解决它的方法:

    function deepEqual(obj1, obj2) {
    if (typeof obj1 === "object" && typeof obj2 === "object") {
        let isObjectMatch = false;
        for (let property1 in obj1) {
            let isPropertyMatch = false;
            for (let property2 in obj2) {
                if (property1 === property2) {
                    isPropertyMatch = deepEqual(obj1[property1], obj2[property2])
                }
    
                if(isPropertyMatch){
                    break;
                }
            }
    
            isObjectMatch  = isPropertyMatch;
    
            if (!isObjectMatch) {
                break;
            }
        }
    
        return isObjectMatch;
    } else {
        return obj1 === obj2;
    }
    }
    

    这是我的测试:

    var obj = {here: {is: "an"}, object: 2};
    console.log(deepEqual(obj, obj));
    // → true
    console.log(deepEqual(obj, {here: 1, object: 2}));
    // → false
    console.log(deepEqual(obj, {here: {is: "an"}, object: 2}))
    // → true
    console.log(deepEqual(obj, {object: 2, here: {is: "an"}}));
    // → true
    console.log(deepEqual(obj, {object: 1, here: {is: "an"}}));
    // → false
    console.log(deepEqual(obj, {objectt: 2, here: {is: "an"}}));
    // → false
    console.log(deepEqual(2, 2));
    // → true
    console.log(deepEqual(2, 3));
    // → false
    console.log(deepEqual(2, null));
    // → false
    console.log(deepEqual(null, null));
    // → false
    console.log(deepEqual(obj, null));
    // → false
    

    【讨论】:

      【解决方案4】:

      感觉这个版本的可读性更好一些(更容易理解)。不过,逻辑与最佳答案非常相似。 (这次是 ES6)

      function deepEqual(obj1, obj2) {
      
          if(obj1 === obj2) // it's just the same object. No need to compare.
              return true;
      
          if(isPrimitive(obj1) && isPrimitive(obj2)) // compare primitives
              return obj1 === obj2;
      
          if(Object.keys(obj1).length !== Object.keys(obj2).length)
              return false;
      
          // compare objects with same number of keys
          for(let key in obj1)
          {
              if(!(key in obj2)) return false; //other object doesn't have this prop
              if(!deepEqual(obj1[key], obj2[key])) return false;
          }
      
          return true;
      }
      
      //check if value is primitive
      function isPrimitive(obj)
      {
          return (obj !== Object(obj));
      }
      

      顺便说一句,有一个 cheater 版本的 deep equal 就像一个魅力))但是,它的速度大约慢了 1.6 倍。

      正如 zero298 所注意到的,这种方法对属性排序很敏感,不应认真对待

      function cheatDeepEqual(obj1, obj2)
      {
          return JSON.stringify(obj1) === JSON.stringify(obj2);
      }
      

      【讨论】:

      • 我认为如果属性顺序不一样,“骗子”版本可能会失败。考虑这个在 node.js 中测试的例子:JSON.stringify({foo:"bar",fizz:"buzz"}) === JSON.stringify({fizz:"buzz", foo:"bar"}); 是假的;但JSON.stringify({foo:"bar",fizz:"buzz"}) === JSON.stringify({foo:"bar",fizz:"buzz"}); 是真的。
      • @zero298,是的,我想你是对的。无论如何,这种方法不应该太认真:)
      • 另外,当您执行 JSON.stringify 并使用 parse 进行转换时,如果有任何日期对象,它将被转换为字符串类型仅供参考
      • Object.keys 需要空检查
      • deepEqual([],{}) 失败
      【解决方案5】:

      我刚刚看完这一章,也想展示我的作品。

      我的缺陷(如果还有更多,请告诉我)是对象属性也必须按正确的顺序排列。我更喜欢@paul 和@danni 的解决方案。

      // Deep equal 
      const deepEqual = (x, y) => {
        const xType = typeof x;
        const yType = typeof y; 
        
        if ( xType === 'object' && yType === 'object' && ( x !== null && y !== null ) ) {
          const xKeys = Object.keys(x);
          const yKeys = Object.keys(y);
          const xValues = Object.values(x);
          const yValues = Object.values(y);  
          
          // check length of both arrays
          if ( xKeys.length !== yKeys.length ) return false;
          
          // compare keys
          for ( i = 0; i < xKeys.length; i++ )
            if (xKeys[i] !== yKeys[i]) return false;
            
          // compare values
          for ( i = 0; i < xValues.length; i++ )
            if (!deepEqual(xValues[i], yValues[i])) return false;
            
        } else {
          if ( x !== y ) return false;
        }
        return true;
      };
      
      // Objects
      let obj1 = {
        value: false,
        pets: null
      };
      
      let obj2 = {
        value: false,
        pets: null
      };
      
      
      let obj3 = {
        value: false,
        pets: {
          cat: false,
          dog: {
            better: 'yes'
          }
        }
      };
      
      let obj4 = {
        value: false,
        pets: { 
          cat: false,
          dog: {
            better: 'yes'
          }
        }
      };
      
      
      let obj5 = {
        value: false,
        dog: true
      };
      
      let obj6 = {
        value: false,
        cat: true
      };
      
      
      let obj7 = {
        value: true,
        dog: {
          cat: {
            wow: true
          }
        }
      };
      
      let obj8 = {
        value: true,
        dog: {
          cat: {
            wow: false
          }
        }
      };
      
      
      let obj9 = {
        value: true,
        dog: {
          cat: {
            wow: true
          }
        }
      };
      
      let obj10 = {
        dog: {
          cat: {
            wow: true
          }
        },
        value: true
      };
      
      // Just for building a pretty result, ignore if you'd like
      const result = (x, y) => {
        return `For: <br/>
                ${JSON.stringify(x)} <br/>
                and <br/>
                ${JSON.stringify(y)} <br/>
                <span>>> ${deepEqual(x, y)}</span>`;
      };
      
      // To print results in
      const resultDivs = document.querySelectorAll('.result');
      
      resultDivs[0].innerHTML = result(obj1, obj2);
      resultDivs[1].innerHTML = result(obj3, obj4);
      resultDivs[2].innerHTML = result(obj5, obj6);
      resultDivs[3].innerHTML = result(obj7, obj8);
      resultDivs[4].innerHTML = result(obj9, obj10);
      body {
        font-family: monospace;
      }
      
      span {
        color: #a0a0a0;
      }
      
      .result {
        margin-bottom: 1em;
      }
      <div class="result">
      </div>
      
      <div class="result">
      </div>
      
      <div class="result">
      </div>
      
      <div class="result">
      </div>
      
      <div class="result">
      </div>

      【讨论】:

        【解决方案6】:

        <script>
        var cmp = function(element, target){
        
           if(typeof element !== typeof target)
           {
              return false;
           }
           else if(typeof element === "object" && (!target || !element))
           {
              return target === element;
           }
           else if(typeof element === "object")
           {
               var keys_element = Object.keys(element);
               var keys_target  = Object.keys(target);
               
               if(keys_element.length !== keys_target.length)
               {
                   return false;
               }
               else
               {
                   for(var i = 0; i < keys_element.length; i++)
                   {
                        if(keys_element[i] !== keys_target[i])
                            return false;
                        if(!cmp(element[keys_element[i]], target[keys_target[i]]))
                            return false;
                   }
        		   return true;
               }
           }
           else
           {
           	   return element === target;
        
           }
        };
        
        console.log(cmp({
            key1: 3,
            key2: "string",
            key3: [4, "45", {key4: [5, "6", false, null, {v:1}]}]
        }, {
            key1: 3,
            key2: "string",
            key3: [4, "45", {key4: [5, "6", false, null, {v:1}]}]
        })); // true
        
        console.log(cmp({
            key1: 3,
            key2: "string",
            key3: [4, "45", {key4: [5, "6", false, null, {v:1}]}]
        }, {
            key1: 3,
            key2: "string",
            key3: [4, "45", {key4: [5, "6", undefined, null, {v:1}]}]
        })); // false
        </script>

        【讨论】:

          【解决方案7】:

          虽然它更冗长,但也许这个选项更容易阅读:

          function deepEqual(elem1, elem2) {
              if(elem1 === elem2) {
                  return true;
              }
              if(typeof elem1 == 'object' && typeof elem2 == 'object' && elem1 != null && elem2 != null) {
                if(Object.keys(elem1).length == Object.keys(elem2).length) {
                    for(let key of Object.keys(elem1)) {
                        if(elem2.hasOwnProperty(key) != true) {
                            return false;
                        }
                    }
                    for(let key of Object.keys(elem1)) {
                        if(typeof elem1[key] == 'object' && typeof elem2[key] == 'object' && typeof elem1[key] != null && typeof elem2[key] != null) {
                            return deepEqual(elem1[key], elem2[key]);
                        }
                        else {
                          if(elem1[key] !== elem2[key]) {
                              return false;
                          }
                        }
                    } else {
                      return false;
                    }
                  }
                }
              else {
                  return false;
              }
              return true;
            }
          

          【讨论】:

            【解决方案8】:

            根据 Paul Roub 接受的答案,我还需要它来匹配函数值,并且我希望它更加简洁,因此我对其进行了重构。

            function deepEqual(x, y, z) {
              return x === y || typeof x == "function" && y && x.toString() == y.toString()
                || x && y && typeof x == "object" && x.constructor == y.constructor
                && (z = Object.keys(y)) && z.length == Object.keys(x).length
                && !z.find(v => !deepEqual(x[v], y[v]));
            }
            
            var myFunc = (x) => { return x*2; }
            var obj = {here: {is: "an", other: "3"}, object: 2, andFunc: myFunc};
            console.log(deepEqual(obj, obj));
            // → true
            console.log(deepEqual(obj, {here: 1, object: 2, andFunc: myFunc}));
            // → false
            console.log(deepEqual(obj, {here: {is: "an"}, object: 2, andFunc: myFunc}));
            // → false
            console.log(deepEqual(obj, {here: {is: "an", other: "2"}, object: 2, andFunc: myFunc}));
            // → false
            console.log(deepEqual(obj, {here: {is: "an", other: "3"}, object: 2, andFunc: myFunc}));
            // → true
            console.log(deepEqual(obj, {here: {is: "an", other: "3"}, object: 2, andFunc: (x) => { return x*2; }}));
            // → true
            console.log(deepEqual(obj, {here: {is: "an", other: "3"}, object: 2, andFunc: (x) => { return x*999; }}));
            // → false

            注释:

            • 您只传入 2 个参数:x 和 y(z 供内部使用)。
            • 如果变量之一是nullundefined,它会返回该值而不是false,但该结果仍然是“错误的”,所以我可以接受。要解决此问题,您可以将所有出现的 y &amp;&amp; 更改为 (y || !1) &amp;&amp;x &amp;&amp; 更改为 (x || !1) &amp;&amp;
            • 如果您绝对不希望在 然后删除您的对象|| typeof x == "function" &amp;&amp; y &amp;&amp; x.toString() == y.toString()

            【讨论】:

            • 根据打印方式比较函数是错误。试试这个:js const plus = x =&gt; y =&gt; x + y; deepEqual(plus(1), plus(2))
            【解决方案9】:

            之前的所有答案都包含细微的错误,在某些情况下会导致它们失败。它们要么 1) 依赖于相同顺序的属性,要么 2) 在某些情况下返回不对称结果,因此 deepEqual(a, b) !== deepEqual(b, a)。这是一个改进的答案,假设如下:

            • 我们对same-value equality感兴趣;我们希望deepEqual(NaN, NaN) 返回true,但deepEqual(0, -0) 返回false
            • 我们只关心直接在我们的对象上定义的可枚举的字符串键属性(即Object.keys() 返回的那些属性)。
            • 不需要完全支持循环引用。
            /**
             * Tests whether two values are deeply equal using same-value equality.
             *
             * Two values are considered deeply equal iff 1) they are the same value, or
             * 2) they are both non-callable objects whose own, enumerable, string-keyed
             * properties are deeply equal.
             *
             * Caution: This function does not fully support circular references. Use this
             * function only if you are sure that at least one of the arguments has no
             * circular references.
             */
            function deepEqual(x, y) {
                // If either x or y is not an object, then they are deeply equal iff they
                // are the same value. For our purposes, objects exclude functions,
                // primitive values, null, and undefined.
                if (typeof x !== "object" || x === null ||
                    typeof y !== "object" || y === null) {
                    // We use Object.is() to check for same-value equality. To check for
                    // strict equality, we would use x === y instead.
                    return Object.is(x, y);
                }
            
                // Shortcut, in case x and y are the same object. Every object is
                // deeply equal to itself.
                if (x === y)
                    return true;
            
                // Obtain the own, enumerable, string-keyed properties of x. We ignore
                // properties defined along x's prototype chain, non-enumerable properties,
                // and properties whose keys are symbols.
                const keys = Object.keys(x);
                // If x and y have a different number of properties, then they are not
                // deeply equal.
                if (Object.keys(y).length !== keys.length)
                    return false;
            
                // For each own, enumerable, string property key of x:
                for (const key of keys) {
                    // If key is not also an own enumerable property of y, or if x[key] and
                    // y[key] are not themselves deeply equal, then x and y are not deeply
                    // equal. Note that we don't just call y.propertyIsEnumerable(),
                    // because y might not have such a method (for example, if it was
                    // created using Object.create(null)), or it might not be the same
                    // method that exists on Object.prototype.
                    if (!Object.prototype.propertyIsEnumerable.call(y, key) ||
                        !deepEqual(x[key], y[key])) {
                        return false;
                    }
                }
            
                // x and y have the same properties, and all of those properties are deeply
                // equal, so x and y are deeply equal.
                return true;
            }
            

            【讨论】:

            • deepEqual([],{}) 失败
            • @hannadrehman 它为deepEqual([], {}) 返回true,根据文档注释这是正确的:它们都是不可调用的对象,具有零自己的、可枚举的、字符串键控的属性。但是对于很多应用程序(例如,比较 JSON 结构),您可能想要区分空对象和空数组,在这种情况下,您需要确保两个参数都是数组或非数组对象。但是没有适用于所有应用程序的 deepEqual() 函数,因为如何定义深度相等取决于用例。
            • 我不确定这是否正确。扩展到我的问题deepEqual({x:[]},{x:{}}) 不应该相等。但根据这个功能它是。 @McMath
            • @hannadrehman deepEqual({x:[]},{x:{}}) 根据文档注释中的定义正确返回true,它根本不区分数组和普通对象。还有更奇怪的例子:deepEqual([10], {0: 10}) 返回truedeepEqual(new Set(), new Date()) 也是如此。问题是这种深度相等的定义在您的用例中没有意义(可能是大多数用例,就此而言)。没有对每个应用程序都有意义的深度相等定义。我会在我的答案中添加这个警告。
            • 这很有趣。我们需要在这里遵循一些标准,否则可能会导致不需要的错误。我检查了节点assert 模块如何处理这种情况nodejs.org/api/…。它在那里得到妥善处理。使用 assert.deepEqual() 时,所有案例都给出了预期的结果
            猜你喜欢
            • 2017-02-02
            • 1970-01-01
            • 2012-04-28
            • 2013-11-21
            • 2016-11-18
            • 2017-11-26
            • 2018-09-13
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多