<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js'></script>
const Tag = Object.freeze({
ARGUMENTS: '[object Arguments]',
ARRAY: '[object Array]',
BOOLEAN: '[object Boolean]',
DATE: '[object Date]',
ERROR: '[object Error]',
MAP: '[object Map]',
NUMBER: '[object Number]',
OBJECT: '[object Object]',
REGEXP: '[object RegExp]',
SET: '[object Set]',
STRING: '[object String]',
SYMBOL: '[object Symbol]',
WEAKMAP: '[object WeakMap]',
WEAKSET: "[object WeakSet]",
ARRAYBUFFER: '[object ArrayBuffer]',
DATAVIEW: '[object DataView]',
FLOAT32: '[object Float32Array]',
FLOAT64: '[object Float64Array]',
INT8: '[object Int8Array]',
INT16: '[object Int16Array]',
INT32: '[object Int32Array]',
UINT8: '[object Uint8Array]',
UINT8CLAMPED: '[object Uint8ClampedArray]',
UINT16: '[object Uint16Array]',
UINT32: '[object Uint32Array]',
BIGINT64: "[object BigInt64Array]",
BIGUINT64: "[object BigUint64Array]"
});
function cloneInternalNoRecursion(_value, customizer, log, doThrow) {
if (typeof log !== "function") log = console.warn;
let result;
// Will be used to store cloned values so that we don't loop infinitely on
// circular references.
const cloneStore = new Map();
// This symbol is used to indicate that the cloned value is the top-level
// object that will be returned by the function.
const TOP_LEVEL = Symbol("TOP_LEVEL");
// A queue so we can avoid recursion.
const queue = [{ value: _value, parentOrAssigner: TOP_LEVEL }];
// We will do a second pass through everything to check Object.isExtensible,
// Object.isSealed and Object.isFrozen. We do it last so we don't run into
// issues where we append properties on a frozen object, etc
const isExtensibleSealFrozen = [];
function warn(message, cause) {
class CloneDeepWarning extends Error {
constructor(message, cause) {
super(message, cause);
this.name = CloneDeepWarning.name;
}
}
return new CloneDeepWarning(message, cause);
}
function assign(cloned, parentOrAssigner, prop, metadata) {
if (parentOrAssigner === TOP_LEVEL)
result = cloned;
else if (typeof parentOrAssigner === "function")
parentOrAssigner(cloned, prop, metadata);
else if (typeof metadata === "object") {
const hasAccessor = ["get", "set"].some(key =>
typeof metadata[key] === "function");
// `cloned` or getAccessor will determine the value
delete metadata.value;
// defineProperty throws if property with accessors is writeable
if (hasAccessor) {
delete metadata.writable;
log(warn("Cloning value whose property descriptor is a get " +
"or set accessor."));
}
Object.defineProperty(parentOrAssigner, prop, Object.assign(
// defineProperty throws if value and set/get accessor coexist
hasAccessor ? {} : { value: cloned },
metadata,
));
}
else
parentOrAssigner[prop] = cloned;
return cloned;
}
function tagOf(value) {
return Object.prototype.toString.call(value);
}
for (let obj = queue.shift(); obj !== undefined; obj = queue.shift()) {
// `value` is the value to deeply clone
// `parentOrAssigner` is either
// - TOP_LEVEL - this value is the top-level object that will be
// returned by the function
// - object - a parent object this value is nested under
// - function - an "assigner" that has the responsiblity of
// assigning the cloned value to something
// `prop` is used with `parentOrAssigner` if it is an object so that the
// cloned object will be assigned to `parentOrAssigner[prop]`.
// `metadata` contains the property descriptor(s) for the value. It may
// be undefined.
const { value, parentOrAssigner, prop, metadata } = obj;
// Will contain the cloned object.
let cloned;
// Check for circular references.
const seen = cloneStore.get(value);
if (seen !== undefined) {
assign(seen, parentOrAssigner, prop, metadata);
continue;
}
// If true, do not not clone the properties of value.
let ignoreProps;
// If true, do not have `cloned` share the prototype of `value`.
let ignoreProto;
// Is true if the customizer determines the value of `cloned`.
let useCustomizerClone;
// Perform user-injected logic if applicable.
if (typeof customizer === "function") {
let clone, additionalValues, ignore;
try {
const customResult = customizer(value);
if (typeof customResult === "object") {
useCustomizerClone = true;
// Must wrap destructure in () if not variable declaration
({ clone,
additionalValues,
ignore,
ignoreProps,
ignoreProto
} = customResult);
if (ignore === true) continue;
cloned = assign(clone,
parentOrAssigner,
prop,
metadata);
if (Array.isArray(additionalValues))
additionalValues.forEach(object => {
if (typeof object === "object") {
queue.push({
value: object.value,
parentOrAssigner: object.assigner
});
}
});
}
}
catch(error) {
if (doThrow === true) throw error;
clone = undefined;
useCustomizerClone = false;
error.message = "customizer encountered error. Its results " +
"will be ignored for the current value, and " +
"the algorithm will proceed with default " +
"behavior. Error encountered: " + error.message;
log(warn(error.message, error.cause));
}
}
try {
// skip the following "else if" branches
if (useCustomizerClone === true) {}
// If value is primitive, just assign it directly.
else if (value === null || !["object", "function"]
.includes(typeof value)) {
assign(value, parentOrAssigner, prop, metadata);
continue;
}
// We won't clone weakmaps or weaksets.
else if ([Tag.WEAKMAP, Tag.WEAKSET].includes(tagOf(value)))
throw warn(`Attempted to clone unsupported type${
typeof value.constructor === "function" &&
typeof value.constructor.name === "string"
? ` ${value.constructor.name}`
: ""
}.`);
// We only copy functions if they are methods.
else if (typeof value === "function") {
cloned = assign(parentOrAssigner !== TOP_LEVEL
? value
: {},
parentOrAssigner,
prop,
metadata);
log(warn(`Attempted to clone function${typeof prop === "string"
? ` with name ${prop}`
: "" }. ` +
"JavaScript functions cannot be cloned. If this " +
"function is a method, then it will be copied "+
"directly."));
if (parentOrAssigner === TOP_LEVEL) continue;
}
// If value is a Node Buffer, just use Buffer's subarray method.
else if (typeof global === "object"
&& global.Buffer
&& typeof Buffer === "function"
&& typeof Buffer.isBuffer === "function"
&& Buffer.isBuffer(value))
cloned = assign(value.subarray(),
parentOrAssigner,
prop,
metadata);
else if (Array.isArray(value))
cloned = assign(new Array(value.length),
parentOrAssigner,
prop,
metadata);
// Ordinary objects, or the rare `arguments` clone
else if ([Tag.OBJECT, Tag.ARGUMENTS].includes(tagOf(value)))
cloned = assign(Object.create(Object.getPrototypeOf(value)),
parentOrAssigner,
prop,
metadata);
// values that will be called using contructor
else {
const Value = value.constructor;
// Booleans, Number, String or Symbols which used `new` syntax
// so JavaScript thinks they are objects
// We also handle Date here because it is convenient
if ([Tag.BOOLEAN, Tag.DATE].includes(tagOf(value)))
cloned = assign(new Value(Number(value)),
parentOrAssigner,
prop,
metadata);
else if ([Tag.NUMBER, Tag.STRING].includes(tagOf(value)))
cloned = assign(new Value(value),
parentOrAssigner,
prop,
metadata);
else if (Tag.SYMBOL === tagOf(value)) {
cloned = assign(
Object(Symbol.prototype.valueOf.call(value)),
parentOrAssigner,
prop,
metadata);
}
else if (Tag.REGEXP === tagOf(value)) {
const regExp = new Value(value.source, /\w*$/.exec(value));
regExp.lastIndex = value.lastIndex;
cloned = assign(regExp, parentOrAssigner, prop, metadata);
}
else if (Tag.ERROR === tagOf(value)) {
const cause = value.cause;
cloned = assign(cause === undefined
? new Value(value.message)
: new Value(value.message, { cause }),
parentOrAssigner,
prop,
metadata);
}
else if (Tag.ARRAYBUFFER === tagOf(value)) {
// copy data over to clone
const arrayBuffer = new Value(value.byteLength);
new Uint8Array(arrayBuffer).set(new Uint8Array(value));
cloned = assign(arrayBuffer,
parentOrAssigner,
prop,
metadata);
}
// TypeArrays
else if ([
Tag.DATAVIEW,
Tag.FLOAT32,
Tag.FLOAT64,
Tag.INT8,
Tag.INT16,
Tag.INT32,
Tag.UINT8,
Tag.UINT8CLAMPED,
Tag.UINT16,
Tag.UINT32,
Tag.BIGINT64,
Tag.BIGUINT64
].includes(tagOf(value))) {
// copy data over to clone
const buffer = new value.buffer.constructor(
value.buffer.byteLength);
new Uint8Array(buffer).set(new Uint8Array(value.buffer));
cloned = assign(
new Value(buffer, value.byteOffset, value.length),
parentOrAssigner,
prop,
metadata);
}
else if (Tag.MAP === tagOf(value)) {
const map = new Value;
cloned = assign(map, parentOrAssigner, prop, metadata);
value.forEach((subValue, key) => {
queue.push({
value: subValue,
parentOrAssigner: cloned => {
isExtensibleSealFrozen.push([subValue, cloned]);
map.set(key, cloned)
}
});
});
}
else if (Tag.SET === tagOf(value)) {
const set = new Value;
cloned = assign(set, parentOrAssigner, prop, metadata);
value.forEach(subValue => {
queue.push({
value: subValue,
parentOrAssigner: cloned => {
isExtensibleSealFrozen.push([subValue, cloned]);
map.set(key, cloned)
}
});
});
}
else
throw warn("Attempted to clone unsupported type.");
}
}
catch(error) {
error.message = "Encountered error while attempting to clone " +
"specific value. The value will be \"cloned\" " +
"into an empty object. Error encountered: " +
error.message
log(warn(error.message, error.cause));
cloned = assign({}, parentOrAssigner, prop, metadata);
// We don't want the prototype if we failed and set the value to an
// empty object.
ignoreProto = true;
}
cloneStore.set(value, cloned);
isExtensibleSealFrozen.push([value, cloned]);
// Ensure clone has prototype of value
if (ignoreProto !== true
&& Object.getPrototypeOf(cloned) !== Object.getPrototypeOf(value))
Object.setPrototypeOf(cloned, Object.getPrototypeOf(value));
if (ignoreProps === true) continue;
// Now copy all enumerable and non-enumerable properties.
[Object.getOwnPropertyNames(value), Object.getOwnPropertySymbols(value)]
.flat()
.forEach(key => {
queue.push({
value: value[key],
parentOrAssigner: cloned,
prop: key,
metadata: Object.getOwnPropertyDescriptor(value, key)
});
});
}
// Check extensible, seal, and frozen statuses.
isExtensibleSealFrozen.forEach(([value, cloned]) => {
if (!Object.isExtensible(value)) Object.preventExtensions(cloned);
if (Object.isSealed(value)) Object.seal(cloned);
if (Object.isFrozen(value)) Object.freeze(cloned);
});
return result;
}
function cloneDeep(value, options) {
if (typeof options === "function") options = { customizer: options };
else if (typeof options !== "object") options = {};
let { customizer, log, logMode, letCustomizerThrow } = options;
if (logMode !== "string" || typeof log === "function");
else if (logMode.toLowerCase() === "silent")
log = () => { /* no-op */ };
else if (logMode.toLowerCase() === "quiet")
log = error => console.warn(error.message);
return cloneInternalNoRecursion(value, customizer, log, letCustomizerThrow);
}
var myObject = {};
let next = myObject;
for (let i = 0; i < 1000; i++) {
next.b = {};
next = next.b;
}
let myCopy;
myCopy = _.cloneDeep(myObject);
myCopy = structuredClone(myObject);
myCopy = cloneDeep(myObject);
--enable-precise-memory-info
flag.
Test case name | Result |
---|---|
Lodash cloneDeep | |
Native structuredClone | |
mt cloneDeep |
Test name | Executions per second |
---|---|
Lodash cloneDeep | 3915.8 Ops/sec |
Native structuredClone | 5390.4 Ops/sec |
mt cloneDeep | 1003.2 Ops/sec |
I'm here to help!
It appears that we have three benchmark results from different libraries:
Lodash cloneDeep
Native structuredClone
(from the Web API)mt cloneDeep
(another cloning library)To provide a helpful answer, I'll need to know what you'd like me to do with these benchmark results.
Do you want me to: A) Compare the execution speeds of these libraries B) Analyze the differences in the implementation details between these libraries C) Provide suggestions on which library might be more suitable for your specific use case
Let me know, and I'll do my best to assist you!