Related
I am comparing two objects that contains values as string, number, array and object. To this point there is no problem. When I am trying to compare self-referenced objects I am getting the following error RangeError: Maximum call stack size exceeded. Self-referenced objects should be considered equal if they are referenced to the same level of the other object. My question is how to implement it. Here is my code :
const equalsComplex = function(value, other) {
// Get the value type
const type = Object.prototype.toString.call(value);
// If the two objects are not the same type, return false
if (type !== Object.prototype.toString.call(other)) return false;
// If items are not an object or array, return false
if (['[object Array]', '[object Object]'].indexOf(type) < 0) return false;
// Compare the length of the length of the two items
const valueLen =
type === '[object Array]' ? value.length : Object.keys(value).length;
const otherLen =
type === '[object Array]' ? other.length : Object.keys(other).length;
if (valueLen !== otherLen) return false;
// Compare two items
const compare = function(item1, item2) {
// Get the object type
const itemType = Object.prototype.toString.call(item1);
// If an object or array, compare recursively
if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) {
if (!equalsComplex(item1, item2)) return false;
}
// Otherwise, do a simple comparison
else {
// If the two items are not the same type, return false
if (itemType !== Object.prototype.toString.call(item2)) return false;
// Else if it's a function, convert to a string and compare
// Otherwise, just compare
if (itemType === '[object Function]') {
if (item1.toString() !== item2.toString()) return false;
} else {
if (item1 !== item2) return false;
}
}
};
// Compare properties
if (type === '[object Array]') {
for (let i = 0; i < valueLen; i++) {
if (compare(value[i], other[i]) === false) return false;
}
} else {
for (let key in value) {
if (value.hasOwnProperty(key)) {
if (compare(value[key], other[key]) === false) return false;
}
}
}
// If nothing failed, return true
return true;
};
const r = { a: 1 };
r.b = r;
const d = { a: 1 };
d.b = d;
console.log(
equalsComplex(
{
a: 2,
b: '2',
c: false,
g: [
{ a: { j: undefined } },
{ a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
r
]
},
{
a: 2,
b: '2',
c: false,
g: [
{ a: { j: undefined } },
{ a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
r
]
}
)
);
Before we begin
Is there a reason you aren't using an existing library like deep-equal? Sometimes it's easier to use code that's already written for you than to write it yourself
Now fixing a few simple issues in the code
For starters, utilizing Object.prototype.toString to determine the type feels like a hack, and might risk bugs in the future if different browsers implement the toString method differently. If someone knows whether or not the toString method's return value is explicitly defined in the ECMAScript specification, please chime in. Otherwise, I would avoid this hack, because JavaScript provides a perfect alternative: typeof https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
Interestingly typeof value will return the same for both objects and arrays, because as far as ECMAScript is concerned, arrays are a subclass of objects. Therefore your later comparison for [Object object] and [Object Array] can be simplified to just checking the type for object
Once you start using typeof value instead of Object.prototype.toString.apply(value), you will need a way to differentiate objects from arrays for comparison. For this purpose, you can use Array.isArray
On to the meat of the problem
Now regarding self-references, the issue you're referring to is a cycle. A simple cycle would be:
var a = {};
a.foo = a;
This creates the cycle: a.foo.foo.foo.foo.foo.... == a
There is a nice way to check if two references point to the same object in JavaScript, which is good for determining when equality is true, but it won't help in the case when equality is false. To check if two references point to the same object, just use the == operator! This returns true is the objects point to the exact same instance in memory. For instance:
var a = {foo: "bar"}
var b = {foo: "bar"}
var c = a;
a == b; // false
a == c; // true
b == c; // false
So you can trivially see if two references are the same by checking that item1 == item2
But when they don't equal, you will still do a complexCompare, which will dive into each self-reference, and will have the same stack overflow. To resolve this, you need a way to detect cycles. As with deep equality, there are libraries for this, but for intellectual reasons we'll see if we can recreate them.
To do this, we need to remember every other object we've seen, and compare with them as we recurse. A simple solution might look like:
var objectsWeveSeen = [];
function decycle(obj) {
for (var key in obj) {
if (typeof obj[key] == "object") {
for (var i = 0; i < objectsWeveSeen.length; i++) {
if (objectsWeveSeen[i] == obj[key]) {
obj[key] = "CYCLE! -- originally seen at index " + i;
}
}
objectsWeveSeen.push(obj[key]);
}
}
}
(NOTE: This decycle function is destructive. It modifies the original object. Also, this decycle function isn't recursive, so it actually sucks. But it at least gives you the general idea and you can try to write your own, or look at how others have done it)
We could then pass an object to it like so:
var a = {foo: {}};
a.baz = a.foo;
console.log(decycle(a));
// Outputs: {foo: {}, baz: "CYCLE! -- originally seen at index 0"}
Since this object lacks cycles, you can now perform your complex comparison on it:
complexCompare(decycle(a));
Of course there are still some edge cases to consider. Are two Date objects equivalent if they reference the same time, but have different timezones? Does null equal null? And my simple decycle algorithm fails to account for a reference to the root object, it only remembers all keys that it has seen (although this should be simple for you to add if you think about it)
A not-quite-perfect but working-on-it solution
I haven't written out a perfect deep-equals implementation for two reasons:
I feel like writing code is the best way to learn, not copying and pasting it from others
I'm sure there are edge cases I'm not thinking about (which is the reason you should use a battle-tested library like Lodash instead of writing your own code) and by admitting that this is an incomplete solution instead of selling it as what it isn't, you will be encouraged to go find someone who has written a more complete answer
function complexCompare(value, other) {
var objectsWeveSeen = [];
function nonDestructiveDecycle(obj) {
var newObj = {};
for (var key in obj) {
newObj[key] = obj[key];
if (typeof obj[key] == "object") {
for (var i = 0; i < objectsWeveSeen.length; i++) {
if (objectsWeveSeen[i] == obj[key]) {
newObj[key] = "CYCLE! -- originally seen at index " + i;
break;
}
}
objectsWeveSeen.push(obj[key]);
}
}
return newObj;
}
var type = typeof value;
if (type !== typeof other) return false;
if (type !== "object") return value === other;
if (Array.isArray(value)) {
if (!Array.isArray(other)) return false;
if (value.length !== other.length) return false;
for (var i = 0; i < value.length; i++) {
if (!complexCompare(value[i], other[i])) return false;
}
return true;
}
// TODO: Handle other "object" types, like Date
// Now we're dealing with JavaScript Objects...
var decycledValue = nonDestructiveDecycle(value);
var decycleOther = nonDestructiveDecycle(other);
for (var key in value) {
if (!complexCompare(decycledValue[key], decycleOther[key])) return false;
}
return true;
}
Update
In response to comments:
== versus ===
== performs a "loose" comparison between two variables. For instance, 3 == "3" will return true. === performs a "strict" comparison between two variables. So 3 === "3" will return false. In our case, you can use whichever you prefer and there should be no difference in the outcome, because:
typeof always returns a string. Therefore typeof x == typeof y is the exact same as typeof x === typeof y
If you check that two variables are the same type before you compare their values, you should never run into one of the edge cases where == and === return different results. For instance, 0 == false but typeof 0 != typeof false (0 is a "number" and false is a "boolean")
I stuck with == for my examples because I felt like it would be more familiar to avoid any confusion between the two
[] versus Set
I took a look at using Set to re-write decycle and quickly ran into an issue. You can use Set to detect if there is a cycle, but you can't trivially use it to detect that two cycles are identical. Notice that in my decycle method that I replace a cycle with the string CYCLE! -- originally seen at index X. The reason for this "at index X" is because it tell you which object was referenced. Instead of just having "some object we've seen before", we have "THAT object that we've seen before". Now if two objects reference the same one, we can detect that (because the strings will be equal, having the same index). If two objects reference different ones, we will detect that as well (because the strings will not be equal)
There is, however, a problem with my solution. Consider the following:
var a = {};
a.foo = a;
var b = {};
b.foo = b;
var c = {};
c.foo = a;
In this case my code would claim a and c are equal (because they both reference the same object) but a and b are not (because even though they have the same values, same patterns, and same structures - they reference different objects)
A better solution may be to replace the "index" (a number representing the order in which we found the objects) with "path" (a string representing how to reach the object)
var objectsWeveSeen = []
function nonDestructiveRecursiveDecycle(obj, path) {
var newObj = {};
for (var key in obj) {
var newPath = path + "." + key;
newObj[key] = obj[key];
if (typeof obj[key] == "object") {
for (var i = 0; i < objectsWeveSeen.length; i++) {
if (objectsWeveSeen[i].obj == obj[key]) {
newObj[key] = "$ref:" + objectsWeveSeen[i].path;
break;
}
}
if (typeof newObj[key] != "string") {
objectsWeveSeen.push({obj: obj[key], path: newPath});
newObj[key] = nonDestructiveRecursiveDecycle(obj[key], newPath);
}
}
}
return newObj;
}
var decycledValue = nonDestructiveRecursiveDecycle(value, "#root");
I like #stevendesu's response. He addresses the problem of the circular structure well. I wrote up a solution using your code that might be helpful as well.
const equalsComplex = function(value, other, valueRefs, otherRefs) {
valueRefs = valueRefs || [];
otherRefs = otherRefs || [];
// Get the value type
const type = Object.prototype.toString.call(value);
// If the two objects are not the same type, return false
if (type !== Object.prototype.toString.call(other)) return false;
// If items are not an object or array, return false
if (['[object Array]', '[object Object]'].indexOf(type) < 0) return false;
// We know that the items are objects or arrays, so let's check if we've seen this reference before.
// If so, it's a circular reference so we know that the branches match. If both circular references
// are in the same index of the list then they are equal.
valueRefIndex = valueRefs.indexOf(value);
otherRefIndex = otherRefs.indexOf(other);
if (valueRefIndex == otherRefIndex && valueRefIndex >= 0) return true;
// Add the references into the list
valueRefs.push(value);
otherRefs.push(other);
// Compare the length of the length of the two items
const valueLen =
type === '[object Array]' ? value.length : Object.keys(value).length;
const otherLen =
type === '[object Array]' ? other.length : Object.keys(other).length;
if (valueLen !== otherLen) return false;
// Compare two items
const compare = function(item1, item2) {
// Get the object type
const itemType = Object.prototype.toString.call(item1);
// If an object or array, compare recursively
if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) {
if (!equalsComplex(item1, item2, valueRefs.slice(), otherRefs.slice())) return false;
}
// Otherwise, do a simple comparison
else {
// If the two items are not the same type, return false
if (itemType !== Object.prototype.toString.call(item2)) return false;
// Else if it's a function, convert to a string and compare
// Otherwise, just compare
if (itemType === '[object Function]') {
if (item1.toString() !== item2.toString()) return false;
} else {
if (item1 !== item2) return false;
}
}
};
// Compare properties
if (type === '[object Array]') {
for (let i = 0; i < valueLen; i++) {
if (compare(value[i], other[i]) === false) return false;
}
} else {
for (let key in value) {
if (value.hasOwnProperty(key)) {
if (compare(value[key], other[key]) === false) return false;
}
}
}
// If nothing failed, return true
return true;
};
const r = { a: 1 };
r.b = {c: r};
const d = { a: 1 };
d.b = {c: d};
console.log(
equalsComplex(
{
a: 2,
b: '2',
c: false,
g: [
{ a: { j: undefined } },
{ a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
r
]
},
{
a: 2,
b: '2',
c: false,
g: [
{ a: { j: undefined } },
{ a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
d
]
}
)
);
Basically, you keep track of the references to objects and arrays that you have seen so far in each branch (the slice() method makes a shallow copy of the array of references). Then, every time you see an object or an array you check your history of references to see if it's a circular reference. If so, you make sure both circular references point to the same part of the history (this is important because both circular references might point to different places in the object structures).
I would recommend using a library for this since I haven't deeply tested my code, but there's a simple solution for you.
This package #enio.ai/data-ferret has a util method that supports data comparisons with circular reference support out-of-the-box.
First, install it npm i #enio.ai/data-ferret.
Then use it like so:
import { setConfig, isIdential, hasCircularReference } from '#enio.ai/data-ferret'
setConfig({ detectCircularReferences: true })
isIdential(a, b) // Returns boolean. Where a and b can contain circular reference
Full disclosure, I am the author of this package. I set out to solve this problem to scratch my own itch and decided to share it with the community. You can read the full specifications of the algorithm here https://github.com/enio-ireland/enio/blob/develop/packages/data-ferret/src/lib/isIdentical/isIdentical.spec.ts.
I'm trying to recursively search an object that contains strings, arrays, and other objects to find an item (match a value) at the deepest level however I'm always getting undefined as the return result. I can see through some console logging that I am finding the item but it gets overwritten. Any idea where I'm going wrong?
var theCobWeb = {
biggestWeb: {
item: "comb",
biggerWeb: {
items: ["glasses", "paperclip", "bubblegum"],
smallerWeb: {
item: "toothbrush",
tinyWeb: {
items: ["toenails", "lint", "wrapper", "homework"]
}
}
},
otherBigWeb: {
item: "headphones"
}
}
};
function findItem (item, obj) {
var foundItem;
for (var key in obj) {
if (obj[key] === item) {
foundItem = obj;
} else if (Array.isArray(obj[key]) && obj[key].includes(item)) {
foundItem = obj;
} else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
findItem(item, obj[key]);
}
}
return foundItem;
}
var foundIt = findItem('glasses', theCobWeb);
console.log('The item is here: ' + foundIt); // The item is here: undefined
Edit: cleaned up the code a bit based on feedback below.
function findItem (item, obj) {
for (var key in obj) {
if (obj[key] === item) { // if the item is a property of the object
return obj; // return the object and stop further searching
} else if (Array.isArray(obj[key]) && obj[key].includes(item)) { // if the item is inside an array property of the object
return obj; // return the object and stop the search
} else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { // if the property is another object
var res = findItem(item, obj[key]); // get the result of the search in that sub object
if(res) return res; // return the result if the search was successful, otherwise don't return and move on to the next property
}
}
return null; // return null or any default value you want if the search is unsuccessful (must be falsy to work)
}
Note 1: Array.isArray and Array.prototype.includes already returning booleans so there is no need to check them against booleans.
Note 2: You can flip the value of a boolean using the NOT operator (!).
Note3: You have to return the result (if found) immediately after it is found so you won't waste time looking for something you already have.
Note4: The return result of the search will be an object (if found) and since objects are passed by reference not by value, changing the properties of that object will change the properties of the original object too.
Edit: Find the deepest object:
If you want to find the deepest object, you'll have to go throug every object and sub-object in the object obj and everytime you have to store the object and it's depth (if the depth of the result is bigger than the previous result of course). Here is the code with some comments (I used an internal function _find that actually get called on all the objects):
function findItem (item, obj) {
var found = null; // the result (initialized to the default return value null)
var depth = -1; // the depth of the current found element (initialized to -1 so any found element could beat this one) (matched elements will not be assigned to found unless they are deeper than this depth)
function _find(obj, d) { // a function that take an object (obj) and on which depth it is (d)
for (var key in obj) { // for each ...
// first call _find on sub-objects (pass a depth of d + 1 as we are going to a one deep bellow)
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
_find(obj[key], d + 1);
}
// then check if this object actually contain the item (we still at the depth d)
else if (obj[key] === item || (Array.isArray(obj[key]) && obj[key].includes(item))) {
// if we found something and the depth of this object is deeper than the previously found element
if(d > depth) {
depth = d; // then assign the new depth
found = obj; // and assign the new result
}
}
}
}
_find(obj, 0); // start the party by calling _find on the object obj passed to findItem with a depth of 0
// at this point found is either the initial value (null) means nothing is found or it is an object (the deepest one)
return found;
}
"I'm trying to recursively search an object that contains strings, arrays, and other objects to find an item (match a value) at the deepest level however I'm always getting undefined as the return result."
var foundIt = findItem('glasses', theCobWeb);
console.log('The item is here: ' + foundIt); // The item is here: undefined
"The item is here ..." – where?
Well what exactly do you want as a return value? Should it just say "glasses" when it's all done? In my opinion, that's kind of pointless – fundamentally it's no better than just returning true or false.
I wrote this function a while ago because I needed to search a heap of data but also know specifically where it matched. I'd probably revise this a little bit now (or at least include type annotations), but it works as-is, so here you go.
// helpers
const keys = Object.keys
const isObject = x=> Object(x) === x
const isArray = Array.isArray
const rest = ([x,...xs])=> xs
// findDeep
const findDeep = (f,x) => {
let make = (x,ks)=> ({node: x, keys: ks || keys(x)})
let processNode = (parents, path, {node, keys:[k,...ks]})=> {
if (k === undefined)
return loop(parents, rest(path))
else if (isArray(node[k]) || isObject(node[k]))
return loop([make(node[k]), make(node, ks), ...parents], [k, ...path])
else if (f(node[k], k))
return {parents, path: [k,...path], node}
else
return loop([{node, keys: ks}, ...parents], path)
}
let loop = ([node,...parents], path) => {
if (node === undefined)
return {parents: [], path: [], node: undefined}
else
return processNode(parents, path, node)
}
return loop([make(x)], [])
}
// your sample data
var theCobWeb = {biggestWeb: {item: "comb",biggerWeb: {items: ["glasses", "paperclip", "bubblegum"],smallerWeb: {item: "toothbrush",tinyWeb: {items: ["toenails", "lint", "wrapper", "homework"]}}},otherBigWeb: {item: "headphones"}}};
// find path returns {parents, path, node}
let {path, node} = findDeep((value,key)=> value === "glasses", theCobWeb)
// path to get to the item, note it is in reverse order
console.log(path) // => [0, 'items', 'biggerWeb', 'biggestWeb']
// entire matched node
console.log(node) // => ['glasses', 'paperclip', 'bubblegum']
The basic intuition here is node[path[0]] === searchTerm
Complete path to the matched query
We get the entire key path to the matched data. This is useful because we know exactly where it is based on the root of our search. To verify the path is correct, see this example
const lookup = ([p,...path], x) =>
(p === undefined) ? x : lookup(path,x)[p]
lookup([0, 'items', 'biggerWeb', 'biggestWeb'], theCobWeb) // => 'glasses'
Unmatched query
Note if we search for something that is not found, node will be undefined
let {path, node} = findDeep((value,key)=> value === "sonic the hog", theCobWeb)
console.log(path) // => []
console.log(node) // => undefined
Searching for a specific key/value pair
The search function receives a value and key argument. Use them as you wish
let {path, node} = findDeep((value,key)=> key === 'item' && value === 'toothbrush', theCobWeb)
console.log(path) // => [ 'item', 'smallerWeb', 'biggerWeb', 'biggestWeb' ]
console.log(node) // => { item: 'toothbrush', tinyWeb: { items: [ 'toenails', 'lint', 'wrapper', 'homework' ] } }
Short circuit – 150cc
Oh and because I spoil you, findDeep will give an early return as soon as the first match is found. It won't waste computation cycles and continue iterating through your pile of data after it knows the answer. This is a good thing.
Go exploring
Have courage, be adventurous. The findDeep function above also gives a parents property on returned object. It's probably useful to you in some ways, but it's a little more complicated to explain and not really critical for answering the question. For the sake of keeping this answer simplified, I'll just mention it's there.
That's because the recursive call doesn't assign the return to the variable.
And you should check the return from the recursive call and return if true or break from the for loop if you have other logic after it.
function findItem(item, obj) {
for (var key in obj) {
if (obj[key] === item) {
return obj;
} else if (Array.isArray(obj[key]) === true && obj[key].includes(item) === true) {
return obj;
} else if (typeof obj[key] === 'object' && Array.isArray(obj[key]) === false) {
var foundItem = findItem(item, obj[key]);
if(foundItem)
return foundItem;
}
}
I am using angular-translate for a big application. Having several people committing code + translations, many times the translation objects are not in sync.
I am building a Grunt plugin to look at both files' structure and compare it (just the keys and overall structure, not values).
The main goals are:
Look into each file, and check if the structure of the whole object
(or file, in this case) is the exact same as the translated ones;
On error, return the key that doesn't match.
It turns out it was a bit more complicated than I anticipated. So i figured I could do something like:
Sort the object;
Check the type of data the value contains (since they are translations, it will only have strings, or objects for the nestings) and store it in another object, making the key equal to the original key and the value would be a string 'String', or an object in case it's an object. That object contains the children elements;
Recursively repeat steps 1-2 until the whole object is mapped and sorted;
Do the same for all the files
Stringify and compare everything.
A tiny example would be the following object:
{
key1: 'cool',
key2: 'cooler',
keyWhatever: {
anotherObject: {
key1: 'better',
keyX: 'awesome'
},
aObject: 'actually, it\'s a string'
},
aKey: 'more awesomeness'
}
would map to:
{
aKey: 'String',
key1: 'String',
key2: 'String',
keyWhatever: {
aObject: 'String',
anotherObject: {
key1: 'String',
keyX: 'String'
}
}
}
After this, I would stringify all the objects and proceed with a strict comparison.
My question is, is there a better way to perform this? Both in terms of simplicity and performance, since there are many translation files and they are fairly big.
I tried to look for libraries that would already do this, but I couldn't find any.
Thank you
EDIT: Thank you Jared for pointing out objects can't be sorted. I am ashamed for saying something like that :D Another solution could be iterating each of the properties on the main translation file, and in case they are strings, compare the key with the other files. In case they are objects, "enter" them, and do the same. Maybe it is even simpler than my first guess. What should be done?
Lets say you have two JSON objects, jsonA and 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);
Note that this can overflow the stack for deeply nested JSON objects. Note also that this will work for JSON but not necessarily regular JS objects as special handling is needed for Date Objects, Regexes, etc.
Actually you do need to sort the keys, as they are not required to be spit out in any particular order. Write a function,
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 + "}";
}
Where getValueRepresentation is a function that either returns "String" or calls getComparableForObject. If you are worried about circular references, add a Symbol to the outer scope, repr, assign obj[repr] = comparable in the function above, and in getValueRepresentation check every object for a defined obj[repr] and return it instead of processing it recursively.
Sorting an array of the keys from the object works. However, sorting has an average time complexity of O(n⋅log(n)). We can do better. A fast general algorithm for ensuring two sets A and B are equivalent is as follows:
for item in B
if item in A
remove item from A
else
sets are not equivalent
sets are equivalent iff A is empty
To address #Katana31, we can detect circular references as we go by maintaining a set of visited objects and ensuring that all descendents of that object are not already in the list:
# 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)
Here's a complete implementation (you can find a simplified version that assumes JSON/non-circular input 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, [], []);
}
Note that this implementation directly compares two objects for structural equivalency, but doesn't reduce the objects to a directly comparable value (like a string). I haven't done any performance testing, but I suspect that it won't add significant value, though it will remove the need to repeatedly ensure objects are non-cyclic. For that reason, you could easily split compareObjectStructure into two functions - one to compare the structure and one to check for cycles.
This question already has answers here:
How can I merge properties of two JavaScript objects dynamically?
(69 answers)
Closed 6 years ago.
I want to update an object that could look like this:
currentObject = {
someValue : "value",
myObject : {
attribute1 : "foo",
attribute2 : "bar"
}
};
.. with an object that contains some changes i.e.:
updateObject = {
myObject : {
attribute2 : "hello world"
}
};
At the end I would like to have currentObject updated so that:
currentObject.myObject.attribute2 == "hello world"
That should be posible for other objects as well..
As a solution I thought about iterating over the object and somehow take care of the namespace. But I wonder if there is an easy solution for that problem by using a library like jQuery or prototype.
I suggest using underscore.js (or better, lo-dash) extend:
_.extend(destination, *sources)
Copy all of the properties in the source objects over to the
destination object, and return the destination object. It's in-order,
so the last source will override properties of the same name in
previous arguments.
_.extend({name: 'moe'}, {age: 50});
=> {name: 'moe', age: 50}
function update(obj/*, …*/) {
for (var i=1; i<arguments.length; i++) {
for (var prop in arguments[i]) {
var val = arguments[i][prop];
if (typeof val == "object") // this also applies to arrays or null!
update(obj[prop], val);
else
obj[prop] = val;
}
}
return obj;
}
should do the trick: update(currentObject, updateObject). You might want to add some type checks, like Object(obj) === obj to extend only real objects with real objects, use a correct loop for arrays or hasOwnProperty tests.
Here's an Object.keys and recursive example:
// execute object update function
update(currentObject, updateObject)
// instantiate object update function
function update (targetObject, obj) {
Object.keys(obj).forEach(function (key) {
// delete property if set to undefined or null
if ( undefined === obj[key] || null === obj[key] ) {
delete targetObject[key]
}
// property value is object, so recurse
else if (
'object' === typeof obj[key]
&& !Array.isArray(obj[key])
) {
// target property not object, overwrite with empty object
if (
!('object' === typeof targetObject[key]
&& !Array.isArray(targetObject[key]))
) {
targetObject[key] = {}
}
// recurse
update(targetObject[key], obj[key])
}
// set target property to update property
else {
targetObject[key] = obj[key]
}
})
}
JSFiddle demo (open console).
A simple implementation would look like this.
function copyInto(target /*, source1, sourcen */) {
if (!target || typeof target !== "object")
target = {};
if (arguments.length < 2)
return target;
for (var len = arguments.length - 1; len > 0; len--)
cloneObject(arguments[len-1], arguments[len]);
return target;
}
function cloneObject(target, source) {
if (!source || !target || typeof source !== "object" || typeof target !== "object")
throw new TypeError("Invalid argument");
for (var p in source)
if (source.hasOwnProperty(p))
if (source[p] && typeof source[p] === "object")
if (target[p] && typeof target[p] === "object")
cloneObject(target[p], source[p]);
else
target[p] = source[p];
else
target[p] = source[p];
}
This assumes no inherited properties should be cloned. It also does no checks for things like DOM objects, or boxed primitives.
We need to iterate in reverse through the arguments so that the copy is done in a right to left matter.
Then we make a separate cloneObject function to handle the recursive copying of nested objects in a manner that doesn't interfere with the right to left copying of the original object arguments.
It also ensures that the initial target is a plain object.
The cloneObject function will throw an error if a non-object was passed to it.
I found myself in an interesting situation. I am using an object literal to represent a product in the real-world. Now each product has a length associated to it for shipping purposes. It looks something like this:
var product = {
name: 'MacBook Pro 15 Inch',
description: 'The new macbook pros....',
length: 15
height: 15
Weight: 4
}
This this works fine. But for products that have unknown length they default to length -1.
Again this works fine, until you try to do this:
console.log('Product has the following properties');
_.each(product, function(val, key) {
console.log(key + ":" + val);
});
No keys will be printed for a product that has a length of -1. Why? Well because internally underscore uses the length attribute, that every object in Javascript has to loop over all the attributes of the passed in object. Since we overwrote that value, it is now -1, and since we start looping at i = 0, the loop will never be executed.
Now for the question, how can I prevent the length property from being overridden? Best practices to prevent this from happening would also be appreciated.
try this:
var product = {
name: "MacBook Pro 15 Inch",
description: 'The new macbook pros....',
length: 15,
height: 15,
weight: 4
};
console.log('Product has the following properties');
_.each(_.keys(product), function(key){
console.log(key + ":" + product[key]);
});
This is perhaps due to some magic in _'s each function as I suspect it accepts both Objects and Arrays .. make your own?
Simple implementation (that avoids some common pitfalls):
function each(obj, map_func) {
var key;
for (key in obj) {
if (Object.prototype.hasOwnerProperty.call(obj, key)) {
map_func(key, obj[key]);
}
}
}
The magic in _'s each is probably due a nasty 'habit' in JavaScript of having things that 'look' like Arrays but aren't actually Arrays, like the magical variable arguments, and I think options in a select DOM Elements as well.
I can't think of anything off-hand that overrides it. I suppose using the length property is a form of duck typing that marks it by jQuery as iterable.
But.. you can just not use the jQuery each method, and do it the manual way..
By simply using
for (key in product) {
}
You get your basic loop. Now if you are potentially overriding the object's prototype, you should also check product.hashOwnProperty(key) to make sure the current key you're iterating is defined in the product instance.
Now if you also need a new closure scope, that's pretty simple too.. here's an alternative each function..
var myEach = function(subject, callback) {
for (key in subject) {
if (subject.hasOwnProperty(key)) {
callback(subject[key], key);
}
}
}
Note: untested.
If you can't change the name of the length property, you could add an additional check to underscore to prevent this case:
_.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length && obj.constructor != Object) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
The addition here is the && obj.constructor != Object on line 5. It works, though monkeypatching underscore may not be the most desirable thing in the world.
EDIT: Actually, this breaks on pseudo-arrays like arguments, since its constructor is Object, but <an array-like object>.hasOwnProperty(<an integer>) is false. Woo JavaScript.