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:

  1. 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 returns undefined.
  2. Assignment: Lodash recursively clones the evaluated value and assigns it directly to the new object using standard assignment (target[key] = clonedValue).
  3. 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:

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.