Accessing Arbitrary Paths via String Dot Notation in JavaScript

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 reduced 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!
    • That’s a 24% difference!

Long story short - always lean on people smarter than you when trying to develop fast code! :)