How Lodash _.set Parses Bracket Notation Paths
Lodash’s _.set method allows developers to modify or
create deeply nested object properties using string paths that support
both dot notation and bracket notation. Under the hood, Lodash
transforms these string paths into structured arrays of property keys
using an internal parser known as stringToPath. This
article breaks down the internal regular expressions, string
tokenization, escaping rules, and memoization techniques Lodash uses to
parse bracket notation into property accessors.
The Path Normalization Pipeline
When you call _.set(object, path, value), Lodash does
not directly manipulate the string while traversing the object. Instead,
it normalizes the path into an array of keys using an internal helper
function named castPath.
If path is already an array (such as
['a', '0', 'b']), Lodash uses it directly. If the path is a
string (such as a[0].b or
a['complex.key'][1]), Lodash delegates the string parsing
to the internal stringToPath function.
The Regular Expression Engine
At the core of stringToPath is a complex regular
expression designed to match property identifiers, bracketed indices,
and quoted bracketed strings while handling edge cases like escaped
characters. In Lodash v4, this expression is defined as
rePropName:
const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;This regular expression matches three primary token patterns sequentially across the string:
- Standard Unbracketed Properties
(
[^.[\]]+): Matches standard property names separated by dots, stopping whenever it encounters a dot (.) or an opening/closing bracket ([or]). - Bracketed Values (
\[...\]):- Numeric Indices (
-?\d+(?:\.\d+)?): Matches unquoted integers or floats placed inside brackets, such as[0]or[-1]. - Quoted Property Names
(
(["'])((?:(?!\2)[^\\]|\\.)*?)\2): Matches single- or double-quoted strings inside brackets, such as['foo']or["bar"]. It captures the quote type in a backreference and matches any character until the matching closing quote, respecting backslash escapes (e.g.,['escaped\'quote']).
- Numeric Indices (
- Empty Brackets or Empty Segments
(
(?=(?:\.|\[\])(?:\.|\[\]|$))): A lookahead group that captures empty segments like[], which are normalized to empty string keys.
Token Processing and Unescaping
As stringToPath executes
RegExp.prototype.replace or match iterations
using rePropName, it processes each matched segment into a
sanitized string key:
- Quoted Bracketed Strings: If a bracketed token
contains quotes (e.g.,
["prop"]), Lodash strips the leading and trailing quote characters and replaces backslash-escaped characters (such as\"or\\) with their unescaped literals using the internalreEscapeCharregex (/\\(\\)?/g). - Unquoted Bracketed Numbers: If the value is a
number (e.g.,
[42]), it is extracted as a string representation of that number ("42"). - Direct Keys: Dot-separated segments (e.g.,
users.name) are pushed directly to the array without alteration.
For example, the path string:
"users[0]['first.name']"is parsed into three discrete array elements:
["users", "0", "first.name"]Because first.name was wrapped in quotes inside
brackets, the parser treats the dot as a literal character rather than a
path delimiter.
Memoization for Performance
Parsing strings with complex regular expressions can introduce
performance bottlenecks if executed repeatedly. To avoid this overhead,
Lodash wraps stringToPath in a memoization function
(memoizeCachable).
Parsed paths are stored in an internal MapCache (capped
at 500 entries). When _.set is called multiple times with
the exact same path string, the regular expression engine is bypassed
entirely after the first invocation, returning the cached array of
segment keys instantly.
Path Application in
_.set
Once the path string is fully decomposed into an array of string
keys, _.set traverses the target object step-by-step. If an
intermediate property does not exist, Lodash checks the upcoming key: if
the next key parses as an integer index, Lodash initializes that
property as an empty array ([]); otherwise, it initializes
it as a plain object ({}). This ensures that paths like
items[0] correctly build array structures, while
items['0'] and items[0] are evaluated
symmetrically.