diff --git a/__tests__/copiers.ts b/__tests__/copiers.ts new file mode 100644 index 0000000..d456073 --- /dev/null +++ b/__tests__/copiers.ts @@ -0,0 +1,162 @@ +type PlainObject = { + [key: string]: any; + [index: number]: any; +}; + +import { createCache } from '../src/utils'; + +let copiers: typeof import('../src/copiers'); + +beforeEach(() => { + jest.isolateModules(() => { + copiers = require('../src/copiers'); + }); +}); + +describe('copyObjectLoose', () => { + it('will create an object clone when property symbols are not supported', () => { + const original = Object.getOwnPropertySymbols; + + jest.isolateModules(() => { + Object.getOwnPropertySymbols = undefined; + copiers = require('../src/copiers'); + }); + + const symbol = Symbol('quz'); + const object = { + bar: { baz: 'quz' }, + foo: 'bar', + [symbol]: 'blah', + }; + const handleCopy = jest.fn().mockImplementation((arg) => arg); + const cache = createCache(); + + const result = copiers.copyObjectLoose( + object, + Object.getPrototypeOf(object), + handleCopy, + cache + ); + + Object.getOwnPropertySymbols = original; + + expect(result).not.toBe(object); + expect(result).toEqual( + Object.keys(object).reduce((clone: PlainObject, key): PlainObject => { + clone[key] = object[key as keyof typeof object]; + + return clone; + }, {}) + ); + + expect(handleCopy).toHaveBeenCalledTimes(Object.keys(object).length); + }); + + it('will create an object clone when property symbols are supported', () => { + const object = { + bar: { baz: 'quz' }, + [Symbol('quz')]: 'blah', + }; + const handleCopy = jest.fn().mockImplementation((arg) => arg); + const cache = createCache(); + + const result = copiers.copyObjectLoose( + object, + Object.getPrototypeOf(object), + handleCopy, + cache + ); + + expect(result).not.toBe(object); + expect(result).toEqual(object); + + expect(handleCopy).toHaveBeenCalledTimes( + Object.keys(object).length + Object.getOwnPropertySymbols(object).length + ); + }); +}); + +describe('copyObjectStrict', () => { + it('will create an object clone when property symbols are not supported', () => { + const original = Object.getOwnPropertySymbols; + + jest.isolateModules(() => { + Object.getOwnPropertySymbols = undefined; + copiers = require('../src/copiers'); + }); + + const object: PlainObject = { + bar: { baz: 'quz' }, + }; + + Object.defineProperty(object, 'foo', { + value: 'bar', + }); + + Object.defineProperty(object, Symbol('quz'), { + enumerable: true, + value: 'blah', + }); + + const handleCopy = jest.fn().mockImplementation((arg) => arg); + const cache = createCache(); + + const result = copiers.copyObjectStrict( + object, + Object.getPrototypeOf(object), + handleCopy, + cache + ); + + Object.getOwnPropertySymbols = original; + + expect(result).not.toBe(object); + expect(result).toEqual( + Object.keys(object).reduce( + (clone: PlainObject, key: string): PlainObject => { + clone[key] = object[key]; + + return clone; + }, + {} + ) + ); + + expect(handleCopy).toHaveBeenCalledTimes( + Object.getOwnPropertyNames(object).length + ); + }); + + it('will create an object clone when property symbols are not supported', () => { + const object: PlainObject = { + bar: { baz: 'quz' }, + }; + + Object.defineProperty(object, 'foo', { + value: 'bar', + }); + + Object.defineProperty(object, Symbol('quz'), { + enumerable: true, + value: 'blah', + }); + + const handleCopy = jest.fn().mockImplementation((arg) => arg); + const cache = createCache(); + + const result = copiers.copyObjectStrict( + object, + Object.getPrototypeOf(object), + handleCopy, + cache + ); + + expect(result).not.toBe(object); + expect(result).toEqual(object); + + expect(handleCopy).toHaveBeenCalledTimes( + Object.getOwnPropertyNames(object).length + + Object.getOwnPropertySymbols(object).length + ); + }); +}); diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 1f95bf0..7316102 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -137,154 +137,6 @@ describe('getCleanClone', () => { }); }); -describe('getObjectCloneLoose', () => { - it('will create an object clone when property symbols are not supported', () => { - const original = Object.getOwnPropertySymbols; - - jest.isolateModules(() => { - Object.getOwnPropertySymbols = undefined; - utils = require('../src/utils'); - }); - - const symbol = Symbol('quz'); - const object = { - bar: { baz: 'quz' }, - foo: 'bar', - [symbol]: 'blah', - }; - const handleCopy = jest.fn().mockImplementation((arg) => arg); - const cache = utils.createCache(); - - const result = utils.getObjectCloneLoose( - object, - Object.getPrototypeOf(object), - handleCopy, - cache - ); - - Object.getOwnPropertySymbols = original; - - expect(result).not.toBe(object); - expect(result).toEqual( - Object.keys(object).reduce((clone: PlainObject, key): PlainObject => { - clone[key] = object[key as keyof typeof object]; - - return clone; - }, {}) - ); - - expect(handleCopy).toHaveBeenCalledTimes(Object.keys(object).length); - }); - - it('will create an object clone when property symbols are supported', () => { - const object = { - bar: { baz: 'quz' }, - [Symbol('quz')]: 'blah', - }; - const handleCopy = jest.fn().mockImplementation((arg) => arg); - const cache = utils.createCache(); - - const result = utils.getObjectCloneLoose( - object, - Object.getPrototypeOf(object), - handleCopy, - cache - ); - - expect(result).not.toBe(object); - expect(result).toEqual(object); - - expect(handleCopy).toHaveBeenCalledTimes( - Object.keys(object).length + Object.getOwnPropertySymbols(object).length - ); - }); -}); - -describe('getObjectCloneStrict', () => { - it('will create an object clone when property symbols are not supported', () => { - const original = Object.getOwnPropertySymbols; - - jest.isolateModules(() => { - Object.getOwnPropertySymbols = undefined; - utils = require('../src/utils'); - }); - - const object: PlainObject = { - bar: { baz: 'quz' }, - }; - - Object.defineProperty(object, 'foo', { - value: 'bar', - }); - - Object.defineProperty(object, Symbol('quz'), { - enumerable: true, - value: 'blah', - }); - - const handleCopy = jest.fn().mockImplementation((arg) => arg); - const cache = utils.createCache(); - - const result = utils.getObjectCloneStrict( - object, - Object.getPrototypeOf(object), - handleCopy, - cache - ); - - Object.getOwnPropertySymbols = original; - - expect(result).not.toBe(object); - expect(result).toEqual( - Object.keys(object).reduce( - (clone: PlainObject, key: string): PlainObject => { - clone[key] = object[key]; - - return clone; - }, - {} - ) - ); - - expect(handleCopy).toHaveBeenCalledTimes( - Object.getOwnPropertyNames(object).length - ); - }); - - it('will create an object clone when property symbols are not supported', () => { - const object: PlainObject = { - bar: { baz: 'quz' }, - }; - - Object.defineProperty(object, 'foo', { - value: 'bar', - }); - - Object.defineProperty(object, Symbol('quz'), { - enumerable: true, - value: 'blah', - }); - - const handleCopy = jest.fn().mockImplementation((arg) => arg); - const cache = utils.createCache(); - - const result = utils.getObjectCloneStrict( - object, - Object.getPrototypeOf(object), - handleCopy, - cache - ); - - expect(result).not.toBe(object); - expect(result).toEqual(object); - - expect(handleCopy).toHaveBeenCalledTimes( - Object.getOwnPropertyNames(object).length + - Object.getOwnPropertySymbols(object).length - ); - }); -}); - describe('getRegExpFlags', () => { it('will return an empty string when no flags are on the regexp', () => { const regexp = /foo/; diff --git a/src/copiers.ts b/src/copiers.ts new file mode 100644 index 0000000..f9b0969 --- /dev/null +++ b/src/copiers.ts @@ -0,0 +1,199 @@ +import { getCleanClone, getRegExpFlags } from './utils'; + +import type { Cache } from './utils'; + +type InternalCopier = (value: Value, cache: Cache) => Value; + +const { + defineProperty, + getOwnPropertyDescriptor, + getOwnPropertyNames, + getOwnPropertySymbols, +} = Object; +const { hasOwnProperty, propertyIsEnumerable } = Object.prototype; + +const SUPPORTS_SYMBOL = typeof getOwnPropertySymbols === 'function'; + +function getStrictPropertiesModern(object: any): Array { + return (getOwnPropertyNames(object) as Array).concat( + getOwnPropertySymbols(object) + ); +} + +const getStrictProperties = SUPPORTS_SYMBOL + ? getStrictPropertiesModern + : getOwnPropertyNames; + +export function copyArrayLoose( + array: any[], + prototype: any, + handleCopy: InternalCopier, + cache: Cache +) { + const clone = new prototype.constructor(); + + cache.set(array, clone); + + for (let index: number = 0, length = array.length; index < length; ++index) { + clone[index] = handleCopy(array[index], cache); + } + + return clone; +} + +function copyObjectLooseLegacy( + object: Value, + prototype: any, + handleCopy: InternalCopier, + cache: Cache +): Value { + const clone: any = getCleanClone(prototype); + + // set in the cache immediately to be able to reuse the object recursively + cache.set(object, clone); + + for (const key in object) { + if (hasOwnProperty.call(object, key)) { + clone[key] = handleCopy(object[key], cache); + } + } + + return clone; +} + +function copyObjectLooseModern( + object: Value, + prototype: any, + handleCopy: InternalCopier, + cache: Cache +): Value { + const clone: any = getCleanClone(prototype); + + // set in the cache immediately to be able to reuse the object recursively + cache.set(object, clone); + + for (const key in object) { + if (hasOwnProperty.call(object, key)) { + clone[key] = handleCopy(object[key], cache); + } + } + + const symbols: symbol[] = 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; +} + +/** + * 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 copyObjectLoose = SUPPORTS_SYMBOL + ? copyObjectLooseModern + : copyObjectLooseLegacy; + +/** + * 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 copyObjectStrict( + object: Value, + prototype: any, + handleCopy: InternalCopier, + cache: Cache +): Value { + const clone = getCleanClone(prototype); + + // set in the cache immediately to be able to reuse the object recursively + cache.set(object, clone); + + const properties = getStrictProperties(object); + + for ( + let index = 0, length = properties.length, property, descriptor; + index < length; + ++index + ) { + property = properties[index]; + + if (property !== 'callee' && property !== 'caller') { + descriptor = getOwnPropertyDescriptor(object, property); + + if (descriptor) { + // Only clone the value if actually a value, not a getter / setter. + if (!descriptor.get && !descriptor.set) { + descriptor.value = handleCopy((object as any)[property], cache); + } + + try { + defineProperty(clone, property, descriptor); + } catch (error) { + // Tee above can fail on node in edge cases, so fall back to the loose assignment. + clone[property] = descriptor.value; + } + } else { + // In extra edge cases where the property descriptor cannot be retrived, fall back to + // the loose assignment. + clone[property] = handleCopy((object as any)[property], cache); + } + } + } + + return clone; +} + +export function copyMap>( + value: Value, + Constructor: MapConstructor, + handleCopy: InternalCopier, + cache: Cache +): Value { + const clone = new Constructor() as Value; + + value.forEach((v, k) => { + clone.set(k, handleCopy(v, cache)); + }); + + return value; +} + +export function copyRegExp( + value: Value, + Constructor: RegExpConstructor +): Value { + const clone = new Constructor(value.source, getRegExpFlags(value)) as Value; + + clone.lastIndex = value.lastIndex; + + return clone; +} + +export function copySet>( + value: Value, + Constructor: SetConstructor, + handleCopy: InternalCopier, + cache: Cache +): Value { + const clone = new Constructor() as Value; + + value.forEach((v) => { + clone.add(handleCopy(v, cache)); + }); + + return value; +} diff --git a/src/index.ts b/src/index.ts index 5d515ff..a96b750 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,17 @@ import { - createCache, - getArrayCloneLoose, - getObjectCloneLoose, - getObjectCloneStrict, - getRegExpFlags, -} from './utils'; + copyArrayLoose, + copyObjectLoose, + copyObjectStrict, + copyMap, + copyRegExp, + copySet, +} from './copiers'; +import { createCache } from './utils'; import type { Cache } from './utils'; -type GetArrayClone = typeof getArrayCloneLoose | typeof getObjectCloneStrict; -type GetObjectClone = typeof getObjectCloneLoose | typeof getObjectCloneStrict; +type CopyArray = typeof copyArrayLoose | typeof copyObjectStrict; +type CopyObject = typeof copyObjectLoose | typeof copyObjectStrict; const { isArray } = Array; const { getPrototypeOf } = Object; @@ -35,11 +37,7 @@ const UNCOPIABLE_OBJECT_CLASSES: Record = { ['[object WeakSet]']: true, }; -function performCopy( - value: Value, - getObjectClone: GetObjectClone, - getArrayClone: GetArrayClone -) { +function createCopier(copyArray: CopyArray, copyObject: CopyObject) { function handleCopy(value: any, cache: Cache): any { if (!value || typeof value !== 'object') { return value; @@ -54,12 +52,12 @@ function performCopy( // plain objects if (!Constructor || Constructor === Object) { - return getObjectClone(value, prototype, handleCopy, cache); + return copyObject(value, prototype, handleCopy, cache); } // arrays if (isArray(value)) { - return getArrayClone(value, prototype, handleCopy, cache); + return copyArray(value, prototype, handleCopy, cache); } const objectClass = toString.call(value); @@ -71,19 +69,12 @@ function performCopy( // regexps if (objectClass === '[object RegExp]') { - const clone = new Constructor( - value.source, - value.flags || getRegExpFlags(value) - ); - - clone.lastIndex = value.lastIndex; - - return clone; + return copyRegExp(value, Constructor); } // maps if (objectClass === '[object Map]') { - const clone = new Constructor(value.entries()); + const clone = copyMap(value, Constructor, handleCopy, cache); cache.set(value, clone); @@ -92,7 +83,7 @@ function performCopy( // sets if (objectClass === '[object Set]') { - const clone = new Constructor(value.values()); + const clone = copySet(value, Constructor, handleCopy, cache); cache.set(value, clone); @@ -133,24 +124,20 @@ function performCopy( } // assume anything left is a custom constructor - return getObjectClone(value, prototype, handleCopy, cache); + return copyObject(value, prototype, handleCopy, cache); } - return handleCopy(value, createCache()); + return (value: Value): Value => handleCopy(value, createCache()); } /** * Copy an value deeply as much as possible. */ -export function copy(value: Value): Value { - return performCopy(value, getObjectCloneLoose, getArrayCloneLoose); -} +export const copy = createCopier(copyArrayLoose, copyObjectLoose); /** * Copy an value deeply as much as possible, where strict recreation of object properties * are maintained. All properties (including non-enumerable ones) are copied with their * original property descriptors on both objects and arrays. */ -export function copyStrict(value: Value): Value { - return performCopy(value, getObjectCloneStrict, getObjectCloneStrict); -} +export const copyStrict = createCopier(copyObjectStrict, copyObjectStrict); diff --git a/src/utils.ts b/src/utils.ts index d4e0c38..f70e4da 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,16 +9,7 @@ export interface Cache { } const { toString: toStringFunction } = Function.prototype; -const { - create, - defineProperty, - getOwnPropertyDescriptor, - getOwnPropertyNames, - getOwnPropertySymbols, -} = Object; -const { hasOwnProperty, propertyIsEnumerable } = Object.prototype; - -const SYMBOL_SUPPORT = typeof getOwnPropertySymbols === 'function'; +const { create } = Object; class LegacyCache { _keys: any[] = []; @@ -52,33 +43,6 @@ function createCacheModern(): Cache { export const createCache = typeof WeakMap !== 'undefined' ? createCacheModern : createCacheLegacy; -function getStrictPropertiesModern(object: any): Array { - return (getOwnPropertyNames(object) as Array).concat( - getOwnPropertySymbols(object) - ); -} - -const getStrictProperties = SYMBOL_SUPPORT - ? getStrictPropertiesModern - : getOwnPropertyNames; - -export function getArrayCloneLoose( - array: any[], - prototype: any, - handleCopy: InternalCopier, - cache: Cache -) { - const clone = new prototype.constructor(); - - cache.set(array, clone); - - for (let index: number = 0, length = array.length; index < length; ++index) { - clone[index] = handleCopy(array[index], cache); - } - - return clone; -} - /** * Get an empty version of the object with the same prototype it has. */ @@ -102,122 +66,6 @@ export function getCleanClone(prototype: any): any { return create(prototype); } -function getObjectCloneLooseLegacy( - object: Value, - prototype: any, - handleCopy: InternalCopier, - cache: Cache -): Value { - const clone: any = getCleanClone(prototype); - - // set in the cache immediately to be able to reuse the object recursively - cache.set(object, clone); - - for (const key in object) { - if (hasOwnProperty.call(object, key)) { - clone[key] = handleCopy(object[key], cache); - } - } - - return clone; -} - -function getObjectCloneLooseModern( - object: Value, - prototype: any, - handleCopy: InternalCopier, - cache: Cache -): Value { - const clone: any = getCleanClone(prototype); - - // set in the cache immediately to be able to reuse the object recursively - cache.set(object, clone); - - for (const key in object) { - if (hasOwnProperty.call(object, key)) { - clone[key] = handleCopy(object[key], cache); - } - } - - const symbols: symbol[] = 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; -} - -/** - * 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: Value, - prototype: any, - handleCopy: InternalCopier, - cache: Cache -): Value { - const clone = getCleanClone(prototype); - - // set in the cache immediately to be able to reuse the object recursively - cache.set(object, clone); - - const properties = getStrictProperties(object); - - for ( - let index = 0, length = properties.length, property, descriptor; - index < length; - ++index - ) { - property = properties[index]; - - if (property !== 'callee' && property !== 'caller') { - descriptor = getOwnPropertyDescriptor(object, property); - - if (descriptor) { - // Only clone the value if actually a value, not a getter / setter. - if (!descriptor.get && !descriptor.set) { - descriptor.value = handleCopy((object as any)[property], cache); - } - - try { - defineProperty(clone, property, descriptor); - } catch (error) { - // Tee above can fail on node in edge cases, so fall back to the loose assignment. - clone[property] = descriptor.value; - } - } else { - // In extra edge cases where the property descriptor cannot be retrived, fall back to - // the loose assignment. - clone[property] = handleCopy((object as any)[property], cache); - } - } - } - - return clone; -} - function getRegExpFlagsLegacy(regExp: RegExp): string { let flags = '';