Find property by name in a deep object - javascript

I have a HUGE collection and I am looking for a property by key someplace inside the collection. What is a reliable way to get a list of references or full paths to all objects containing that key/index? I use jQuery and lodash if it helps and you can forget about infinite pointer recursion, this is a pure JSON response.
fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "d");
// [o.c]
fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "e");
// [o.c.d]
fn({ 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');
// [o.cc,o.cc.dd]
fwiw lodash has a _.find function that will find nested objects that are two nests deep, but it seems to fail after that. (e.g. http://codepen.io/anon/pen/bnqyh)

This should do it:
function fn(obj, key) {
if (_.has(obj, key)) // or just (key in obj)
return [obj];
// elegant:
return _.flatten(_.map(obj, function(v) {
return typeof v == "object" ? fn(v, key) : [];
}), true);
// or efficient:
var res = [];
_.forEach(obj, function(v) {
if (typeof v == "object" && (v = fn(v, key)).length)
res.push.apply(res, v);
});
return res;
}

a pure JavaScript solution would look like the following:
function findNested(obj, key, memo) {
var i,
proto = Object.prototype,
ts = proto.toString,
hasOwn = proto.hasOwnProperty.bind(obj);
if ('[object Array]' !== ts.call(memo)) memo = [];
for (i in obj) {
if (hasOwn(i)) {
if (i === key) {
memo.push(obj[i]);
} else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
findNested(obj[i], key, memo);
}
}
}
return memo;
}
here's how you'd use this function:
findNested({'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');
and the result would be:
[{x: 9}, {y: 9}]

this will deep search an array of objects (hay) for a value (needle) then return an array with the results...
search = function(hay, needle, accumulator) {
var accumulator = accumulator || [];
if (typeof hay == 'object') {
for (var i in hay) {
search(hay[i], needle, accumulator) == true ? accumulator.push(hay) : 1;
}
}
return new RegExp(needle).test(hay) || accumulator;
}

If you can write a recursive function in plain JS (or with combination of lodash) that will be the best one (by performance), but if you want skip recursion from your side and want to go for a simple readable code (which may not be best as per performance) then you can use lodash#cloneDeepWith for any purposes where you have to traverse a object recursively.
let findValuesDeepByKey = (obj, key, res = []) => (
_.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)
So, the callback you passes as the 2nd argument of _.cloneDeepWith will recursively traverse all the key/value pairs recursively and all you have to do is the operation you want to do with each. the above code is just a example of your case. Here is a working example:
var object = {
prop1: 'ABC1',
prop2: 'ABC2',
prop3: {
prop4: 'ABC3',
prop5Arr: [{
prop5: 'XYZ'
},
{
prop5: 'ABC4'
},
{
prop6: {
prop6NestedArr: [{
prop1: 'XYZ Nested Arr'
},
{
propFurtherNested: {key100: '100 Value'}
}
]
}
}
]
}
}
let findValuesDeepByKey = (obj, key, res = []) => (
_.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)
console.log(findValuesDeepByKey(object, 'prop1'));
console.log(findValuesDeepByKey(object, 'prop5'));
console.log(findValuesDeepByKey(object, 'key100'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

With Deepdash you can pickDeep and then get paths from it, or indexate (build path->value object)
var obj = { 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}
var cherry = _.pickDeep(obj,"d");
console.log(JSON.stringify(cherry));
// {"cc":{"d":{}},"dd":{"d":{}}}
var paths = _.paths(cherry);
console.log(paths);
// ["cc.d", "dd.d"]
paths = _.paths(cherry,{pathFormat:'array'});
console.log(JSON.stringify(paths));
// [["cc","d"],["dd","d"]]
var index = _.indexate(cherry);
console.log(JSON.stringify(index));
// {"cc.d":{},"dd.d":{}}
Here is a Codepen demo

Something like this would work, converting it to an object and recursing down.
function find(jsonStr, searchkey) {
var jsObj = JSON.parse(jsonStr);
var set = [];
function fn(obj, key, path) {
for (var prop in obj) {
if (prop === key) {
set.push(path + "." + prop);
}
if (obj[prop]) {
fn(obj[prop], key, path + "." + prop);
}
}
return set;
}
fn(jsObj, searchkey, "o");
}
Fiddle: jsfiddle

In case you don't see the updated answer from #eugene, this tweak allows for passing a list of Keys to search for!
// Method that will find any "message" in the Apex errors that come back after insert attempts
// Could be a validation rule, or duplicate record, or pagemessage.. who knows!
// Use in your next error toast from a wire or imperative catch path!
// message: JSON.stringify(this.findNested(error, ['message', 'stackTrace'])),
// Testing multiple keys: this.findNested({thing: 0, list: [{message: 'm'}, {stackTrace: 'st'}], message: 'm2'}, ['message', 'stackTrace'])
findNested(obj, keys, memo) {
let i,
proto = Object.prototype,
ts = proto.toString,
hasOwn = proto.hasOwnProperty.bind(obj);
if ('[object Array]' !== ts.call(memo)) memo = [];
for (i in obj) {
if (hasOwn(i)) {
if (keys.includes(i)) {
memo.push(obj[i]);
} else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
this.findNested(obj[i], keys, memo);
}
}
}
return memo.length == 0 ? null : memo;
}

Here's how I did it:
function _find( obj, field, results )
{
var tokens = field.split( '.' );
// if this is an array, recursively call for each row in the array
if( obj instanceof Array )
{
obj.forEach( function( row )
{
_find( row, field, results );
} );
}
else
{
// if obj contains the field
if( obj[ tokens[ 0 ] ] !== undefined )
{
// if we're at the end of the dot path
if( tokens.length === 1 )
{
results.push( obj[ tokens[ 0 ] ] );
}
else
{
// keep going down the dot path
_find( obj[ tokens[ 0 ] ], field.substr( field.indexOf( '.' ) + 1 ), results );
}
}
}
}
Testing it with:
var obj = {
document: {
payload: {
items:[
{field1: 123},
{field1: 456}
]
}
}
};
var results = [];
_find(obj.document,'payload.items.field1', results);
console.log(results);
Outputs
[ 123, 456 ]

We use object-scan for data processing tasks. It's pretty awesome once you've wrapped your head around how to use it.
// const objectScan = require('object-scan');
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' } } };
const r = objectScan(['a.*.*'], { joined: true, rtn: 'entry' })(haystack);
console.log(r);
// => [ [ 'a.e.f', 'g' ], [ 'a.b.c', 'd' ] ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
There are plenty more examples on the website.

The shortest and simplest solution:
Array.prototype.findpath = function(item,path) {
return this.find(function(f){return item==eval('f.'+path)});
}

Related

How to deeply remove null values, empty objects and empty array from an object

I have an object that looks like this:
var myObject = { a: { b: [{}], c: [{}, {d: 2}], e: 2, f: {} }, g:{}, h:[], i: [null, 2] }
I want to remove null values and and empty objects (array and objects) so that it looks like:
{ a: {c: [ {d: 2} ], e: 2 }, i: [ 2 ] }
The function should remove null values, empty objects and empty arrays. Any elegant way to do it ?
Here is a function that clean the object recursively. It will loop deeply through all the properties and remove null values, null arrays and null objects:
cleanUpObject(jsonObject: object): object {
Object.keys(jsonObject).forEach(function (key, index) {
const currentObj = jsonObject[key]
if (_.isNull(currentObj)) {
delete jsonObject[key]
} else if (_.isObject(currentObj)) {
if (_.isArray(currentObj)) {
if (!currentObj.length) {
delete jsonObject[key]
} else {
const cleanupArrayObj = []
for (const obj of currentObj) {
if (!_.isNull(obj)) {
const cleanObj = this.cleanUpJson(obj)
if (!_.isEmpty(cleanObj)) {
cleanupArrayObj.push(cleanObj)
}
}
}
if (!cleanupArrayObj.length) {
delete jsonObject[key]
} else {
jsonObject[key] = cleanupArrayObj
}
}
} else {
if (_.isEmpty(Object.keys(jsonObject[key]))) {
delete jsonObject[key]
} else {
jsonObject[key] = this.cleanUpJson(currentObj)
if (_.isEmpty(Object.keys(jsonObject[key]))) {
delete jsonObject[key]
}
}
}
}
}, this)
return jsonObject
}
We don't know what you mean by clean, but from what I understand, you want to remove all null and empty values. This algorithm is straight-forward: recursively check for and remove any empty / null values (which are recursively checked).
function clean(obj) {
// clean array
if (Array.isArray(obj)) {
for (let i=0; i<obj.length; i++) {
if (isNothing(obj[i])) obj.splice(i, 1); // remove value if falsy
else if (typeof obj[i] === 'object') clean(obj[i]); // recurse if it's a truthy object
}
// clean other object
} else {
for (let prop in obj) {
if (!obj.hasOwnProperty(prop)) continue;
if (isNothing(obj[prop])) delete obj[prop]; // remove value if falsy
else if (typeof obj[prop] === 'object') clean(obj[prop]); // recurse if it's a truthy object
}
}
}
// Recursively check for populated or nonnull content. If none found, return `true`. Recursive so [{}] will be treated as empty.
function isNothing(item) {
// null / undefined
if (item == null) return true;
// deep object falsiness
if (typeof item === 'object') {
if (Array.isArray(item)) {
// array -> check for populated/nonnull value
for (let i=0; i<item.length; i++) {
if (!isNothing(item[i])) return false;
}
return true;
}
// other object -> check for populated/nonnull value
for (let prop in item) {
if (!item.hasOwnProperty(prop)) continue;
if (!isNothing(item[prop])) return false;
}
return true;
}
return false;
}
var myObject = { a: { b: [{}], c: [{}, {d: 2}], e: 2, f: {} }, g:{}, h:[], i: [null, 2] };
console.log("Before: " + JSON.stringify(myObject));
clean(myObject);
console.log("After: " + JSON.stringify(myObject));
To reduce repetitive code, one option is to define a function (let's call it itemToBool) that can determine whether a generic value passed to it is truthy, or recursively truthy somewhere, should the value be an array or object. Then, in the function that gets passed the original object (or, gets recursively passed an object or array), you can call that itemToBool function whenever there's a value to validate.
In the case of arrays, map by itemToBool and then filter by Boolean. In the case of objects, reduce the entries of the object into another object: pass each value of the object through itemToBool to recursively transform it (in case the value is an array or object), and if the transformed value has any keys (or is a truthy primitive), assign it to the accumulator. No need to depend a library:
var myObject = {
a: {
b: [{}],
c: [{}, {
d: 2
}],
e: 2,
f: {}
},
g: {},
h: [],
i: [null, 2]
};
// Returns a falsey value if the item is falsey,
// or if the deep cleaned array or object is empty:
const itemToBool = item => {
if (typeof item !== 'object' || item === null) return item;
const cleanedItem = clean(item);
return Object.keys(cleanedItem).length !== 0 && cleanedItem;
};
const clean = obj => {
if (Array.isArray(obj)) {
const newArr = obj.map(itemToBool).filter(Boolean);
return newArr.length && newArr;
}
const newObj = Object.entries(obj).reduce((a, [key, val]) => {
const newVal = itemToBool(val);
if (newVal) a[key] = newVal;
return a;
}, {});
return Object.keys(newObj).length > 0 && newObj;
};
console.log(clean(myObject));
Hmm... you also might abstract the check of the number of keys into a function as well:
var myObject={a:{b:[{}],c:[{},{d:2}],e:2,f:{}},g:{},h:[],i:[null,2]}
// Returns the object / array if it has at least one key, else returns false:
const validObj = obj => Object.keys(obj).length && obj;
const itemToBool = item => (
typeof item !== 'object' || item === null
? item
: validObj(clean(item))
);
const clean = obj => validObj(
Array.isArray(obj)
? obj.map(itemToBool).filter(Boolean)
: Object.entries(obj).reduce((a, [key, val]) => {
const newVal = itemToBool(val);
if (newVal) a[key] = newVal;
return a;
}, {})
);
console.log(clean(myObject));

how to get keys of nested object

If I have a flat object then this works:
let stateCopy={...this.state}
Object.entries(dictionary).map(([key,value])=>{
stateCopy.key = value.toString())
})
Is there a way to do this if dictionary contains a nested object. Suppose a dictionary looks like:
dictionary={left:{name:'WORK',
min:2,
sec:0,}
start:true}
I need some way of updating stateCopy, i.e
stateCopy.left.name='WORK'
stateCopy.left.min=2
stateCopy.left.sec=0
stateCopy.start=true
function flattenDictionary(dict) {
if (!dict) {
return {};
}
/** This will hold the flattened keys/values */
const keys = {};
// Perform the flatten
flattenH(dict);
return keys;
function flattenH(obj, prefix) {
Object.keys(obj).forEach((key) => {
const val = obj[key];
/** This is what we pass forward as a new prefix, or is the flattened key */
let passKey;
// Only expect to see this when the original dictionary is passed as `obj`
if (!prefix || prefix === '') {
passKey = key;
} else {
// "Ignore" keys that are empty strings
passKey = ((key === '') ? prefix : `${prefix}.${key}`);
}
if (typeof obj[key] !== 'object') {
keys[passKey] = val;
} else {
flattenH(val, passKey);
}
});
}
}
Seems like you can do this with a little recursive function:
let state = {
left:{
start: "mark",
anotherLevel: {
test: 'leveltest'
}
},
test: "will be replaced"
}
let dictionary={
test2: {
foo: 'bar'
},
left:{
name:'WORK',
min:2,
sec:0,
anotherLevel: {
test_add: 'leveltest_add'
}
},
start:true,
test: 'replaced with me'
}
let stateCopy={...state}
function merge(obj, dict){
Object.entries(dict).forEach(([k, v]) =>{
if (!obj[k] || typeof v !== 'object') obj[k] = v
else merge(obj[k], v)
})
}
merge(stateCopy, dictionary)
console.log(stateCopy)

How to do a deep comparison between 2 objects with lodash?

I have 2 nested objects which are different and I need to know if they have a difference in one of their nested properties.
var a = {};
var b = {};
a.prop1 = 2;
a.prop2 = { prop3: 2 };
b.prop1 = 2;
b.prop2 = { prop3: 3 };
The object could be much more complex with more nested properties. But this one is a good example. I have the option to use recursive functions or something with lodash...
An easy and elegant solution is to use _.isEqual, which performs a deep comparison:
var a = {};
var b = {};
a.prop1 = 2;
a.prop2 = { prop3: 2 };
b.prop1 = 2;
b.prop2 = { prop3: 3 };
console.log(_.isEqual(a, b)); // returns false if different
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
However, this solution doesn't show which property is different.
If you need to know which properties are different, use reduce():
_.reduce(a, function(result, value, key) {
return _.isEqual(value, b[key]) ?
result : result.concat(key);
}, []);
// → [ "prop2" ]
For anyone stumbling upon this thread, here's a more complete solution. It will compare two objects and give you the key of all properties that are either only in object1, only in object2, or are both in object1 and object2 but have different values:
/*
* Compare two objects by reducing an array of keys in obj1, having the
* keys in obj2 as the intial value of the result. Key points:
*
* - All keys of obj2 are initially in the result.
*
* - If the loop finds a key (from obj1, remember) not in obj2, it adds
* it to the result.
*
* - If the loop finds a key that are both in obj1 and obj2, it compares
* the value. If it's the same value, the key is removed from the result.
*/
function getObjectDiff(obj1, obj2) {
const diff = Object.keys(obj1).reduce((result, key) => {
if (!obj2.hasOwnProperty(key)) {
result.push(key);
} else if (_.isEqual(obj1[key], obj2[key])) {
const resultKeyIndex = result.indexOf(key);
result.splice(resultKeyIndex, 1);
}
return result;
}, Object.keys(obj2));
return diff;
}
Here's an example output:
// Test
let obj1 = {
a: 1,
b: 2,
c: { foo: 1, bar: 2},
d: { baz: 1, bat: 2 }
}
let obj2 = {
b: 2,
c: { foo: 1, bar: 'monkey'},
d: { baz: 1, bat: 2 }
e: 1
}
getObjectDiff(obj1, obj2)
// ["c", "e", "a"]
If you don't care about nested objects and want to skip lodash, you can substitute the _.isEqual for a normal value comparison, e.g. obj1[key] === obj2[key].
Based on the answer by Adam Boduch, I wrote this function which compares two objects in the deepest possible sense, returning paths that have different values as well as paths missing from one or the other object.
The code was not written with efficiency in mind, and improvements in that regard are most welcome, but here is the basic form:
var compare = function (a, b) {
var result = {
different: [],
missing_from_first: [],
missing_from_second: []
};
_.reduce(a, function (result, value, key) {
if (b.hasOwnProperty(key)) {
if (_.isEqual(value, b[key])) {
return result;
} else {
if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
//dead end.
result.different.push(key);
return result;
} else {
var deeper = compare(a[key], b[key]);
result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
return key + "." + sub_path;
}));
result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
return key + "." + sub_path;
}));
result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
return key + "." + sub_path;
}));
return result;
}
}
} else {
result.missing_from_second.push(key);
return result;
}
}, result);
_.reduce(b, function (result, value, key) {
if (a.hasOwnProperty(key)) {
return result;
} else {
result.missing_from_first.push(key);
return result;
}
}, result);
return result;
}
You can try the code using this snippet (running in full page mode is recommended):
var compare = function (a, b) {
var result = {
different: [],
missing_from_first: [],
missing_from_second: []
};
_.reduce(a, function (result, value, key) {
if (b.hasOwnProperty(key)) {
if (_.isEqual(value, b[key])) {
return result;
} else {
if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
//dead end.
result.different.push(key);
return result;
} else {
var deeper = compare(a[key], b[key]);
result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
return key + "." + sub_path;
}));
result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
return key + "." + sub_path;
}));
result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
return key + "." + sub_path;
}));
return result;
}
}
} else {
result.missing_from_second.push(key);
return result;
}
}, result);
_.reduce(b, function (result, value, key) {
if (a.hasOwnProperty(key)) {
return result;
} else {
result.missing_from_first.push(key);
return result;
}
}, result);
return result;
}
var a_editor = new JSONEditor($('#a')[0], {
name: 'a',
mode: 'code'
});
var b_editor = new JSONEditor($('#b')[0], {
name: 'b',
mode: 'code'
});
var a = {
same: 1,
different: 2,
missing_from_b: 3,
missing_nested_from_b: {
x: 1,
y: 2
},
nested: {
same: 1,
different: 2,
missing_from_b: 3
}
}
var b = {
same: 1,
different: 99,
missing_from_a: 3,
missing_nested_from_a: {
x: 1,
y: 2
},
nested: {
same: 1,
different: 99,
missing_from_a: 3
}
}
a_editor.set(a);
b_editor.set(b);
var result_editor = new JSONEditor($('#result')[0], {
name: 'result',
mode: 'view'
});
var do_compare = function() {
var a = a_editor.get();
var b = b_editor.get();
result_editor.set(compare(a, b));
}
#objects {} #objects section {
margin-bottom: 10px;
}
#objects section h1 {
background: #444;
color: white;
font-family: monospace;
display: inline-block;
margin: 0;
padding: 5px;
}
.jsoneditor-outer, .ace_editor {
min-height: 230px !important;
}
button:hover {
background: orangered;
}
button {
cursor: pointer;
background: red;
color: white;
text-align: left;
font-weight: bold;
border: 5px solid crimson;
outline: 0;
padding: 10px;
margin: 10px 0px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="objects">
<section>
<h1>a (first object)</h1>
<div id="a"></div>
</section>
<section>
<h1>b (second object)</h1>
<div id="b"></div>
</section>
<button onClick="do_compare()">compare</button>
<section>
<h1>result</h1>
<div id="result"></div>
</section>
</div>
Here's a concise solution using Lodash:
_.differenceWith(a, b, _.isEqual);
Note both inputs need to be arrays (possibly an array of one object).
To recursively show how an object is different with other you can use _.reduce combined with _.isEqual and _.isPlainObject. In this case you can compare how a is different with b or how b is different with a:
const objectA = {
a: {
1: "SAME WILL BE MISSING IN RESULT",
2: "BBB",
3: [1, 2, 3]
},
b: "not",
c: "foo bar"
};
const objectB = {
a: {
1: "SAME WILL BE MISSING IN RESULT",
2: [1, 2]
},
b: "foo",
c: "bar"
};
const diff = function(obj1, obj2) {
return _.reduce(obj1, function(result, value, key) {
if (_.isPlainObject(value)) {
result[key] = diff(value, obj2[key]);
} else if (!_.isEqual(value, obj2[key])) {
result[key] = value;
}
return result;
}, {});
};
const diffAOverB = diff(objectA, objectB);
const diffBOverA = diff(objectA, objectB);
console.log(diffAOverB);
console.log(diffBOverA);
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.4/lodash.min.js"></script>
This code returns an object with all properties that have a different value and also values of both objects. Useful to logging the difference.
var allkeys = _.union(_.keys(obj1), _.keys(obj2));
var difference = _.reduce(allkeys, function (result, key) {
if ( !_.isEqual(obj1[key], obj2[key]) ) {
result[key] = {obj1: obj1[key], obj2: obj2[key]}
}
return result;
}, {});
Simple use _.isEqual method, it will work for all comparing...
Note: This method supports comparing arrays, array buffers,
booleans,
* date objects, error objects, maps, numbers, Object objects, regexes,
* sets, strings, symbols, and typed arrays. Object objects are compared
* by their own, not inherited, enumerable properties. Functions and DOM
* nodes are not supported.
So if you have below:
const firstName = {name: "Alireza"};
const otherName = {name: "Alireza"};
If you do: _.isEqual(firstName, otherName);,
it will return true
And if const fullName = {firstName: "Alireza", familyName: "Dezfoolian"};
If you do: _.isEqual(firstName, fullName);,
will return false
There have been many answers posted but for those curious to avoid writing any code to calculate difference between two objects having any type of structure there is actually a library to do this. Lodash isEqual only returns true or false it doesn't return any information about the changed properties. https://www.npmjs.com/package/deep-diff
its returns full detail of differences between two objects
import DeepDiff from 'deep-diff';
let a = {...} //some object
let b = {...} //some object
var differences = DeepDiff.diff(a, b);
Similar question has also been asked in this thread
Getting the difference between 2 JSON objects
I took a stab a Adam Boduch's code to output a deep diff - this is entirely untested but the pieces are there:
function diff (obj1, obj2, path) {
obj1 = obj1 || {};
obj2 = obj2 || {};
return _.reduce(obj1, function(result, value, key) {
var p = path ? path + '.' + key : key;
if (_.isObject(value)) {
var d = diff(value, obj2[key], p);
return d.length ? result.concat(d) : result;
}
return _.isEqual(value, obj2[key]) ? result : result.concat(p);
}, []);
}
diff({ foo: 'lol', bar: { baz: true }}, {}) // returns ["foo", "bar.baz"]
As it was asked, here's a recursive object comparison function. And a bit more. Assuming that primary use of such function is object inspection, I have something to say. Complete deep comparison is a bad idea when some differences are irrelevant. For example, blind deep comparison in TDD assertions makes tests unnecessary brittle. For that reason, I'd like to introduce a much more valuable partial diff. It is a recursive analogue of a previous contribution to this thread. It ignores keys not present in a
var bdiff = (a, b) =>
_.reduce(a, (res, val, key) =>
res.concat((_.isPlainObject(val) || _.isArray(val)) && b
? bdiff(val, b[key]).map(x => key + '.' + x)
: (!b || val != b[key] ? [key] : [])),
[]);
BDiff allows checking for expected values while tolerating other properties, which is exactly what you want for automatic inspection. This allows building all kinds of advanced assertions. For example:
var diff = bdiff(expected, actual);
// all expected properties match
console.assert(diff.length == 0, "Objects differ", diff, expected, actual);
// controlled inequality
console.assert(diff.length < 3, "Too many differences", diff, expected, actual);
Returning to the complete solution. Building a full traditional diff with bdiff is trivial:
function diff(a, b) {
var u = bdiff(a, b), v = bdiff(b, a);
return u.filter(x=>!v.includes(x)).map(x=>' < ' + x)
.concat(u.filter(x=>v.includes(x)).map(x=>' | ' + x))
.concat(v.filter(x=>!u.includes(x)).map(x=>' > ' + x));
};
Running above function on two complex objects will output something similar to this:
[
" < components.0.components.1.components.1.isNew",
" < components.0.cryptoKey",
" | components.0.components.2.components.2.components.2.FFT.min",
" | components.0.components.2.components.2.components.2.FFT.max",
" > components.0.components.1.components.1.merkleTree",
" > components.0.components.2.components.2.components.2.merkleTree",
" > components.0.components.3.FFTResult"
]
Finally, in order to have a glimpse into how the values differ, we may want to directly eval() the diff output. For that, we need an uglier version of bdiff that outputs syntactically correct paths:
// provides syntactically correct output
var bdiff = (a, b) =>
_.reduce(a, (res, val, key) =>
res.concat((_.isPlainObject(val) || _.isArray(val)) && b
? bdiff(val, b[key]).map(x =>
key + (key.trim ? '':']') + (x.search(/^\d/)? '.':'[') + x)
: (!b || val != b[key] ? [key + (key.trim ? '':']')] : [])),
[]);
// now we can eval output of the diff fuction that we left unchanged
diff(a, b).filter(x=>x[1] == '|').map(x=>[x].concat([a, b].map(y=>((z) =>eval('z.' + x.substr(3))).call(this, y)))));
That will output something similar to this:
[" | components[0].components[2].components[2].components[2].FFT.min", 0, 3]
[" | components[0].components[2].components[2].components[2].FFT.max", 100, 50]
MIT license ;)
Here is a simple Typescript with Lodash deep difference checker which will produce a new object with just the differences between an old object and a new object.
For example, if we had:
const oldData = {a: 1, b: 2};
const newData = {a: 1, b: 3};
the resulting object would be:
const result: {b: 3};
It is also compatible with multi-level deep objects, for arrays it may need some tweaking.
import * as _ from "lodash";
export const objectDeepDiff = (data: object | any, oldData: object | any) => {
const record: any = {};
Object.keys(data).forEach((key: string) => {
// Checks that isn't an object and isn't equal
if (!(typeof data[key] === "object" && _.isEqual(data[key], oldData[key]))) {
record[key] = data[key];
}
// If is an object, and the object isn't equal
if ((typeof data[key] === "object" && !_.isEqual(data[key], oldData[key]))) {
record[key] = objectDeepDiff(data[key], oldData[key]);
}
});
return record;
};
Deep compare using a template of (nested) properties to check
function objetcsDeepEqualByTemplate(objectA, objectB, comparisonTemplate) {
if (!objectA || !objectB) return false
let areDifferent = false
Object.keys(comparisonTemplate).some((key) => {
if (typeof comparisonTemplate[key] === 'object') {
areDifferent = !objetcsDeepEqualByTemplate(objectA[key], objectB[key], comparisonTemplate[key])
return areDifferent
} else if (comparisonTemplate[key] === true) {
areDifferent = objectA[key] !== objectB[key]
return areDifferent
} else {
return false
}
})
return !areDifferent
}
const objA = {
a: 1,
b: {
a: 21,
b: 22,
},
c: 3,
}
const objB = {
a: 1,
b: {
a: 21,
b: 25,
},
c: true,
}
// template tells which props to compare
const comparisonTemplateA = {
a: true,
b: {
a: true
}
}
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateA)
// returns true
const comparisonTemplateB = {
a: true,
c: true
}
// returns false
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateB)
This will work in the console. Array support could be added if needed
I need to know if they have difference in one of their nested properties
Other answers provide potentially satisfactory solutions to this problem, but it is sufficiently difficult and common that it looks like there's a very popular package to help solve this issue deep-object-diff.
To use this package you'd need to npm i deep-object-diff then:
const { diff } = require('deep-object-diff');
var a = {};
var b = {};
a.prop1 = 2;
a.prop2 = { prop3: 2 };
b.prop1 = 2;
b.prop2 = { prop3: 3 };
if (!_.isEqual(a, b)) {
const abDiff = diff(a, b);
console.log(abDiff);
/*
{
prop2: {
prop3: 3
}
}
*/
}
// or alternatively
const abDiff = diff(a, b);
if(!_.isEmpty(abDiff)) {
// if a diff exists then they aren't deeply equal
// perform needed actions with diff...
}
Here's a more detailed case with property deletions directly from their docs:
const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(diff(lhs, rhs)); // =>
/*
{
foo: {
bar: {
a: {
'1': undefined
},
c: {
'2': 'z'
},
d: 'Hello, world!',
e: undefined
}
},
buzz: 'fizz'
}
*/
For implementation details and other usage info, refer to that repo.
I know this doesn't directly answer the OP's question but I was led here by searching for how to remove lodash. So hopefully this helps someone else in a similar position as me.
Credit goes to #JohanPersson. I built off of that answer to implement comparing deeply nested values and getting the key reference to the diffs
getObjectDiff = (obj1, obj2) => {
const obj1Props = Object.keys(obj1);
const obj2Props = Object.keys(obj2);
const keysWithDiffValue = obj1Props.reduce((keysWithDiffValueAccumulator, key) => {
const propExistsOnObj2 = obj2.hasOwnProperty(key);
const hasNestedValue = obj1[key] instanceof Object && obj2[key] instanceof Object;
const keyValuePairBetweenBothObjectsIsEqual = obj1[key] === obj2[key];
if (!propExistsOnObj2) {
keysWithDiffValueAccumulator.push(key);
} else if (hasNestedValue) {
const keyIndex = keysWithDiffValueAccumulator.indexOf(key);
if (keyIndex >= 0) {
keysWithDiffValueAccumulator.splice(keyIndex, 1);
}
const nestedDiffs = getObjectDiff(obj1[key], obj2[key]);
for (let diff of nestedDiffs) {
keysWithDiffValueAccumulator.push(`${key}.${diff}`);
}
} else if (keyValuePairBetweenBothObjectsIsEqual) {
const equalValueKeyIndex = keysWithDiffValueAccumulator.indexOf(key);
keysWithDiffValueAccumulator.splice(equalValueKeyIndex, 1);
}
return keysWithDiffValueAccumulator;
}, obj2Props);
return keysWithDiffValue;
}
const obj1 = {a0: {a1: {a2: {a3: 'Im here'}}}};
const obj2 = {a0: {a1: {a2: {a3: 'Not here', b3: 'some'}}}};
console.log('final', getObjectDiff(obj1, obj2));
Without use of lodash/underscore, I have written this code and is working fine for me for a deep comparison of object1 with object2
function getObjectDiff(a, b) {
var diffObj = {};
if (Array.isArray(a)) {
a.forEach(function(elem, index) {
if (!Array.isArray(diffObj)) {
diffObj = [];
}
diffObj[index] = getObjectDiff(elem, (b || [])[index]);
});
} else if (a != null && typeof a == 'object') {
Object.keys(a).forEach(function(key) {
if (Array.isArray(a[key])) {
var arr = getObjectDiff(a[key], b[key]);
if (!Array.isArray(arr)) {
arr = [];
}
arr.forEach(function(elem, index) {
if (!Array.isArray(diffObj[key])) {
diffObj[key] = [];
}
diffObj[key][index] = elem;
});
} else if (typeof a[key] == 'object') {
diffObj[key] = getObjectDiff(a[key], b[key]);
} else if (a[key] != (b || {})[key]) {
diffObj[key] = a[key];
} else if (a[key] == (b || {})[key]) {
delete a[key];
}
});
}
Object.keys(diffObj).forEach(function(key) {
if (typeof diffObj[key] == 'object' && JSON.stringify(diffObj[key]) == '{}') {
delete diffObj[key];
}
});
return diffObj;
}
This is my solution to the problem
const _ = require('lodash');
var objects = [{ 'x': 1, 'y': 2, 'z':3, a:{b:1, c:2, d:{n:0}}, p:[1, 2, 3] }, { 'x': 2, 'y': 1, z:3, a:{b:2, c:2,d:{n:1}}, p:[1,3], m:3 }];
const diffFn=(a,b, path='')=>_.reduce(a, function(result, value, key) {
if(_.isObjectLike(value)){
if(_.isEqual(value, b[key])){
return result;
}else{
return result.concat(diffFn(value, b[key], path?(`${path}.${key}`):key))
}
}else{
return _.isEqual(value, b[key]) ?
result : result.concat(path?(`${path}.${key}`):key);
}
}, []);
const diffKeys1=diffFn(objects[0], objects[1])
const diffKeys2=diffFn(objects[1], objects[0])
const diffKeys=_.union(diffKeys1, diffKeys2)
const res={};
_.forEach(diffKeys, (key)=>_.assign(res, {[key]:{ old: _.get(objects[0], key), new:_.get(objects[1], key)} }))
res
/*
Returns
{
x: { old: 1, new: 2 },
y: { old: 2, new: 1 },
'a.b': { old: 1, new: 2 },
'a.d.n': { old: 0, new: 1 },
'p.1': { old: 2, new: 3 },
'p.2': { old: 3, new: undefined },
m: { old: undefined, new: 3 }
}
*/
Completing the answer from Adam Boduch, this one takes into differences in properties
const differenceOfKeys = (...objects) =>
_.difference(...objects.map(obj => Object.keys(obj)));
const differenceObj = (a, b) =>
_.reduce(a, (result, value, key) => (
_.isEqual(value, b[key]) ? result : [...result, key]
), differenceOfKeys(b, a));
If you need only key comparison:
_.reduce(a, function(result, value, key) {
return b[key] === undefined ? key : []
}, []);
We had this requirement on getting the delta between two json updates for tracking database updates. Maybe someone else can find this helpful.
https://gist.github.com/jp6rt/7fcb6907e159d7851c8d59840b669e3d
const {
isObject,
isEqual,
transform,
has,
merge,
} = require('lodash');
const assert = require('assert');
/**
* Perform a symmetric comparison on JSON object.
* #param {*} baseObj - The base object to be used for comparison against the withObj.
* #param {*} withObj - The withObject parameter is used as the comparison on the base object.
* #param {*} invert - Because this is a symmetric comparison. Some values in the with object
* that doesn't exist on the base will be lost in translation.
* You can execute again the function again with the parameters interchanged.
* However you will lose the reference if the value is from the base or with
* object if you intended to do an assymetric comparison.
* Setting this to true will do make sure the reference is not lost.
* #returns - The returned object will label the result of the comparison with the
* value from base and with object.
*/
const diffSym = (baseObj, withObj, invert = false) => transform(baseObj, (result, value, key) => {
if (isEqual(value, withObj[key])
&& has(withObj, key)) {
return;
}
if (isObject(value)
&& isObject(withObj[key])
&& !Array.isArray(value)) {
result[key] = diffSym(value, withObj[key], invert);
return;
}
if (!invert) {
result[key] = {
base: value,
with: withObj[key],
};
return;
}
if (invert) {
result[key] = {
base: withObj[key],
with: value,
};
}
});
/**
* Perform a assymmetric comparison on JSON object.
* #param {*} baseObj - The base object to be used for comparison against the withObj.
* #param {*} withObj - The withObject parameter is used as the comparison on the base object.
* #returns - The returned object will label the values with
* reference to the base and with object.
*/
const diffJSON = (baseObj, withObj) => {
// Deep clone the objects so we don't update the reference objects.
const baseObjClone = JSON.parse(JSON.stringify(baseObj));
const withObjClone = JSON.parse(JSON.stringify(withObj));
const beforeDelta = diffSym(baseObjClone, withObjClone);
const afterDelta = diffSym(withObjClone, baseObjClone, true);
return merge(afterDelta, beforeDelta);
};
// By Example:
const beforeDataObj = {
a: 1,
c: { d: 2, f: 3 },
g: 4,
h: 5,
};
const afterDataObj = {
a: 2,
b: 3,
c: { d: 1, e: 1 },
h: 5,
};
const delta = diffJSON(beforeDataObj, afterDataObj);
// Assert expected result.
assert(isEqual(delta, {
a: { base: 1, with: 2 },
b: { base: undefined, with: 3 },
c: {
d: { base: 2, with: 1 },
e: { base: undefined, with: 1 },
f: { base: 3, with: undefined },
},
g: { base: 4, with: undefined },
}));
This solution returns an object with the modified attributes.
_.reduce(a, (r, v, k) => { return _.merge(r, _.isEqual(v, b[k]) ? {} : { [k]: v }); }, {});
var isEqual = function(f,s) {
if (f === s) return true;
if (Array.isArray(f)&&Array.isArray(s)) {
return isEqual(f.sort(), s.sort());
}
if (_.isObject(f)) {
return isEqual(f, s);
}
return _.isEqual(f, s);
};
this was based on #JLavoie, using lodash
let differences = function (newObj, oldObj) {
return _.reduce(newObj, function (result, value, key) {
if (!_.isEqual(value, oldObj[key])) {
if (_.isArray(value)) {
result[key] = []
_.forEach(value, function (innerObjFrom1, index) {
if (_.isNil(oldObj[key][index])) {
result[key].push(innerObjFrom1)
} else {
let changes = differences(innerObjFrom1, oldObj[key][index])
if (!_.isEmpty(changes)) {
result[key].push(changes)
}
}
})
} else if (_.isObject(value)) {
result[key] = differences(value, oldObj[key])
} else {
result[key] = value
}
}
return result
}, {})
}
https://jsfiddle.net/EmilianoBarboza/0g0sn3b9/8/
To build upon Sridhar Gudimela's answer, here it is updated in a way that uses TypeScript:
/// U T I L S
interface LooseObjectInterface {
[key: string]: any;
};
type inputOptions = LooseObjectInterface | any[];
/// E X P O R T
export const objectCompare = (objectA: inputOptions, objectB: inputOptions): LooseObjectInterface => {
let diffObj: LooseObjectInterface = {};
switch(true) {
case (Array.isArray(objectA)):
objectA.forEach((elem: any, index: number) => {
if (!Array.isArray(diffObj))
diffObj = [];
diffObj[index] = objectCompare(elem, (objectB || [])[index]);
});
break;
case (objectA !== null && typeof objectA === "object"):
Object.keys(objectA).forEach((key: any) => {
if (Array.isArray(objectA[key])) {
let arr = objectCompare(objectA[key], objectB[key]);
if (!Array.isArray(arr))
arr = [];
arr.forEach((elem: any, index: number) => {
if (!Array.isArray(diffObj[key]))
diffObj[key] = [];
diffObj[key][index] = elem;
});
} else if (typeof objectA[key] === "object")
diffObj[key] = objectCompare(objectA[key], objectB[key]);
else if (objectA[key] !== (objectB || {})[key])
diffObj[key] = objectA[key];
else if (objectA[key] === (objectB || {})[key])
delete objectA[key];
});
break;
default:
break;
}
Object.keys(diffObj).forEach((key: any) => {
if (typeof diffObj[key] === "object" && JSON.stringify(diffObj[key]) === "{}")
delete diffObj[key];
});
return diffObj;
};
EDIT: My original answer used Flow, hence the downvotes (I assume, or maybe because my answer didn't use Lodash...however, having an answer to a similar problem can't hurt).

How to get the path from javascript object from key and value

I have a javascript object width depth.
I need to know the exact path from this key within the object ex: "obj1.obj2.data1"
I already know the key is data1, the value is 123.
My javascript object look like this
{
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto"
}
}
How could I achieve that ?
here is a jsfiddle : http://jsfiddle.net/3hvav8xf/8/
I am trying to implement getPath.
I think recursive function can help to you (Updated version, to check value)
function path(c, name, v, currentPath, t){
var currentPath = currentPath || "root";
for(var i in c){
if(i == name && c[i] == v){
t = currentPath;
}
else if(typeof c[i] == "object"){
return path(c[i], name, v, currentPath + "." + i);
}
}
return t + "." + name;
};
console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "s", 1));
The following finds the path in any level of nested objects. Also with arrays.
It returns all the paths found, which is something you want if you have keys with the same name.
I like this approach because it works with lodash methods get and set out-of-the-box.
function findPathsToKey(options) {
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (obj.hasOwnProperty(key)) {
results.push(`${oldPath}${key}`);
return;
}
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (obj.hasOwnProperty(k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (obj[k] !== null && typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return results;
}
findPathsToKey({ obj: objWithDuplicates, key: "d" })
// ["parentKey.arr[0].c.d", "parentKey.arr[1].c.d", "parentKey.arr[2].c.d"]
Try it here - https://jsfiddle.net/spuhb8v7/1/
If you want the result to be a single key (first encountered), you can change the results to be a string and if defined, then return the function with it.
I ended up with the following function, that works with nested objects/arrays :
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}['${name}']`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}['${key}']`)
}
if (matchingPath) break
}
return matchingPath
}
const treeData = [{
id: 1,
children: [{
id: 2
}]
}, {
id: 3,
children: [{
id: 4,
children: [{
id: 5
}]
}]
}]
console.log(findPath (treeData, 'id', 5))
Here you go!
function getPath(obj, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
console.log(key);
var t = path;
var v = obj[key];
if(!path) {
path = key;
}
else {
path = path + '.' + key;
}
if(v === value) {
return path;
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, valueYouWantToFindPath);
Rerutns path if found, else returns undefined.
I have only tested it with objects & comparison is very strict(ie: used ===).
Update:
Updated version that takes key as an argument.
function getPath(obj, key, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var k in obj) {
if(obj.hasOwnProperty(k)) {
console.log(k);
var t = path;
var v = obj[k];
if(!path) {
path = k;
}
else {
path = path + '.' + k;
}
if(v === value) {
if(key === k) {
return path;
}
else {
path = t;
}
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, key, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, key, valueYouWantToFindPath);
JSON Object can be handled in JavaScript as associative array.
So You can cycle through and store indexes of "parents" in some variables.
Assume the whole object to be stored in variable called obj.
for( var p1 in obj )
{
for( var p2 in obj[ p1 ] )
{
for( var p3 in obj[ p1 ][ p2 ] )
{
// obj[ p1 ][ p2 ][ p3 ] is current node
// so for Your example it is obj.obj1.obj2.data1
}
}
}
Hope answer was helpful.
I would do this job as follows;
Object.prototype.paths = function(root = [], result = {}) {
var ok = Object.keys(this);
return ok.reduce((res,key) => { var path = root.concat(key);
typeof this[key] === "object" &&
this[key] !== null ? this[key].paths(path,res)
: res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
: res[this[key]] = [path];
return res;
},result);
};
var myObj = {
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto",
cougars: "Jodi",
category: "milf"
}
},
value = "milf",
milfPath = myObj.paths()[value]; // the value can be set dynamically and if exists it's path will be listed.
console.log(milfPath);
A few words of warning: We should be cautious when playing with the Object prototype. Our modification should have the descriptor enumerable = false or it will list in the for in loops and for instance jQuery will not work. (this is how silly jQuery is, since apparently they are not making a hasOwnProperty check in their for in loops) Some good reads are here and here So we have to add this Object method with Object.defineProperty() to make it enumerable = false;. But for the sake of simplicity and to stay in the scope of the question i haven't included that part in the code.
Here is a pretty short, and relatively easy to understand function I wrote for retrieving the JSON Path for every property/field on an Object (no matter how deeply nested, or not).
The getPaths(object) function just takes the Object you'd like the JSON Paths for and returns an array of paths. OR, if you would like the initial object to be denoted with a symbol that is different from the standard JSON Path symbol, $, you can call getPaths(object, path), and each JSON Path will begin with the specified path.
For Example: getPaths({prop: "string"}, 'obj'); would return the following JSON Path: obj.prop, rather than $.prop.
See below for a more detailed, in depth example of what getPaths returns, and how it is used.
object = {
"firstName": "John",
"lastName": "doe",
"age": 26,
"fakeData": true,
"address": {
"streetAddress": "fake street",
"city": "fake city",
"postalCode": "12345"
},
"phoneNumbers": [{
"type": "iPhone",
"number": "0123-4567-8888"
}, {
"type": "home",
"number": "0123-4567-8910"
}]
};
function getPaths(object, path = "$") {
return Object.entries(object).flatMap(function(o, i) {
if (typeof o[1] === "object" && !o[1].length) {
return `${getPaths(o[1], path + '.' + o[0])}`.split(',');
} else if (typeof o[1] === "object" && o[1].length) {
return Object.entries(o[1]).flatMap((no, i) => getPaths(no[1], `${path}.${o[0]}[${i}]`));
} else {
return `${path}.${o[0]}`;
}
});
}
console.log(`%o`, getPaths(object));
I really liked Roland Jegorov's answer, but I had a very complex object that I needed to search through and that answer could not account for it.
If you were in a situation like mine you may want to first make sure you have no circular references (or else you'll run into an infinite search). There are a few ways to do this, but I was having to stringify my object to copy it into other windows, so I ended up using this circular replacer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
(Update here - I made a small change to the getCircularReplacer function from MDN so it no longer leaves out function references since that is what I was looking for!)
(Update 3 - I also wanted to check on methods of any instances of classes, but I was returning just 'function' too early, so I have adjusted it to include instance methods. I think it finally works as I intended!)
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "function") {
if (value?.prototype) {
if (seen.has(value.prototype)) {
return;
}
seen.add(value.prototype)
return value.prototype
}
return "function";
}
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const nonCyclicObject = JSON.parse(JSON.stringify(myComplexObject, getCircularReplacer()));
Then I used this modified version of Roland's answer:
(Update 2: I had to make sure not to return after the key was found as it would always simply return after only calling the function once if the first level of the object had that key)
function findPathsToKey(options) {
let count = 0;
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
count += 1;
if (obj === null) return;
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (Object.hasOwnProperty.call(obj, key)) {
results.push(`${oldPath}${key}`);
}
if (typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (Object.hasOwnProperty.call(obj, k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return { count, results };
};
The count was just to troubleshoot a little bit and make sure it was actually running through the amount of keys I thought it was. Hope this helps any others looking for a solution!
⚠️ This code doesn't answer the question but does related: transforms nested object to query object with dot.divided.path as keys and non-object values; compatible with URlSearchParams & qs. Maybe will be useful for someone.
const isPlainObject = (v) => {
if (Object.prototype.toString.call(v) !== '[object Object]') return false;
const prototype = Object.getPrototypeOf(v);
return prototype === null || prototype === Object.prototype;
};
const objectToQueryObject = (obj, path) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
const newPath = path ? `${path}.${key}` : key;
if (isPlainObject(value)) {
return {
...acc,
...objectToQueryObject(value, newPath)
};
}
acc[newPath] = value;
return acc;
}, {})
};
const queryObjectRaw = {
value: {
field: {
array: {
'[*]': {
field2: {
eq: 'foo',
ne: 'bar',
}
}
},
someOtherProp: { in: [1, 2, 3],
ne: 'baz',
}
},
someOtherField: {
gt: 123
},
},
otherValue: {
eq: 2
},
};
const result = objectToQueryObject(queryObjectRaw);
console.log('result', result);
const queryString = new URLSearchParams(result).toString();
console.log('queryString', queryString);
If you know only the value and not the key, and want to find all paths with this value use this.
It will find all property with that value, and print the complete path for every founded value.
const createArrayOfKeys = (obj, value) => {
const result = []
function iter(o) {
Object.keys(o).forEach(function(k) {
if (o[k] !== null && typeof o[k] === 'object') {
iter(o[k])
return
}
if (o[k]=== value) {
result.push(k)
return
}
})
}
iter(obj)
return result
}
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}/${name}/${val}`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}/${key}`)
}
if (matchingPath) break
}
return matchingPath
}
const searchMultiplePaths = (obj, value) => {
const keys = createArrayOfKeys(obj, value)
console.log(keys);
keys.forEach(key => {
console.log(findPath(obj, key, value))
})
}
var data = { ffs: false, customer: { customer_id: 1544248, z_cx_id: '123456' }, selected_items: { '3600196': [{ id: 4122652, name: 'Essential Large (up to 8\'x10\')', selected: true }] }, service_partner: { id: 3486, name: 'Some String', street: '1234 King St.', hop: '123456' }, subject: 'Project-2810191 - Orange Juice Stain (Rug)', description: 'Product Type: \n\nIssue: (copy/paste service request details here)\n\nAction Required:', yes: '123456' };
searchMultiplePaths(data, '123456')
I know the post is old but the answers don't really satisfy me.
A simple solution is to add the object path to each object in the structure. Then you can easily read the path when you need it.
let myObject = {
name: 'abc',
arrayWithObject: [
{
name: "def"
},
{
name: "ghi",
obj: {
name: "jkl"
}
}
],
array: [15, 'mno'],
arrayArrayObject: [
[
{
name: '...'
}
]
]
}
function addPath(obj, path = [], objectPathKey = '_path') {
if (Array.isArray(obj)) {
obj.map((item, idx) => addPath(item, [...path, idx]))
} else if (typeof obj === "object") {
obj[objectPathKey] = path;
for (const key in obj) {
obj[key] = addPath(obj[key], [...path, key])
}
}
return obj
}
myObject = addPath(myObject);
let changeMe = _.cloneDeep(myObject.arrayWithObject[0])
changeMe.newProp = "NEW"
changeMe.newNested = {name: "new", deeper: {name: "asdasda"}}
changeMe = addPath(changeMe, changeMe._path)
_.set(myObject, changeMe._path, changeMe);
When your updates are done sanitize your object and remove your _path property.
Advantages of this solution:
You do the work once
you keep your code simple
no need for own property checks
no cognitive overload
I can highly suggest you to use lodash for this problem.
In their documentation this should help you out
// using "_.where" callback shorthand
_.find(characters, { 'age': 1 });
// → { 'name': 'pebbles', 'age': 1, 'blocked': false }

Multiple key names, same pair value

I'm trying to setup an object literal in a JavaScript script that has a key with multiple names. referring to the same object value i.e. something like these that I have already tried:
var holidays: {
"thanksgiving day", "thanksgiving", "t-day": {
someValue : "foo"
}
}
var holidays: {
["thanksgiving day", "thanksgiving", "t-day"]: {
someValue : "foo"
}
}
Is there a way I can accomplish this?
Another approach is to do some postprocessing
function expand(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; ++i) {
var key = keys[i],
subkeys = key.split(/,\s?/),
target = obj[key];
delete obj[key];
subkeys.forEach(function(key) { obj[key] = target; })
}
return obj;
}
var holidays = expand({
"thanksgiving day, thanksgiving, t-day": {
someValue : "foo"
}
});
JSON does not offer such a feature, nor do Javascript object literals.
You might be able to make do with something like this:
holidays = {
thanksgiving: {foo: 'foo'},
groundhogDay: {foo: 'bar'},
aliases: {
'thanksgiving day': 'thanksgiving',
't-day': 'thanksgiving',
'Bill Murrays nightmare': 'groundhogDay'
}
}
and then you can check
holidays[name] || holidays[holidays.aliases[name]]
for your data.
It's not a wonderful solution. But it wouldn't be too difficult to write a little function that created this sort of object out of a representation like:
[
{
names: ['thanksgiving', 'thanksgiving day', 't-day'],
obj: {foo: 'foo'}
},
{
names: ['groundhogDay', 'Bill Murrays nightmare'],
obj: {foo: 'bar'}
},
]
if that would be easier to maintain.
Another solution, if you can afford RegExp execution, and ES6 Proxy:
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, {
get: function(target, property, receiver) {
for (let k in target)
if (new RegExp(k).test(property))
return target[k]
return null
}
})
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
See MDN Proxy
2021 EDIT:
Another (cleaner?) solution using reduce & defineProperty :
const myDict = [
// list of pairs [value, keys],
// note that a key should appear only once
[-1, ['start', 'left', 'top']],
[0, ['center', 'middle']],
[1, ['end', 'right', 'bottom']],
].reduce((obj, [value, keys]) => {
for (const key of keys) {
Object.defineProperty(obj, key, { value })
}
return obj
}, {})
I guess you could do something like this:
var holidays = {
'thanksgiving day': {
foo: 'foo'
}
};
holidays.thanksgiving = holidays['t-day'] = holidays['thanksgiving day'];
If you see yourself doing this often or you have more values consider this pattern:
'thanksgiving, t-day, thanks, thank, thank u'.split(',').forEach(function(key) {
holidays[key] = holidays['thanksgiving day'];
});
A better approach would be to process your data beforehand instead of adding duplicates.
That should work as expected:
function getItem(_key) {
items = [{
item: 'a',
keys: ['xyz','foo']
},{
item: 'b',
keys: ['xwt','bar']
}];
_filtered = items.filter(function(item) {
return item.keys.indexOf(_key) != -1
}).map(function(item) {
return item.item;
});
return !!_filtered.length ? _filtered[0] : false;
}
With ES6 you could do it like this, but it's not ideal:
const holidays = {
"single": {
singleValue: "foo",
},
...([
"thanksgiving day", "thanksgiving", "t-day",
].reduce((a, v) => ({...a, [v]: {
someValue: "foo",
}}), {})),
"other": {
otherValue: "foo",
},
};
I still think the cleanest solution is probably:
let holidays = {
"t-day": {
someValue: "foo",
},
};
holidays["thanksgiving"] = holidays["t-day"];
holidays["thanksgiving day"] = holidays["t-day"];
Now this may be overkill for you, but here's a generic function that will create an object with "multiple keys." What it actually does is have one real property with the actual value, and then defines getters and setters to forward operations from the virtual keys to the actual property.
function multiKey(keyGroups) {
let obj = {};
let props = {};
for (let keyGroup of keyGroups) {
let masterKey = keyGroup[0];
let prop = {
configurable: true,
enumerable: false,
get() {
return obj[masterKey];
},
set(value) {
obj[masterKey] = value;
}
};
obj[masterKey] = undefined;
for (let i = 1; i < keyGroup.length; ++i) {
if (keyGroup.hasOwnProperty(i)) {
props[keyGroup[i]] = prop;
}
}
}
return Object.defineProperties(obj, props);
}
This is less sketchy than you would expect, has basically no performance penalty once the object is created, and behaves nicely with enumeration (for...in loops) and membership testing (in operator). Here's some example usage:
let test = multiKey([
['north', 'up'],
['south', 'down'],
['east', 'left'],
['west', 'right']
]);
test.north = 42;
test.down = 123;
test.up; // returns 42
test.south; // returns 123
let count = 0;
for (let key in test) {
count += 1;
}
count === 4; // true; only unique (un-linked) properties are looped over
Taken from my Gist, which you may fork.
Same reponse (ES6 Proxy, RegExp), but in a shorter way (and significantly less legible)
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, { get: (t, p) => Object.keys(t).reduce((r, v) => r !== undefined ? r : (new RegExp(v).test(p) ? t[v] : undefined), undefined) })
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
//create some objects(!) you want to have aliases for..like tags
var {learn,image,programming} =
["learn", "image", "programming"].map(tag=>({toString:()=>tag }));
//create arbitrary many aliases using a Map
var alias = new Map();
alias.set("photo", image);
alias.set("pic", image);
alias.set("learning", learn);
alias.set("coding", programming);
//best put the original tagNames in here too..
//pretty easy huh?
// returns the image object
alias.get("pic");
// ;)
here is a way you can initialize an object with several keys sharing the same value
var holidays = {
...["thanksgiving day", "thanksgiving", "t-day"].reduce((acc, key) => ({ ...acc, [key]: 'foo' }), {})
}
although I would personally think it was more clear if it was written out
Object.fromEntries produces some fairly readable and concise code:
var holidays = Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"]));
The spread syntax can be used to include this alongside other key/value pairs:
var holidaysAndMore = {
"A": "a",
...Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"])),
"B": "b"
};

Categories

Resources