Skip to content

Commit

Permalink
code cleanup, and optimize for runtime speed on object clones
Browse files Browse the repository at this point in the history
  • Loading branch information
planttheidea committed Sep 28, 2022
1 parent 4a37cf8 commit b82035e
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 43 deletions.
12 changes: 6 additions & 6 deletions __tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('getCleanClone', () => {
it('will return a pure object when there is no constructor', () => {
const object = Object.create(null);

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual(object);
Expand All @@ -70,7 +70,7 @@ describe('getCleanClone', () => {

object.__proto__ = null;

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual(object);
Expand All @@ -81,7 +81,7 @@ describe('getCleanClone', () => {
it('will return an empty POJO when the object passed is a POJO', () => {
const object = { foo: 'bar' };

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual({});
Expand All @@ -96,7 +96,7 @@ describe('getCleanClone', () => {

object.foo = 'bar';

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual({});
Expand All @@ -107,7 +107,7 @@ describe('getCleanClone', () => {
it('will return an empty object with the given constructor when it is a global constructor', () => {
const object = new Map();

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual(new Map());
Expand All @@ -128,7 +128,7 @@ describe('getCleanClone', () => {

const object = new Foo('bar');

const result = utils.getCleanClone(object, Object.getPrototypeOf(object));
const result = utils.getCleanClone(Object.getPrototypeOf(object));

expect(result).not.toBe(object);
expect(result).toEqual(Object.create(Foo.prototype));
Expand Down
99 changes: 62 additions & 37 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ const {
getOwnPropertyDescriptor,
getOwnPropertyNames,
getOwnPropertySymbols,
getPrototypeOf,
} = Object;
const { hasOwnProperty, propertyIsEnumerable } = Object.prototype;

const SYMBOL_PROPERTIES = typeof getOwnPropertySymbols === 'function';
const SYMBOL_SUPPORT = typeof getOwnPropertySymbols === 'function';

class LegacyCache {
_keys: any[] = [];
Expand Down Expand Up @@ -53,6 +52,16 @@ function createCacheModern(): Cache {
export const createCache =
typeof WeakMap !== 'undefined' ? createCacheModern : createCacheLegacy;

function getStrictPropertiesModern(object: any): Array<string | symbol> {
return (getOwnPropertyNames(object) as Array<string | symbol>).concat(
getOwnPropertySymbols(object)
);
}

const getStrictProperties = SYMBOL_SUPPORT
? getStrictPropertiesModern
: getOwnPropertyNames;

export function getArrayCloneLoose(
array: any[],
prototype: any,
Expand All @@ -73,7 +82,7 @@ export function getArrayCloneLoose(
/**
* Get an empty version of the object with the same prototype it has.
*/
export function getCleanClone(object: any, prototype: any): any {
export function getCleanClone(prototype: any): any {
if (!prototype) {
return create(null);
}
Expand All @@ -93,17 +102,13 @@ export function getCleanClone(object: any, prototype: any): any {
return create(prototype);
}

/**
* Get a copy of the object based on loose rules, meaning all enumerable keys
* and symbols are copied, but property descriptors are not considered.
*/
export function getObjectCloneLoose(
object: any,
function getObjectCloneLooseLegacy<Value extends {}>(
object: Value,
prototype: any,
handleCopy: InternalCopier,
cache: Cache
): any {
const clone: any = getCleanClone(object, prototype);
): Value {
const clone: any = getCleanClone(prototype);

// set in the cache immediately to be able to reuse the object recursively
cache.set(object, clone);
Expand All @@ -114,46 +119,66 @@ export function getObjectCloneLoose(
}
}

if (SYMBOL_PROPERTIES) {
const symbols: symbol[] = getOwnPropertySymbols(object);
return clone;
}

function getObjectCloneLooseModern<Value extends {}>(
object: Value,
prototype: any,
handleCopy: InternalCopier,
cache: Cache
): Value {
const clone: any = getCleanClone(prototype);

for (
let index = 0, length = symbols.length, symbol;
index < length;
++index
) {
symbol = symbols[index];
// set in the cache immediately to be able to reuse the object recursively
cache.set(object, clone);

if (propertyIsEnumerable.call(object, symbol)) {
clone[symbol] = handleCopy(object[symbol], cache);
}
for (const key in object) {
if (hasOwnProperty.call(object, key)) {
clone[key] = handleCopy(object[key], cache);
}
}

return clone;
}
const symbols: symbol[] = getOwnPropertySymbols(object);

function getStrictPropertiesModern(object: any): Array<string | symbol> {
return (getOwnPropertyNames(object) as Array<string | symbol>).concat(
getOwnPropertySymbols(object)
);
if (!symbols.length) {
return clone;
}

for (
let index = 0, length = symbols.length, symbol;
index < length;
++index
) {
symbol = symbols[index];

if (propertyIsEnumerable.call(object, symbol)) {
clone[symbol] = handleCopy((object as any)[symbol], cache);
}
}

return clone;
}

const getStrictProperties = SYMBOL_PROPERTIES
? getStrictPropertiesModern
: getOwnPropertyNames;
/**
* Get a copy of the object based on loose rules, meaning all enumerable keys
* and symbols are copied, but property descriptors are not considered.
*/
export const getObjectCloneLoose = SYMBOL_SUPPORT
? getObjectCloneLooseModern
: getObjectCloneLooseLegacy;

/**
* Get a copy of the object based on strict rules, meaning all keys and symbols
* are copied based on the original property descriptors.
*/
export function getObjectCloneStrict(
object: any,
export function getObjectCloneStrict<Value extends {}>(
object: Value,
prototype: any,
handleCopy: InternalCopier,
cache: Cache
): any {
const clone: any = getCleanClone(object, prototype);
): Value {
const clone = getCleanClone(prototype);

// set in the cache immediately to be able to reuse the object recursively
cache.set(object, clone);
Expand All @@ -173,7 +198,7 @@ export function getObjectCloneStrict(
if (descriptor) {
// Only clone the value if actually a value, not a getter / setter.
if (!descriptor.get && !descriptor.set) {
descriptor.value = handleCopy(object[property], cache);
descriptor.value = handleCopy((object as any)[property], cache);
}

try {
Expand All @@ -185,7 +210,7 @@ export function getObjectCloneStrict(
} else {
// In extra edge cases where the property descriptor cannot be retrived, fall back to
// the loose assignment.
clone[property] = handleCopy(object[property], cache);
clone[property] = handleCopy((object as any)[property], cache);
}
}
}
Expand Down

0 comments on commit b82035e

Please sign in to comment.