Lodash isPlainObject Behavior with ES6 Proxy

Passing an instantiated ES6 Proxy to Lodash’s _.isPlainObject method can produce unexpected evaluations, silent side effects, and structural bugs in data pipelines. Because an ES6 Proxy transparently forwards internal operations to its target by default, Lodash evaluates the proxy based on the underlying target's prototype chain rather than the proxy wrapper itself. This article breaks down the internal mechanism of _.isPlainObject, how Proxies interact with its checks, and the practical implications for deep cloning, state management, and debugging.

How Lodash Evaluates Plain Objects

Lodash defines a plain object as an object created by the Object constructor or one with a [[Prototype]] of null. To determine this, _.isPlainObject performs two primary checks:

  1. Tag Verification: It checks Object.prototype.toString.call(value) === '[object Object]' to rule out primitives, functions, arrays, and standard built-ins (like Map or Date).
  2. Prototype Chain Traversal: It walks up the prototype chain using Object.getPrototypeOf(value) until it reaches the root prototype, verifying that the prototype matches Object.prototype or null.

Proxy Evaluation Mechanics

An ES6 Proxy does not have its own distinct [[Prototype]] or type tag; it delegates internal calls ([[GetPrototypeOf]], [[Get]]) to the target object unless a handler trap intercepts them.

const target = {};
const proxy = new Proxy(target, {});
console.log(_.isPlainObject(proxy)); // true

const classInstance = new (class MyClass {})();
const classProxy = new Proxy(classInstance, {});
console.log(_.isPlainObject(classProxy)); // false

Major Implications

1. Invalidation of Protective Boundaries

Proxies are commonly used to implement encapsulation, access control, or reactive wrappers (e.g., Vue Reactivity, MobX). When _.isPlainObject(proxy) returns true, external systems assume the object is dumb, inert data. This can bypass developer expectations that the object is a managed or controlled structure.

2. Trap Execution and Unintended Side Effects

During prototype inspection, Lodash calls Object.getPrototypeOf. If your proxy defines a getPrototypeOf trap with logging, analytics, or lazy-initialization logic, simply running _.isPlainObject(proxy) executes that code. Similarly, if the object uses a Symbol.toStringTag getter, the initial tag validation step triggers property access traps.

3. Destruction of Dynamic Behavior in Merges and Clones

Utilities like _.cloneDeep and _.merge rely heavily on _.isPlainObject to decide whether to create a new object and recurse or copy the reference directly.

If a Proxy evaluates as a plain object:

4. False Negatives via Custom String Tags

If a proxy's target uses Symbol.toStringTag (or intercepts it via a get trap) to assign a custom type label, Object.prototype.toString.call(proxy) returns [object CustomName]. In this scenario, _.isPlainObject returns false immediately, even if the object's prototype chain is entirely plain.

Summary of Best Practices

When handling objects that may be Proxies: