Cyclic reference danger
Recursive property searches are dangerous because many objects used in javascript at some point have a reference to themselves. Eg the global object console.log(this.window === this); // displays true
You need to check or be sure that there are no cyclic references. A common method is to use the JSON.stringify
function that will throw if an object is cyclic. This is a very inelegant method and will prevent you from finding the information you are after as you will not search if this fails.
The best way (unfortunately) is to store each object you iterate and then check every other object against that array of objects, ignoring all object you have already iterated.
More than one result
The second problem you have is that the search will find only one path, while there could be many paths that satisfy the requirement, the first result may not be the one you are after. If you use a callback to determine the matching result you can be a lot more adaptable. The callback can accept not only the key name, but the current path and the current object the key belongs to.
The function should return all paths that satisfy the condition set by the callback function.
Modernise
ES6 has been available in the majority of browsers for some time. It is best to use ES6 as it is a major improvement on ES5, not using it is a major No No if you plan on an IT career. If you are concerned about legacy browsers (especially that annoying thorn called IE11) you should use a transpiler (for example Babel) to ensure backward compatibility.
A rewrite
I present a modification that is safer and more versity and in two forms. One as a standalone function and the other as a prototype of Object.
function findPropPaths(obj, predicate) { // The function
const discoveredObjects = []; // For checking for cyclic object
const path = []; // The current path being searched
const results = []; // The array of paths that satify the predicate === true
if (!obj && (typeof obj !== "object" || Array.isArray(obj))) {
throw new TypeError("First argument of finPropPath is not the correct type Object");
}
if (typeof predicate !== "function") {
throw new TypeError("Predicate is not a function");
}
(function find(obj) {
for (const key of Object.keys(obj)) { // use only enumrable own properties.
if (predicate(key, path, obj) === true) { // Found a path
path.push(key); // push the key
results.push(path.join(".")); // Add the found path to results
path.pop(); // remove the key.
}
const o = obj[key]; // The next object to be searched
if (o && typeof o === "object" && ! Array.isArray(o)) { // check for null then type object
if (! discoveredObjects.find(obj => obj === o)) { // check for cyclic link
path.push(key);
discoveredObjects.push(o);
find(o);
path.pop();
}
}
}
} (obj));
return results;
}
Usage
Usage is simple
const arrayOfPaths = findPropPaths(myObj, key => key === "x");
The predicate
takes the form of
function(key, path, obj) {
Where key
is the property name being tested, path
is an array of property names up to the current object. obj
is the current object the property key
belongs to.
The function returns true
if the test passes, and any other value if the test fails. Only paths that the predicate returns true for are returned in the array of paths found.
Example code
The function is very versatile and below it will find all paths to properties that are named "x" and the second search will find all paths named x and contain a string
function findPropPaths(obj, predicate) { // The function
const discoveredObjects = []; // For checking for cyclic object
const path = []; // The current path being searched
const results = []; // The array of paths that satify the predicate === true
if (!obj && (typeof obj !== "object" || Array.isArray(obj))) {
throw new TypeError("First argument of finPropPath is not the correct type Object");
}
if (typeof predicate !== "function") {
throw new TypeError("Predicate is not a function");
}
(function find(obj) {
for (const key of Object.keys(obj)) { // use only enumrable own properties.
if (predicate(key, path, obj) === true) { // Found a path
path.push(key); // push the key
results.push(path.join(".")); // Add the found path to results
path.pop(); // remove the key.
}
const o = obj[key]; // The next object to be searched
if (o && typeof o === "object" && ! Array.isArray(o)) { // check for null then type object
if (! discoveredObjects.find(obj => obj === o)) { // check for cyclic link
path.push(key);
discoveredObjects.push(o);
find(o);
path.pop();
}
}
}
} (obj));
return results;
}
// create test object. Sorry but for brevity I have made it all one line
const myObj = { a:0, g : [0, 1, 2, 3, "x", {x : 0}], d : null, z: { b: { c: { x: { a : {x : 0, } }, y: 2 }, d: { x: "0", y: 2 } } } };
// add self reference to test cyclic protection
myObj.f = myObj;
// find all paths to property name "x"
const arrayOfPaths = findPropPaths(myObj,key => key === "x");
console.log("All unique paths to 'x'");
arrayOfPaths.forEach(path => console.log(path));
// find all paths to property name "x" that has a string
const res1 = findPropPaths(myObj,(key, path, obj) => key === "x" && typeof obj[key] === "string");
console.log("All unique paths to property 'x' holding a string");
res1.forEach(path => console.log(path));
As Object.prototype
As you are after a fundamental function you can chose to add it to the Object prototype thus making it available to all object.
Many here will throw their hands up and say "NO WAY", I expect this answer to get more downvotes than up simply because of this. But it is a valid way of using the language, and is better understood than hidden behind gasps of "No" and "Run for the hills the end is now".
If you do it the following way you make it easier to use in your code. But there are warnings.
Warning when adding to the prototype of basic objects
If you do however you must take some precautions. Do not use the function if it already exists as the source may be unknown thus unsafe. Ensure that the function can not be overwritten as the object prototype is global and could be hijacked. Ensure that it works, the last thing you want to do is add a prototype to a basic object that requires some form of vetting before use. It should be bulletproof or it should not end up on the object's prototype.
The object prototype form
if (Object.prototype.findPropPaths !== undefined) {
throw new ReferenceError("The object prototype 'findPropPaths` already exists and may be of unknown origin and thus unsafe to use.");
} else {
Object.defineProperty(Object.prototype, 'findPropPaths', {
writable : false, // During development you should have this as true if you are working
// in an environment that does not reset, or this will throw an error
enumerable : false, // You don't want this to be seen
configurable : false, // You don't want this to be changed.
value : function(predicate) { // The function
const discoveredObjects = []; // For checking for cyclic object
const path = []; // The current path being searched
const results = []; // The array of paths that satify the predicate === true
if (typeof predicate !== "function") {
throw new TypeError("Predicate is not a function");
}
(function find(obj) {
for (const key of Object.keys(obj)) { // use only enumerable own properties.
if (predicate(key, path, obj) === true) { // Found a path
path.push(key); // push the key
results.push(path.join(".")); // Add the found path to results
path.pop(); // remove the key.
}
const o = obj[key]; // The next object to be searched
if (o && typeof o === "object" && ! Array.isArray(o)) { // check for null then type object
if (! discoveredObjects.find(obj => obj === o)) { // check for cyclic link
path.push(key);
discoveredObjects.push(o);
find(o);
path.pop();
}
}
}
} (this));
return results;
}
});
}
findPropPath(myObj,"c")
it skipsb
and returns.a.c
How so..? Besides if you have objects with same property names you will probably get only the path for the first one. Imagine arrays they all have same index values as per keys (properties) \$\endgroup\$eval
is more terse, safer and more bullet-proof route. As long as the string contains just valid characters for property names and dots,eval
is not so evil. \$\endgroup\$