*通过共享* JavaScript 对象测试深度相等性
Posted
技术标签:
【中文标题】*通过共享* JavaScript 对象测试深度相等性【英文标题】:Test deep equality *with sharing* of JavaScript objects 【发布时间】:2015-12-23 22:52:56 【问题描述】:关于在 javascript 中测试两个对象的深度相等性的主题已经写了很多。然而,似乎没有人关心区分以下两个对象:
var o1 = [,];
var subitem = ;
var o2 = [subitem, subitem];
var o3 = [, ];
大多数深度相等算法会说o1
、o2
和o3
是相等的。我想要一个算法说o1
和o2
不相等,但o1
和o3
相等。换句话说,我想要一个算法来告诉我指针 graphs 是否具有相同的结构。我关心这一点,因为如果我对第一个元素进行了修改,则会在 o2
中反映在第二个元素中,而不是在 o1
中。
这意味着循环结构的深度相等应该起作用:
var o1 = [];
o1.push(o1);
var o2 = [];
o2.push(o2);
// deepGraphEqual(o1, o2) == true
var o3 = [[]];
o3[0].push(o3);
// deepGraphEqual(o1, o3) == false
如果您要避免改变项目,您可能需要 ECMAScript6 地图,所以我会接受使用这些地图的解决方案。
【问题讨论】:
“大多数深度相等算法会说 o1 和 o2 相等” - citation needed 所以你想检查两个数组是否引用同一个对象,即你想检查引用是否相等而不是值? 只需使用==
。 o1 == o2 // false
随心所欲
大家好,我澄清了这个问题,有一个误解。我不想测试 o1 和 o2 是否指向同一事物,但我想测试它们的指针结构所代表的“图形”是否具有相同的结构
是什么让o2
与o3
不同?
【参考方案1】:
没有 ES6 特性的版本,在二次时间中运行:
function deepGraphEqual(a, b)
var left = [], right = [], has = Object.prototype.hasOwnProperty;
function visit(a, b)
var i, k;
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
return a === b;
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
return false;
for (i = 0; i < left.length; i++)
if (a === left[i])
return b === right[i];
if (b === right[i])
return a === left[i];
for (k in a)
if (has.call(a, k) && !has.call(b, k))
return false;
for (k in b)
if (has.call(b, k) && !has.call(a, k))
return false;
left.push(a);
right.push(b);
for (k in a)
if (has.call(a, k) && !visit(a[k], b[k]))
return false;
return true;
return visit(a, b);
带有 ES6 Map
的版本在线性时间内运行:
function deepGraphEqual(a, b)
let left = new Map(), right = new Map(), has = Object.prototype.hasOwnProperty;
function visit(a, b)
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
return a === b;
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
return false;
if (left.has(a))
return left.get(a) === b
if (right.has(b))
return right.get(b) === a
for (let k in a)
if (has.call(a, k) && !has.call(b, k))
return false;
for (let k in b)
if (has.call(b, k) && !has.call(a, k))
return false;
left.set(a, b);
right.set(b, a);
for (let k in a)
if (has.call(a, k) && !visit(a[k], b[k]))
return false;
return true;
return visit(a, b);
【讨论】:
第二种算法看起来确实不是线性的。 @djechlin:是的。对于图中的每个节点,我们所做的工作量与该节点的边数成正比。我们最多处理每个节点一次,因此总工作量与图的大小(节点加边)呈线性关系。 WTH 是if (a.prototype !== b.prototype)
应该怎么做?
NaN
怎么样?它不应该被认为是平等的吗?在现有实现的基础上构建可能比自行开发更好。
@Bergi:我已将其更正为 if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
。 @FelixKling:我考虑过这一点,并且确实查看了现有的实现,例如 node.js assert.deepEqual()
,它也将 NaN
视为不等于自身。如果你想让NaN
等于它自己,你可以用Object.is(a, b)
替换a === b
测试。【参考方案2】:
如何改进 Anders Kaseorg 的回答:
如果在非常大型数据结构上使用该算法,可能会出现堆栈溢出错误。例如,具有 5,000 个节点的完整图会发生这种情况。所以我写了一个非递归版本,它使用广度优先搜索而不是深度优先搜索,因为这似乎更容易实现(不使用递归时)。迭代版本适用于具有 5,000 个节点的完整图形(不过,在我的机器上需要 6 秒)。这里是:
function deepEqual(item1, item2)
var EQUAL_ATOM = 1, UNEQUAL = 2, OBJECT = 3;
function compareSimple(first, second)
var ty1 = typeof first, ty2 = typeof second;
if (ty1!==ty2) return UNEQUAL;
if (ty1!=='object')
if (first===second) return EQUAL_ATOM;
if ((ty1==='number')&&isNaN(first)&&isNaN(second)) return EQUAL_ATOM;
return UNEQUAL;
if (first===null) return (second===null) ? EQUAL_ATOM : UNEQUAL;
if (second===null) return UNEQUAL;
if (Object.getPrototypeOf(first) !== Object.getPrototypeOf(second)) return UNEQUAL;
return OBJECT;
var c = compareSimple(item1, item2);
if (c !== OBJECT) return (c===EQUAL_ATOM);
var stack1 = [], stack2 = [], inverse1 = new Map(), inverse2 = new Map();
stack1.push(item1); stack2.push(item2);
inverse1.set(item1, 0); inverse2.set(item2, 0);
var currentIdx = 0;
var firstItem, secondItem, i, own, has1, has2, key, kid1, kid2, itemCount;
while (currentIdx < stack1.length)
firstItem = stack1[currentIdx]; secondItem = stack2[currentIdx];
own = ;
for (key in firstItem)
has1 = firstItem.hasOwnProperty(key);
has2 = secondItem.hasOwnProperty(key);
if (has1 !== has2) return false;
if (has1) own[key] = null;
for (key in secondItem)
if (!(key in own))
has1 = firstItem.hasOwnProperty(key);
has2 = secondItem.hasOwnProperty(key);
if (has1 !== has2) return false;
if (has1) own[key] = null;
for (key in own)
kid1 = firstItem[key];
kid2 = secondItem[key];
c = compareSimple(kid1, kid2);
if (c === UNEQUAL) return false;
if (c === OBJECT)
has1 = inverse1.has(kid1);
has2 = inverse2.has(kid2);
if (has1 !== has2) return false;
if (has1)
if (inverse1.get(kid1) !== inverse2.get(kid2)) return false;
else
itemCount = stack1.length;
stack1.push(kid1); stack2.push(kid2);
inverse1.set(kid1, itemCount); inverse2.set(kid2, itemCount);
++currentIdx;
return true;
我在 jsperf.com 网站上添加了一些速度测试。有趣的是,根据数据结构,有时 Anders 的递归版本更快,有时我的迭代版本更快,平均而言,Anders 更受青睐。
这里是 jsperf 测试的链接:
nephews example
cycle free real world JSON from reddit example
uncles example
complete graph with 2K nodes
complete graph with 5K nodes
此外,内置对象不会以您可能想要的方式处理。许多或大多数内置对象“隐藏”它们的键。如果你做Object.keys(...)
,你只会得到一个空数组。
now = new Date();
keys = Object.keys(now); // result: []
因此,例如,任意 2 个 Date
s 彼此是 deepGraphEqual
,任意 2 个 RegExp
s 也是。这很可能不是你想要的。对于所有这些,我没有“包罗万象”,并且遍历所有现有的“内置”对象需要很长时间。但是对于Dates
和RegExp
s,here 是你可以放入更合理的东西,使用.toString()
来比较它们。
【讨论】:
以上是关于*通过共享* JavaScript 对象测试深度相等性的主要内容,如果未能解决你的问题,请参考以下文章