In this blog post, I’d like to talk about a fun problem I had at work.
Without getting too nitty-gritty, here’s the gist of a problem.
How do you write a function that receives a path, such as the.path.to.a.key
, and correctly uses the dot notation to traverse an object and return the value of the key?
const path1 = 'foo.bar' // Should return { baz: 42 }
const path2 = 'foo.bar.baz' // Should return 42
const path3 = 'davis' // Should return 'ford'
const path4 = 'foo.going.even.deeper.now' // Should return 0
const path5 = 'foo.bar.invalid_path' // Should return undefined
const obj = {
davis: 'ford',
foo: {
bar: {
baz: 42,
},
going: {
even: {
deeper: {
now: 0,
},
},
},
},
}
My solution simply split
and then reduce
d the keys and returns the value, if I found it.
It’s a lot of lines of code, and not super readable, but it does work, and it is nicely documented (in my opinion).
/**
* Solution 1: The one I wrote
*/
const getKeyValue = (obj, key) => {
// Best case scenario: The key is a top level key
if (obj.hasOwnProperty(key)) return obj[key]
// If there's no keys in the object, don't bother
if (Object.keys(obj).length === 0) return undefined
// Split the name into keys
const keys = key.split('.')
// Reduce the keys
const { value } = keys.reduce(
(accum, k, i) => {
// Don't process any more if we've already set break to true
if (accum.break) return accum
// If the last key exists in the object, we've found our answer!
if (i === keys.length - 1 && accum.obj.hasOwnProperty(k)) {
accum.value = accum.obj[k]
return accum
}
// Does the obj have this key? If so, keep drilling down
if (accum.obj.hasOwnProperty(k)) {
accum.obj = accum.obj[k]
return accum
}
// If we can't find the key, we've been given an invalid path
// So set `break` to true to stop processing
accum.break = true
return accum
},
// Initialize our `reduce` loop with this object
{
break: false,
value: undefined,
obj,
}
)
return value
}
For reference, here is lodash’s implementation.
import castPath from './castPath.js'
import toKey from './toKey.js'
/**
* The base implementation of `get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = castPath(path, object)
let index = 0
const length = path.length
while (object != null && index < length) {
object = object[toKey(path[index++])]
}
return (index && index == length) ? object : undefined
}
When I asked some fellow developers at work for their ideas, I got some amazing answers! I’d particularly like to highlight this one.
Succint, to-the-point, and that break
really helps performance-wise!
/**
* Co-worker's suggestion
*/
const getKeyValue = (obj, path) => {
const keys = path.split('.')
while (keys.length) {
let loc = keys.shift()
if (obj.hasOwnProperty(loc)) {
obj = obj[loc]
} else {
obj = undefined
break
}
}
return obj
}
You can check the performance of these two approaches in this Codepen.
Given 4 paths of varying complexity, and 1 incorrect key, here are the results:
Long story short - always lean on people smarter than you when trying to develop fast code! :)