How Lodash propertyOf Parses Object Keys Internally

This article provides an in-depth look at how the Lodash library executes its _.propertyOf method behind the scenes. It covers the inner workings of Lodash's functional key lookup system, focusing on how internal helper functions normalize, parse, memoize, and traverse nested object properties when accessing complex data structures dynamically.

The Role of _.propertyOf

_.propertyOf creates a function that accepts a path and returns the value at that path from a targeted source object. Unlike _.get—which takes the target object and key path simultaneously—_.propertyOf curries the target object, creating a reusable accessor function tailored specifically for querying that reference:

const lookup = _.propertyOf({ user: { details: { id: 101 } } });
lookup('user.details.id'); // Returns 101

Internally, _.propertyOf is structured as a closure:

function propertyOf(object) {
  return function(path) {
    return object == null ? undefined : baseGet(object, path);
  };
}

If the closed-over object is null or undefined, the accessor returns undefined immediately. Otherwise, execution delegates to baseGet.

Path Tokenization and castPath

The string or array representing the target key must be resolved into an iterable sequence of property segments. Lodash routes the incoming path through an internal function named castPath.

  1. Array Check: If the provided path is already an array of strings or symbols (e.g., ['user', 'details', 'id']), castPath returns it directly.
  2. Simple Key Optimization (isKey): Before running full string parsing, Lodash checks whether the input string is a flat, single-level property name. If the path is a symbol, a number, or a string lacking dots (.) or brackets ([]), or if the key already exists as an own/inherited property on the target object, parsing is bypassed entirely, treating the path as a single key.
  3. Complex Path Delegation: When the path contains delimiters like dots or brackets, it is handed off to stringToPath.

Parsing Paths via stringToPath and Memoization

To prevent performance degradation when resolving the same key paths repeatedly across iterations, stringToPath wraps its parsing logic in an internal LRU-style cache using memoizeCachable.

When a cache miss occurs, the string is split into individual tokens using a compiled regular expression:

const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;

This pattern matches:

Matched tokens are cleaned, pushed into a sequential array of string components, stored in the memoization cache, and returned to baseGet.

Object Traversal via baseGet and toKey

Once the path is converted into an array of path keys, baseGet iteratively traverses the object graph using a while loop:

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;
}

Property Normalization with toKey

During traversal, each segment passes through toKey. This ensures that negative zeroes (-0) are converted to "-0" and JavaScript Symbol primitives are preserved without triggering unwanted string coercion errors.

Safe Navigation

The while loop checks object != null at each iteration. If any intermediate link in the path resolves to null or undefined, the loop terminates early. This mechanism ensures strict runtime safety, preventing TypeError: Cannot read property of undefined exceptions without requiring modern optional chaining operators (?.).

Verification

After the loop exits, (index && index == length) verifies that traversal resolved every segment in the path sequence. If all segments were navigated successfully, the resolved value is returned; otherwise, it resolves to undefined.