Test name | Executions per second |
---|---|
Lodash cloneDeep | 3915.8 Ops/sec |
Native structuredClone | 5390.4 Ops/sec |
mt cloneDeep | 1003.2 Ops/sec |
<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);