10 May 2020 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:
My approach takes 0.25ms My co-worker’s approach takes only 0.19ms! Long story short - always lean on people smarter than you when trying to develop fast code! :)