How Lodash cloneDeep Handles ES6 Setters
Lodash’s _.cloneDeep is designed to recursively copy
values across nested arrays and plain objects, but it does not preserve
ES6 property descriptors such as dynamically assigned setters or
getters. When processing an object containing accessors,
_.cloneDeep invokes the getter to extract the current
primitive or reference value and assigns that result as a standard data
property on the target clone. This article details the internal behavior
of _.cloneDeep with dynamic ES6 setters, demonstrates the
resulting state of cloned properties, and explains how to preserve
descriptors when deep cloning.
The Mechanism
Behind _.cloneDeep and Accessors
JavaScript objects distinguish between data properties
(which store an actual value) and accessor properties (which
define get and set methods).
Under the hood, Lodash's cloning mechanism uses internal methods such
as baseClone and standard property iteration to duplicate
enumerable properties. It does not inspect or transfer property
descriptors defined via Object.defineProperty or
Object.defineProperties.
When _.cloneDeep encounters a property backed by an ES6
setter:
- Evaluation: It attempts to read the property via
standard member access (
source[key]). If a getter accompanies the setter, the getter executes and returns a value. If only a setter exists, reading the property returnsundefined. - Assignment: Lodash recursively clones the evaluated
value and assigns it directly to the new object using standard
assignment (
target[key] = clonedValue). - Loss of Descriptor: The cloned object receives a standard data property containing the evaluated value. The original setter function is discarded.
Code Example: Dynamic Setters in Action
Consider an object where an ES6 setter is assigned dynamically at runtime:
const _ = require('lodash');
const original = {};
let internalValue = 0;
Object.defineProperty(original, 'computedProp', {
enumerable: true,
configurable: true,
get() {
return internalValue;
},
set(val) {
internalValue = val * 2;
}
});
// Mutate via setter
original.computedProp = 5; // internalValue becomes 10
// Clone using Lodash
const cloned = _.cloneDeep(original);
// Inspect cloned object
console.log(cloned.computedProp); // Outputs: 10
// Attempt to invoke the setter on the clone
cloned.computedProp = 5;
console.log(cloned.computedProp); // Outputs: 5 (setter did not execute)
console.log(Object.getOwnPropertyDescriptor(cloned, 'computedProp'));
// { value: 5, writable: true, enumerable: true, configurable: true }In this scenario:
- The getter executed during cloning, evaluating
original.computedPropto10. - The clone received a flat, writable data descriptor
{ value: 10, ... }. - Subsequent assignments to
cloned.computedPropoperate as simple value reassignments, completely bypassing the original custom setter logic.
Behavior with Write-Only Setters
If an object contains a dynamically assigned setter without a paired getter:
const writeOnlyObj = {};
Object.defineProperty(writeOnlyObj, 'setterOnly', {
enumerable: true,
configurable: true,
set(val) {
this._val = val;
}
});
const clonedWriteOnly = _.cloneDeep(writeOnlyObj);
console.log(clonedWriteOnly.setterOnly); // Outputs: undefined
console.log(Object.getOwnPropertyDescriptor(clonedWriteOnly, 'setterOnly'));
// { value: undefined, writable: true, enumerable: true, configurable: true }Because there is no getter to return a value,
_.cloneDeep reads undefined and statically
defines the property with a value of undefined.
How to Preserve Dynamic ES6 Setters
To duplicate an object while maintaining dynamic accessors, you must clone property descriptors directly using native ES6 reflection methods rather than relying on Lodash's value-based cloning.
A shallow descriptor-preserving clone can be implemented using
Object.getOwnPropertyDescriptors and
Object.create:
function cloneWithDescriptors(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
const descriptors = Object.getOwnPropertyDescriptors(obj);
const prototype = Object.getPrototypeOf(obj);
return Object.create(prototype, descriptors);
}If deep-cloning values within the descriptors is required, you can
combine descriptor enumeration with _.cloneDeep by
selectively applying _.cloneDeep to
descriptor.value while leaving descriptor.get
and descriptor.set intact before defining them on the
target object.