Code Kata: Using Array Reduce() To Navigate An Object Graph In JavaScript
Yesterday, Josh Siok and I were brainstorming on some ways to locate a value in an object graph when given a set of object-properties as a single string. Now, I know (or rather assume) that there are many libraries out there that provide "object query" functionality. But, I figured this would make a fun JavaScript code kata; so, I wanted to share the approach that we came up with using just a touch of native JavaScript functionality.
When given a string like, "A.B.C.D.E", the goal of the problem was to find the value of "E" as located in some root object "R". Now, if we think about what each of the letters in the string are, they're really just properties; each letter is a property on the preceding letter. So, "E" is a property of "D", which is a property of "C", which is a property of "B", and so on until we reach the root of the object graph.
If we can then split the string of properties on the "." character, what we end up with is a set of property names: [ "A", "B", "C", "D", "E" ]. Now, given the root of an object graph, this set of properties really represents a series of navigational steps down into the structure of the object graph: first, access property "A", then access property "B", and so on, until we locate property "E".
Now, in order to return the value referenced by property "E", we can "reduce" the set of navigation steps down into a single value. Out of the box, JavaScript arrays provide a .reduce() method. So, all we have to do is reduce the set of navigation tokens across the object graph:
// I am just an arbitrarily non-trivial object to query.
var config = {
tasks: {
interval: {
//...
},
daily: {
backupFilesToS3: {
dayOfWeek: "Saturday",
hour: 22
}
}
}
};
// Now, imagine that we are given an object query that is a dot-delimited path of
// object navigation instructions. And, we want to find the value that resides at
// the end of this path.
var objectQuery = "tasks.daily.backupFilesToS3.dayOfWeek";
var value = objectQuery
// To find the value, we can split the query / object-path on the dot-operator and
// treat each token as a single object-property navigation instruction.
.split( "." )
// With the resultant array of navigation tokens, we can then reduce the set of
// steps down to the target value by using the reduction's "previousValue" as our
// place-holder in the object graph. Essentially, each step of the reduce() operation
// takes us one level deeper into the object graph, returning the result of a single
// step in the object path.
.reduce(
function iterator( previousValue, navigationToken ) {
// If the previous navigation was successful, return the result of the NEXT
// step in set of navigation operations. Otherwise, return the failed result
// of the previous navigation.
return( previousValue && previousValue[ navigationToken ] );
},
config // ... starting at the root of the object graph.
)
;
console.log( "Query Result:", value );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// And, of course, there's the old-school way to just evaluate the code :D Which is
// likely to throw red-flags by your team's linting system and code-review process.
console.log( "EVAL Query Result:", eval( "config." + objectQuery ) );
As you can see, when given a non-trivial object and an object query / property path, we split the query and reduce it over the object graph. Each step of the reduction operation brings us one-level deeper into the object graph. And, when we run the above code through Node.js, we get the following terminal output:
Query Result: Saturday
EVAL Query Result: Saturday
As you can see, we located the value at the end of the object query.
Again, I know there are libraries out there that provide functionality like this. But, as a lover of JavaScript, I think it's always important to remember how much can be squeezed right out of the tools that we already have at our fingertips. Not only does writing code like this cut down on dependencies, it also helps you think about the features of the language itself. And, the more deeply you can think about the language, the more powerful your object-solving machinery can become.
Want to use code from this post? Check out the license.
Reader Comments
That's pretty cool, didn't see it being that easy.
Cool Exercise as well.
That been said, working without Lodash or any other util library is crazy these days :-).
This is why I love this blog... I never would have thought to reach for reduce, but that's brilliant!
I would have for sure reached for a for-loop like this library `get-object-path` did...
https://github.com/skratchdot/object-path-get/blob/master/index.js
Or even a while loop as `get-value` did...
https://github.com/jonschlinkert/get-value/blob/master/index.js
(I was curious how other libraries did it, so I looked for a couple)
I wasn't able to find anything in lodash that did something similar as @Alex Ilyaev suggested, but I'd love to peak under the hood if they have a similar function.
Thanks again, Ben!
@Alex,
Yo, 100%. Lodash makes so many things easier. Sometimes, however, I think people go a little crazy with the function-composition and make it next-to-impossible to see what the code is actually doing (in terms of intent). But, yeah, Lodash is standard for me these days.
@Chris,
Thank you sir, you are too kind :D And, I think the for / while loop approaches are fine as well. To be honest, I enjoy feeling a little "fancy" using a .reduce(). It seems like one of those tools that doesn't get used as much in JavaScript (though I think other languages might use it more heavily).
The mental model required for .reduce() is a little harder because the flow of data is not as explicit as it is in a loop. But, I think, once you have it in your head, it becomes a bit more easy to reason about. That said, you can't short-circuit / break-out of the reduction like you can with for/while. So... trade-offs :D