├── .yarnrc.yml ├── .gitignore ├── dev ├── index.html ├── index.ts └── App.ts ├── eslint.config.js ├── config ├── rollup.config.js ├── vite.config.ts ├── vitest.config.ts ├── release-it │ ├── stable.json │ ├── rc.json │ ├── beta.json │ └── alpha.json ├── eslint.config.js └── types │ ├── cjs.json │ ├── es.json │ └── umd.json ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── src ├── utils.ts ├── index.ts ├── options.ts └── copier.ts ├── index.d.ts ├── package.json ├── __tests__ ├── utils.test.ts ├── copier.test.ts └── index.test.ts ├── benchmark └── index.js ├── CHANGELOG.md └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | coverage 3 | dist 4 | node_modules -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from './config/eslint.config.js'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@planttheidea/build-tools'; 2 | 3 | export default createRollupConfig(); 4 | -------------------------------------------------------------------------------- /config/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createViteConfig } from '@planttheidea/build-tools'; 2 | 3 | export default createViteConfig({ 4 | development: 'dev', 5 | }); 6 | -------------------------------------------------------------------------------- /config/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { createVitestConfig } from '@planttheidea/build-tools'; 2 | 3 | export default createVitestConfig({ 4 | react: false, 5 | source: 'src', 6 | }); 7 | -------------------------------------------------------------------------------- /config/release-it/stable.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": ["npm run format:check", "npm run typecheck", "npm run lint", "npm run test", "npm run build"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/release-it/rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": ["npm run format:check", "npm run typecheck", "npm run lint", "npm run test", "npm run build"] 8 | }, 9 | "npm": { 10 | "tag": "next" 11 | }, 12 | "preReleaseId": "rc" 13 | } 14 | -------------------------------------------------------------------------------- /config/release-it/beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": ["npm run format:check", "npm run typecheck", "npm run lint", "npm run test", "npm run build"] 8 | }, 9 | "npm": { 10 | "tag": "next" 11 | }, 12 | "preReleaseId": "beta" 13 | } 14 | -------------------------------------------------------------------------------- /config/release-it/alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": ["npm run format:check", "npm run typecheck", "npm run lint", "npm run test", "npm run build"] 8 | }, 9 | "npm": { 10 | "tag": "next" 11 | }, 12 | "preReleaseId": "alpha" 13 | } 14 | -------------------------------------------------------------------------------- /dev/index.ts: -------------------------------------------------------------------------------- 1 | import './App.ts'; 2 | 3 | document.body.style.backgroundColor = '#1d1d1d'; 4 | document.body.style.color = '#d5d5d5'; 5 | document.body.style.margin = '0px'; 6 | document.body.style.padding = '0px'; 7 | 8 | const div = document.createElement('div'); 9 | 10 | div.textContent = 'Check the console for details.'; 11 | 12 | document.body.appendChild(div); 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "checkIgnorePragma": true, 6 | "embeddedLanguageFormatting": "auto", 7 | "endOfLine": "lf", 8 | "experimentalTernaries": false, 9 | "experimentalOperatorPosition": "start", 10 | "jsxSingleQuote": false, 11 | "insertPragma": false, 12 | "objectWrap": "preserve", 13 | "printWidth": 120, 14 | "proseWrap": "always", 15 | "requirePragma": false, 16 | "quoteProps": "as-needed", 17 | "semi": true, 18 | "singleAttributePerLine": true, 19 | "singleQuote": true, 20 | "tabWidth": 2, 21 | "trailingComma": "all", 22 | "useTabs": false 23 | } 24 | -------------------------------------------------------------------------------- /config/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { createEslintConfig } from '@planttheidea/build-tools'; 2 | 3 | export default createEslintConfig({ 4 | config: 'config', 5 | configs: [ 6 | { 7 | rules: { 8 | '@typescript-eslint/no-non-null-assertion': 'off', 9 | '@typescript-eslint/no-unsafe-assignment': 'off', 10 | '@typescript-eslint/no-unsafe-call': 'off', 11 | '@typescript-eslint/no-unsafe-member-access': 'off', 12 | '@typescript-eslint/no-unsafe-return': 'off', 13 | '@typescript-eslint/prefer-for-of': 'off', 14 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 15 | }, 16 | }, 17 | ], 18 | development: 'dev', 19 | react: false, 20 | source: 'src', 21 | }); 22 | -------------------------------------------------------------------------------- /config/types/cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "emitDeclarationOnly": false, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "lib": ["ESNext", "DOM"], 9 | "module": "Node16", 10 | "moduleDetection": "force", 11 | "moduleResolution": "Node16", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noUncheckedIndexedAccess": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "inlineSources": true, 22 | "target": "ES2015", 23 | "verbatimModuleSyntax": true, 24 | "types": ["node"], 25 | "outDir": "../../dist/cjs" 26 | }, 27 | "exclude": ["**/node_modules/**", "**/__tests__/**"], 28 | "include": ["../../src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /config/types/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "emitDeclarationOnly": false, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "lib": ["ESNext", "DOM"], 9 | "module": "NodeNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "NodeNext", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noUncheckedIndexedAccess": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "inlineSources": true, 22 | "target": "ES2015", 23 | "verbatimModuleSyntax": true, 24 | "types": ["node"], 25 | "outDir": "../../dist/es" 26 | }, 27 | "exclude": ["**/node_modules/**", "**/__tests__/**"], 28 | "include": ["../../src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /config/types/umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "emitDeclarationOnly": false, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "lib": ["ESNext", "DOM"], 9 | "module": "ESNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "Bundler", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noUncheckedIndexedAccess": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "inlineSources": true, 22 | "target": "ES2015", 23 | "verbatimModuleSyntax": true, 24 | "types": ["node"], 25 | "outDir": "../../dist/umd" 26 | }, 27 | "exclude": ["**/node_modules/**", "**/__tests__/**"], 28 | "include": ["../../src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": false, 5 | "emitDeclarationOnly": false, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "lib": ["ESNext", "DOM"], 9 | "module": "NodeNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "NodeNext", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noUncheckedIndexedAccess": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "inlineSources": true, 22 | "target": "ES2015", 23 | "verbatimModuleSyntax": true, 24 | "types": ["node"], 25 | "baseUrl": "src", 26 | "outDir": "dist", 27 | "rootDir": "./" 28 | }, 29 | "exclude": ["**/node_modules/**", "dist/**/*"], 30 | "include": ["config/**/*.ts", "dev/**/*.ts", "src/**/*.ts", "__tests__/**/*.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tony Quetano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export interface Cache { 2 | has: (value: any) => boolean; 3 | set: (key: any, value: any) => void; 4 | get: (key: any) => any; 5 | } 6 | 7 | // eslint-disable-next-line @typescript-eslint/unbound-method 8 | const toStringFunction = Function.prototype.toString; 9 | // eslint-disable-next-line @typescript-eslint/unbound-method 10 | const toStringObject = Object.prototype.toString; 11 | 12 | /** 13 | * Get an empty version of the object with the same prototype it has. 14 | */ 15 | export function getCleanClone(prototype: any): any { 16 | if (!prototype) { 17 | return Object.create(null); 18 | } 19 | 20 | const Constructor = prototype.constructor; 21 | 22 | if (Constructor === Object) { 23 | return prototype === Object.prototype ? {} : Object.create(prototype as object | null); 24 | } 25 | 26 | if (Constructor && ~toStringFunction.call(Constructor).indexOf('[native code]')) { 27 | try { 28 | return new Constructor(); 29 | } catch { 30 | // Ignore 31 | } 32 | } 33 | 34 | return Object.create(prototype as object | null); 35 | } 36 | 37 | /** 38 | * Get the tag of the value passed, so that the correct copier can be used. 39 | */ 40 | export function getTag(value: any): string { 41 | const stringTag = value[Symbol.toStringTag]; 42 | 43 | if (stringTag) { 44 | return stringTag; 45 | } 46 | 47 | const type = toStringObject.call(value); 48 | 49 | return type.substring(8, type.length - 1); 50 | } 51 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface Cache { 2 | has: (value: any) => boolean; 3 | set: (key: any, value: any) => void; 4 | get: (key: any) => any; 5 | } 6 | 7 | type InternalCopier = (value: Value, state: State) => Value; 8 | interface State { 9 | Constructor: any; 10 | cache: Cache; 11 | copier: InternalCopier; 12 | prototype: any; 13 | } 14 | 15 | interface CopierMethods { 16 | array?: InternalCopier; 17 | arrayBuffer?: InternalCopier; 18 | asyncGenerator?: InternalCopier; 19 | blob?: InternalCopier; 20 | dataView?: InternalCopier; 21 | date?: InternalCopier; 22 | error?: InternalCopier; 23 | generator?: InternalCopier; 24 | map?: InternalCopier>; 25 | object?: InternalCopier>; 26 | regExp?: InternalCopier; 27 | set?: InternalCopier>; 28 | } 29 | interface CreateCopierOptions { 30 | createCache?: () => Cache; 31 | methods?: CopierMethods; 32 | strict?: boolean; 33 | } 34 | 35 | /** 36 | * Create a custom copier based on custom options for any of the following: 37 | * - `createCache` method to create a cache for copied objects 38 | * - custom copier `methods` for specific object types 39 | * - `strict` mode to copy all properties with their descriptors 40 | */ 41 | declare function createCopier(options?: CreateCopierOptions): (value: Value) => Value; 42 | /** 43 | * Copy an value deeply as much as possible, where strict recreation of object properties 44 | * are maintained. All properties (including non-enumerable ones) are copied with their 45 | * original property descriptors on both objects and arrays. 46 | */ 47 | declare const copyStrict: (value: Value) => Value; 48 | /** 49 | * Copy an value deeply as much as possible. 50 | */ 51 | declare const copy: (value: Value) => Value; 52 | 53 | export { copy, copyStrict, createCopier }; 54 | export type { CreateCopierOptions, State }; 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './copier.ts'; 2 | import { getOptions } from './options.js'; 3 | import type { CreateCopierOptions } from './options.ts'; 4 | import { getTag } from './utils.js'; 5 | 6 | export type { State } from './copier.ts'; 7 | export type { CreateCopierOptions } from './options.ts'; 8 | 9 | /** 10 | * Create a custom copier based on custom options for any of the following: 11 | * - `createCache` method to create a cache for copied objects 12 | * - custom copier `methods` for specific object types 13 | * - `strict` mode to copy all properties with their descriptors 14 | */ 15 | export function createCopier(options: CreateCopierOptions = {}) { 16 | const { createCache, copiers } = getOptions(options); 17 | const { Array: copyArray, Object: copyObject } = copiers; 18 | 19 | function copier(value: any, state: State): any { 20 | state.prototype = state.Constructor = undefined; 21 | 22 | if (!value || typeof value !== 'object') { 23 | return value; 24 | } 25 | 26 | if (state.cache.has(value)) { 27 | return state.cache.get(value); 28 | } 29 | 30 | state.prototype = Object.getPrototypeOf(value); 31 | // Using logical AND for speed, since optional chaining transforms to 32 | // a local variable usage. 33 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain 34 | state.Constructor = state.prototype && state.prototype.constructor; 35 | 36 | // plain objects 37 | if (!state.Constructor || state.Constructor === Object) { 38 | return copyObject(value as Record, state); 39 | } 40 | 41 | // arrays 42 | if (Array.isArray(value)) { 43 | return copyArray(value, state); 44 | } 45 | 46 | const tagSpecificCopier = copiers[getTag(value)]; 47 | 48 | if (tagSpecificCopier) { 49 | return tagSpecificCopier(value, state); 50 | } 51 | 52 | return typeof value.then === 'function' ? value : copyObject(value as Record, state); 53 | } 54 | 55 | return function copy(value: Value): Value { 56 | return copier(value, { 57 | Constructor: undefined, 58 | cache: createCache(), 59 | copier, 60 | prototype: undefined, 61 | }); 62 | }; 63 | } 64 | 65 | /** 66 | * Copy an value deeply as much as possible, where strict recreation of object properties 67 | * are maintained. All properties (including non-enumerable ones) are copied with their 68 | * original property descriptors on both objects and arrays. 69 | */ 70 | export const copyStrict = createCopier({ strict: true }); 71 | 72 | /** 73 | * Copy an value deeply as much as possible. 74 | */ 75 | export const copy = createCopier(); 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "tony_quetano@planttheidea.com", 3 | "browser": "dist/umd/index.js", 4 | "bugs": { 5 | "url": "https://github.com/planttheidea/fast-copy/issues" 6 | }, 7 | "contributors": [ 8 | "Dariusz Rzepka " 9 | ], 10 | "description": "A blazing fast deep object copier", 11 | "devDependencies": { 12 | "@planttheidea/build-tools": "^1.2.2", 13 | "@types/lodash": "^4.17.21", 14 | "@types/node": "^24.10.1", 15 | "@types/ramda": "^0.31.1", 16 | "@types/react": "^19.2.7", 17 | "@vitest/coverage-v8": "^4.0.15", 18 | "cli-table3": "^0.6.5", 19 | "clone": "^2.1.2", 20 | "deepclone": "^1.0.2", 21 | "eslint": "^9.39.1", 22 | "fast-clone": "^1.5.13", 23 | "lodash": "^4.17.21", 24 | "prettier": "^3.7.4", 25 | "ramda": "^0.32.0", 26 | "react": "^19.2.1", 27 | "react-dom": "^19.2.1", 28 | "release-it": "19.0.6", 29 | "rollup": "^4.53.3", 30 | "tinybench": "^6.0.0", 31 | "typescript": "^5.9.3", 32 | "vite": "^7.2.6", 33 | "vitest": "^4.0.15" 34 | }, 35 | "exports": { 36 | ".": { 37 | "import": { 38 | "types": "./dist/es/index.d.mts", 39 | "default": "./dist/es/index.mjs" 40 | }, 41 | "require": { 42 | "types": "./dist/cjs/index.d.cts", 43 | "default": "./dist/cjs/index.cjs" 44 | }, 45 | "default": { 46 | "types": "./dist/umd/index.d.ts", 47 | "default": "./dist/umd/index.js" 48 | } 49 | } 50 | }, 51 | "files": [ 52 | "dist", 53 | "CHANGELOG.md", 54 | "LICENSE", 55 | "README.md", 56 | "index.d.ts", 57 | "package.json" 58 | ], 59 | "homepage": "https://github.com/planttheidea/fast-copy#readme", 60 | "keywords": [ 61 | "clone", 62 | "deep", 63 | "copy", 64 | "fast" 65 | ], 66 | "license": "MIT", 67 | "main": "dist/cjs/index.cjs", 68 | "module": "dist/es/index.mjs", 69 | "name": "fast-copy", 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/planttheidea/fast-copy.git" 73 | }, 74 | "scripts": { 75 | "benchmark": "npm run build && node benchmark/index.js", 76 | "build": "npm run clean && npm run build:dist && npm run build:types", 77 | "build:dist": "NODE_ENV=production rollup -c config/rollup.config.js", 78 | "build:types": "pti fix-types -l dist", 79 | "clean": "rm -rf dist", 80 | "clean:cjs": "rm -rf dist/cjs", 81 | "clean:es": "rm -rf dist/es", 82 | "clean:esm": "rm -rf dist/esm", 83 | "clean:min": "rm -rf dist/min", 84 | "dev": "vite --config=config/vite.config.ts", 85 | "format": "prettier . --log-level=warn --write", 86 | "format:check": "prettier . --log-level=warn --check", 87 | "lint": "eslint --max-warnings=0", 88 | "lint:fix": "npm run lint -- --fix", 89 | "release:alpha": "release-it --config=config/release-it/alpha.json", 90 | "release:beta": "release-it --config=config/release-it/beta.json", 91 | "release:dry": "release-it --dry-run", 92 | "release:rc": "release-it --config=config/release-it/rc.json", 93 | "release:scripts": "npm run format:check && npm run typecheck && npm run lint && npm run test && npm run build", 94 | "release:stable": "release-it --config=config/release-it/stable.json", 95 | "start": "npm run dev", 96 | "test": "vitest run --config=config/vitest.config.ts", 97 | "typecheck": "tsc --noEmit" 98 | }, 99 | "sideEffects": false, 100 | "type": "module", 101 | "types": "./index.d.ts", 102 | "version": "4.0.2" 103 | } 104 | -------------------------------------------------------------------------------- /__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getCleanClone } from '../src/utils.js'; 3 | 4 | interface PlainObject { 5 | [key: string]: any; 6 | [index: number]: any; 7 | } 8 | 9 | describe('getCleanClone', () => { 10 | it('will return a pure object when there is no prototype', () => { 11 | const object = Object.create(null); 12 | 13 | const result = getCleanClone(Object.getPrototypeOf(object)); 14 | 15 | expect(result).not.toBe(object); 16 | expect(result).toEqual(object); 17 | 18 | expect(Object.getPrototypeOf(result)).toBe(null); 19 | }); 20 | 21 | it('will return a pure object when there is a prototype but no constructor', () => { 22 | const Empty = function () { 23 | // empty 24 | }; 25 | Empty.prototype = Object.create(null); 26 | 27 | // @ts-expect-error - Testing `fast-querystring` V8 optimization 28 | const object = new Empty(); 29 | 30 | const result = getCleanClone(Object.getPrototypeOf(object)); 31 | 32 | expect(result).not.toBe(object); 33 | expect(result).toEqual(object); 34 | 35 | expect(Object.getPrototypeOf(result)).toBe(Empty.prototype); 36 | }); 37 | 38 | it('will return a pure object when there is no __proto__ property', () => { 39 | const object: PlainObject = {}; 40 | 41 | object.__proto__ = null; 42 | 43 | const result = getCleanClone(Object.getPrototypeOf(object)); 44 | 45 | expect(result).not.toBe(object); 46 | expect(result).toEqual(object); 47 | 48 | expect(Object.getPrototypeOf(result)).toBe(null); 49 | }); 50 | 51 | it('will return an empty POJO when the object passed is a POJO', () => { 52 | const object = { foo: 'bar' }; 53 | 54 | const result = getCleanClone(Object.getPrototypeOf(object)); 55 | 56 | expect(result).not.toBe(object); 57 | expect(result).toEqual({}); 58 | 59 | expect(Object.getPrototypeOf(result)).toBe(Object.prototype); 60 | }); 61 | 62 | it('will return an empty object with custom prototype when the object created through Object.create()', () => { 63 | const object = Object.create({ 64 | // No need for body in test 65 | // eslint-disable-next-line @typescript-eslint/no-empty-function 66 | method() {}, 67 | }); 68 | 69 | object.foo = 'bar'; 70 | 71 | const result = getCleanClone(Object.getPrototypeOf(object)); 72 | 73 | expect(result).not.toBe(object); 74 | expect(result).toEqual({}); 75 | 76 | expect(Object.getPrototypeOf(result)).toBe(Object.getPrototypeOf(object)); 77 | }); 78 | 79 | it('will return an empty object with the given constructor when it is a global constructor', () => { 80 | const object = new Map(); 81 | 82 | const result = getCleanClone(Object.getPrototypeOf(object)); 83 | 84 | expect(result).not.toBe(object); 85 | expect(result).toEqual(new Map()); 86 | 87 | expect(Object.getPrototypeOf(result)).toBe(Map.prototype); 88 | }); 89 | 90 | it('will return an empty object with the custom prototype when it is a custom constructor', () => { 91 | class Foo { 92 | value: any; 93 | 94 | constructor(value: any) { 95 | this.value = value; 96 | } 97 | 98 | // No need for body in test 99 | // eslint-disable-next-line @typescript-eslint/no-empty-function 100 | method() {} 101 | } 102 | 103 | const object = new Foo('bar'); 104 | 105 | const result = getCleanClone(Object.getPrototypeOf(object)); 106 | 107 | expect(result).not.toBe(object); 108 | expect(result).toEqual(Object.create(Foo.prototype)); 109 | 110 | expect(Object.getPrototypeOf(result)).toBe(Foo.prototype); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3'; 2 | import clone from 'clone'; 3 | import deepclone from 'deepclone'; 4 | import fastClone from 'fast-clone'; 5 | import fastDeepclone from 'fast-deepclone'; 6 | import { copy as fastCopy, copyStrict as fastCopyStrict } from '../dist/es/index.mjs'; 7 | import lodashCloneDeep from 'lodash/cloneDeep.js'; 8 | import orderBy from 'lodash/orderBy.js'; 9 | import { clone as ramdaClone } from 'ramda'; 10 | import React from 'react'; 11 | import { Bench } from 'tinybench'; 12 | import { BIG_DATA } from './bigData.js'; 13 | 14 | function getResults(tasks) { 15 | const table = new Table({ 16 | head: ['Name', 'Ops / sec'], 17 | }); 18 | 19 | tasks.forEach(({ name, result }) => { 20 | table.push([name, +(result.throughput?.mean ?? 0).toFixed(6)]); 21 | }); 22 | 23 | return table.toString(); 24 | } 25 | 26 | class Foo { 27 | constructor(value) { 28 | this.value = value; 29 | } 30 | } 31 | 32 | const simpleObject = { 33 | boolean: true, 34 | nil: null, 35 | number: 123, 36 | string: 'foo', 37 | }; 38 | 39 | const complexObject = Object.assign({}, simpleObject, { 40 | array: ['foo', { bar: 'baz' }], 41 | arrayBuffer: new ArrayBuffer(8), 42 | buffer: Buffer.from('this is a test buffer'), 43 | dataView: new DataView(new ArrayBuffer(16)), 44 | date: new Date(), 45 | error: new Error('boom'), 46 | fn() { 47 | return 'foo'; 48 | }, 49 | map: new Map().set('foo', { bar: { baz: 'quz' } }), 50 | nan: NaN, 51 | object: { foo: { bar: 'baz' } }, 52 | promise: Promise.resolve('foo'), 53 | regexp: /foo/, 54 | set: new Set().add('foo').add({ bar: { baz: 'quz' } }), 55 | typedArray: new Uint8Array([12, 15]), 56 | undef: undefined, 57 | weakmap: new WeakMap([ 58 | [{}, 'foo'], 59 | [{}, 'bar'], 60 | ]), 61 | weakset: new WeakSet([{}, {}]), 62 | [Symbol('key')]: 'value', 63 | }); 64 | 65 | const circularObject = { 66 | deeply: { 67 | nested: { 68 | reference: {}, 69 | }, 70 | }, 71 | }; 72 | 73 | circularObject.deeply.nested.reference = circularObject; 74 | 75 | const specialObject = { 76 | foo: new Foo('value'), 77 | react: React.createElement('main', { 78 | children: [ 79 | React.createElement('h1', { children: 'Title' }), 80 | React.createElement('p', { children: 'Content' }), 81 | React.createElement('p', { children: 'Content' }), 82 | React.createElement('p', { children: 'Content' }), 83 | React.createElement('p', { children: 'Content' }), 84 | React.createElement('div', { 85 | children: [ 86 | React.createElement('div', { 87 | children: 'Item', 88 | style: { flex: '1 1 auto' }, 89 | }), 90 | React.createElement('div', { 91 | children: 'Item', 92 | style: { flex: '1 1 0' }, 93 | }), 94 | ], 95 | style: { display: 'flex' }, 96 | }), 97 | ], 98 | }), 99 | }; 100 | 101 | const methods = { 102 | clone, 103 | deepclone, 104 | 'fast-clone': fastClone, 105 | 'fast-copy': fastCopy, 106 | 'fast-copy (strict)': fastCopyStrict, 107 | // deactivated while it cannot build on linux 108 | 'fast-deepclone': fastDeepclone, 109 | 'lodash.cloneDeep': lodashCloneDeep, 110 | ramda: ramdaClone, 111 | }; 112 | 113 | const benches = { 114 | 'simple object': simpleObject, 115 | 'complex object': complexObject, 116 | 'big data object': BIG_DATA, 117 | 'circular object': circularObject, 118 | 'special values object': specialObject, 119 | }; 120 | 121 | async function run(name, object) { 122 | console.log(''); 123 | console.log(`Testing ${name}...`); 124 | 125 | const bench = new Bench({ iterations: 1000, name, time: 100 }); 126 | 127 | Object.entries(methods).forEach(([pkgName, fn]) => { 128 | bench.add(pkgName, () => { 129 | fn(object); 130 | }); 131 | }); 132 | 133 | await bench.run(); 134 | 135 | const tasks = orderBy( 136 | bench.tasks.filter(({ result }) => result), 137 | ({ result }) => result.throughput?.mean ?? 0, 138 | ['desc'], 139 | ); 140 | const table = getResults(tasks); 141 | 142 | console.log(table); 143 | console.log(`Fastest was "${tasks[0].name}".`); 144 | } 145 | 146 | for (const type in benches) { 147 | await run(type, benches[type]); 148 | } 149 | -------------------------------------------------------------------------------- /dev/App.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep.js'; 2 | import React from 'react'; 3 | import { copy, copyStrict, createCopier } from '../src/index.js'; 4 | 5 | // import '../benchmarks'; 6 | 7 | type PlainObject = Record; 8 | 9 | class Foo { 10 | value: any; 11 | 12 | constructor(value: any) { 13 | this.value = value; 14 | } 15 | } 16 | 17 | const object: PlainObject = { 18 | arguments: (function (_foo, _bar, _baz) { 19 | // Specifically testing arguments object 20 | // eslint-disable-next-line prefer-rest-params 21 | return arguments; 22 | })('foo', 'bar', 'baz'), 23 | array: ['foo', { bar: 'baz' }], 24 | arrayBuffer: new ArrayBuffer(8), 25 | blob: new Blob(['hey!'], { type: 'text/html' }), 26 | boolean: true, 27 | booleanConstructor: new Boolean(true), 28 | customPrototype: Object.create({ 29 | method() { 30 | return 'foo'; 31 | }, 32 | value: 'value', 33 | }), 34 | date: new Date(2000, 0, 1), 35 | dataView: new DataView(new ArrayBuffer(16)), 36 | deeply: { 37 | nested: { 38 | reference: {}, 39 | }, 40 | }, 41 | error: new Error('boom'), 42 | fn() { 43 | return 'foo'; 44 | }, 45 | foo: new Foo('value'), 46 | map: (() => { 47 | const map = new Map().set('foo', { bar: 'baz' }); 48 | 49 | // @ts-expect-error - Testing non-standard property on map. 50 | map.foo = 'bar'; 51 | 52 | return map; 53 | })(), 54 | nan: NaN, 55 | nil: null, 56 | number: 123, 57 | numberConstructor: new Number('123'), 58 | object: { foo: { bar: 'baz' } }, 59 | promise: Promise.resolve('foo'), 60 | react: React.createElement('main', { 61 | children: [ 62 | React.createElement('h1', { children: 'Title' }), 63 | React.createElement('p', { children: 'Content' }), 64 | React.createElement('p', { children: 'Content' }), 65 | React.createElement('p', { children: 'Content' }), 66 | React.createElement('p', { children: 'Content' }), 67 | React.createElement('div', { 68 | children: [ 69 | React.createElement('div', { 70 | children: 'Item', 71 | style: { flex: '1 1 auto' }, 72 | }), 73 | React.createElement('div', { 74 | children: 'Item', 75 | style: { flex: '1 1 0' }, 76 | }), 77 | ], 78 | style: { display: 'flex' }, 79 | }), 80 | ], 81 | }), 82 | regexp: /foo/gi, 83 | set: new Set().add('foo').add({ bar: 'baz' }), 84 | string: 'foo', 85 | stringConstructor: new String('foo'), 86 | symbol: Symbol('foo'), 87 | typedArray: new Uint8Array([12, 15]), 88 | undef: undefined, 89 | weakmap: new WeakMap([ 90 | [{}, 'foo'], 91 | [{}, 'bar'], 92 | ]), 93 | weakset: new WeakSet([{}, {}]), 94 | [Symbol('key')]: 'value', 95 | }; 96 | 97 | object.array.foo = 'bar'; 98 | 99 | Object.defineProperty(object.object, 'not configurable', { 100 | configurable: false, 101 | enumerable: true, 102 | value: 'not configurable', 103 | writable: true, 104 | }); 105 | 106 | Object.defineProperty(object.object, 'not enumerable', { 107 | configurable: true, 108 | enumerable: false, 109 | value: 'not enumerable', 110 | writable: true, 111 | }); 112 | 113 | Object.defineProperty(object.object, 'readonly', { 114 | enumerable: true, 115 | value: 'readonly', 116 | }); 117 | 118 | object.deeply.nested.reference = object; 119 | 120 | const copyShallow = createCopier({ 121 | methods: { 122 | array: (array: any[]) => [...array], 123 | map: (map: Map) => new Map(map.entries()), 124 | object: (object: object) => ({ ...object }), 125 | set: (set: Set) => new Set(set.values()), 126 | }, 127 | }); 128 | 129 | const copyOwnProperties = (value: Value, clone: Value) => 130 | Object.getOwnPropertyNames(value).reduce( 131 | (clone, property) => 132 | Object.defineProperty( 133 | clone, 134 | property, 135 | Object.getOwnPropertyDescriptor(value, property) ?? { 136 | configurable: true, 137 | enumerable: true, 138 | // @ts-expect-error - Allow indexing by string. 139 | value: clone[property], 140 | writable: true, 141 | }, 142 | ), 143 | clone, 144 | ); 145 | 146 | const copyStrictShallow = createCopier({ 147 | methods: { 148 | array: (array: any[]) => copyOwnProperties(array, []), 149 | map: (map: Map) => copyOwnProperties(map, new Map(map.entries())), 150 | object: (object: object) => copyOwnProperties(object, {}), 151 | set: (set: Set) => copyOwnProperties(set, new Set(set.values())), 152 | }, 153 | strict: true, 154 | }); 155 | 156 | console.group('fast-copy'); 157 | console.log('original', object); 158 | console.log('copy', copy(object)); 159 | console.log('copyStrict', copyStrict(object)); 160 | console.log('copyShallow', copyShallow(object)); 161 | console.log('copyStrictShallow', copyStrictShallow(object)); 162 | console.groupEnd(); 163 | 164 | console.group('lodash.cloneDeep'); 165 | console.log('original', object); 166 | console.log('copy', cloneDeep(object)); 167 | console.groupEnd(); 168 | -------------------------------------------------------------------------------- /__tests__/copier.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | import { 3 | copyArrayStrict, 4 | copyMapStrict, 5 | copyObjectLoose, 6 | copyObjectStrict, 7 | copyPrimitiveWrapper, 8 | copySetStrict, 9 | } from '../src/copier.js'; 10 | import { createDefaultCache } from '../src/options.js'; 11 | 12 | interface PlainObject { 13 | [key: string]: any; 14 | [index: number]: any; 15 | } 16 | 17 | describe('copyArrayStrict', () => { 18 | it('will copy both indices and explicit properties', () => { 19 | const object: any = ['foo', 'bar']; 20 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 21 | const cache = createDefaultCache(); 22 | const prototype = Object.getPrototypeOf(object); 23 | 24 | object.baz = 'baz'; 25 | 26 | const result = copyArrayStrict(object, { 27 | Constructor: prototype.constructor, 28 | cache, 29 | copier: mockCopier, 30 | prototype, 31 | }); 32 | 33 | expect(result).not.toBe(object); 34 | expect(result).toEqual(object); 35 | expect(result.baz).toBe(object.baz); 36 | }); 37 | }); 38 | 39 | describe('copyObjectLoose', () => { 40 | it('will create an object clone', () => { 41 | const object = { 42 | bar: { baz: 'quz' }, 43 | [Symbol('quz')]: 'blah', 44 | }; 45 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 46 | const cache = createDefaultCache(); 47 | const prototype = Object.getPrototypeOf(object); 48 | 49 | const result = copyObjectLoose(object, { 50 | Constructor: prototype.constructor, 51 | cache, 52 | copier: mockCopier, 53 | prototype, 54 | }); 55 | 56 | expect(result).not.toBe(object); 57 | expect(result).toEqual(object); 58 | 59 | expect(mockCopier).toHaveBeenCalledTimes(Object.keys(object).length + Object.getOwnPropertySymbols(object).length); 60 | }); 61 | }); 62 | 63 | describe('copyObjectStrict', () => { 64 | it('will create an object clone', () => { 65 | const object: PlainObject = { 66 | bar: { baz: 'quz' }, 67 | }; 68 | 69 | Object.defineProperty(object, 'foo', { 70 | value: 'bar', 71 | }); 72 | 73 | Object.defineProperty(object, Symbol('quz'), { 74 | enumerable: true, 75 | value: 'blah', 76 | }); 77 | 78 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 79 | const cache = createDefaultCache(); 80 | const prototype = Object.getPrototypeOf(object); 81 | 82 | const result = copyObjectStrict(object, { 83 | Constructor: prototype.constructor, 84 | cache, 85 | copier: mockCopier, 86 | prototype, 87 | }); 88 | 89 | expect(result).not.toBe(object); 90 | expect(result).toEqual(object); 91 | 92 | expect(mockCopier).toHaveBeenCalledTimes( 93 | Object.getOwnPropertyNames(object).length + Object.getOwnPropertySymbols(object).length, 94 | ); 95 | }); 96 | }); 97 | 98 | describe('copyMapStrict', () => { 99 | it('will copy both entries and explicit properties', () => { 100 | const object: any = new Map([ 101 | ['foo', 'foo'], 102 | ['bar', 'bar'], 103 | ]); 104 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 105 | const cache = createDefaultCache(); 106 | const prototype = Object.getPrototypeOf(object); 107 | 108 | object.baz = 'baz'; 109 | 110 | const result = copyMapStrict(object, { 111 | Constructor: prototype.constructor, 112 | cache, 113 | copier: mockCopier, 114 | prototype, 115 | }); 116 | 117 | expect(result).not.toBe(object); 118 | expect(result).toEqual(object); 119 | expect(result.baz).toBe(object.baz); 120 | }); 121 | }); 122 | 123 | describe('copyPrimitiveWrapper', () => { 124 | it('will create a copy of the value of the primitive in a new wrapper', () => { 125 | const boolean = new Boolean(true); 126 | const number = new Number('123'); 127 | const string = new String('foo'); 128 | 129 | [boolean, number, string].forEach((primitiveWrapper) => { 130 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 131 | const cache = createDefaultCache(); 132 | const prototype = Object.getPrototypeOf(primitiveWrapper); 133 | 134 | const result = copyPrimitiveWrapper(primitiveWrapper, { 135 | Constructor: prototype.constructor, 136 | cache, 137 | copier: mockCopier, 138 | prototype, 139 | }); 140 | 141 | expect(result).not.toBe(primitiveWrapper); 142 | expect(result).toEqual(primitiveWrapper); 143 | }); 144 | }); 145 | }); 146 | 147 | describe('copySetStrict', () => { 148 | it('will copy both values and explicit properties', () => { 149 | const object: any = new Set(['foo', 'bar']); 150 | const mockCopier = vi.fn().mockImplementation((arg) => arg); 151 | const cache = createDefaultCache(); 152 | const prototype = Object.getPrototypeOf(object); 153 | 154 | object.baz = 'baz'; 155 | 156 | const result = copySetStrict(object, { 157 | Constructor: prototype.constructor, 158 | cache, 159 | copier: mockCopier, 160 | prototype, 161 | }); 162 | 163 | expect(result).not.toBe(object); 164 | expect(result).toEqual(object); 165 | expect(result.baz).toBe(object.baz); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | copyArrayBuffer, 3 | copyArrayLoose, 4 | copyArrayStrict, 5 | copyBlob, 6 | copyDataView, 7 | copyDate, 8 | copyMapLoose, 9 | copyMapStrict, 10 | copyObjectLoose, 11 | copyObjectStrict, 12 | copyPrimitiveWrapper, 13 | copyRegExp, 14 | copySelf, 15 | copySetLoose, 16 | copySetStrict, 17 | } from './copier.js'; 18 | import type { InternalCopier } from './copier.ts'; 19 | import type { Cache } from './utils.ts'; 20 | 21 | export interface CopierMethods { 22 | array?: InternalCopier; 23 | arrayBuffer?: InternalCopier; 24 | asyncGenerator?: InternalCopier; 25 | blob?: InternalCopier; 26 | dataView?: InternalCopier; 27 | date?: InternalCopier; 28 | error?: InternalCopier; 29 | generator?: InternalCopier; 30 | map?: InternalCopier>; 31 | object?: InternalCopier>; 32 | regExp?: InternalCopier; 33 | set?: InternalCopier>; 34 | } 35 | 36 | interface Copiers { 37 | [key: string]: InternalCopier | undefined; 38 | 39 | Arguments: InternalCopier>; 40 | Array: InternalCopier; 41 | ArrayBuffer: InternalCopier; 42 | AsyncGenerator: InternalCopier; 43 | Blob: InternalCopier; 44 | // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 45 | Boolean: InternalCopier; 46 | DataView: InternalCopier; 47 | Date: InternalCopier; 48 | Error: InternalCopier; 49 | Float32Array: InternalCopier; 50 | Float64Array: InternalCopier; 51 | Generator: InternalCopier; 52 | 53 | Int8Array: InternalCopier; 54 | Int16Array: InternalCopier; 55 | Int32Array: InternalCopier; 56 | Map: InternalCopier>; 57 | // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 58 | Number: InternalCopier; 59 | Object: InternalCopier>; 60 | Promise: InternalCopier>; 61 | RegExp: InternalCopier; 62 | Set: InternalCopier>; 63 | // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 64 | String: InternalCopier; 65 | WeakMap: InternalCopier>; 66 | WeakSet: InternalCopier>; 67 | Uint8Array: InternalCopier; 68 | Uint8ClampedArray: InternalCopier; 69 | Uint16Array: InternalCopier; 70 | Uint32Array: InternalCopier; 71 | Uint64Array: InternalCopier; 72 | } 73 | 74 | export interface CreateCopierOptions { 75 | createCache?: () => Cache; 76 | methods?: CopierMethods; 77 | strict?: boolean; 78 | } 79 | 80 | export interface RequiredCreateCopierOptions extends Omit, 'methods'> { 81 | copiers: Copiers; 82 | methods: Required; 83 | } 84 | 85 | export function createDefaultCache(): Cache { 86 | return new WeakMap(); 87 | } 88 | 89 | export function getOptions({ 90 | createCache: createCacheOverride, 91 | methods: methodsOverride, 92 | strict, 93 | }: CreateCopierOptions): RequiredCreateCopierOptions { 94 | const defaultMethods = { 95 | array: strict ? copyArrayStrict : copyArrayLoose, 96 | arrayBuffer: copyArrayBuffer, 97 | asyncGenerator: copySelf, 98 | blob: copyBlob, 99 | dataView: copyDataView, 100 | date: copyDate, 101 | error: copySelf, 102 | generator: copySelf, 103 | map: strict ? copyMapStrict : copyMapLoose, 104 | object: strict ? copyObjectStrict : copyObjectLoose, 105 | regExp: copyRegExp, 106 | set: strict ? copySetStrict : copySetLoose, 107 | }; 108 | 109 | const methods = methodsOverride ? Object.assign(defaultMethods, methodsOverride) : defaultMethods; 110 | const copiers = getTagSpecificCopiers(methods); 111 | const createCache = createCacheOverride || createDefaultCache; 112 | 113 | // Extra safety check to ensure that object and array copiers are always provided, 114 | // avoiding runtime errors. 115 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 116 | if (!copiers.Object || !copiers.Array) { 117 | throw new Error('An object and array copier must be provided.'); 118 | } 119 | 120 | return { createCache, copiers, methods, strict: Boolean(strict) }; 121 | } 122 | 123 | /** 124 | * Get the copiers used for each specific object tag. 125 | */ 126 | export function getTagSpecificCopiers(methods: Required): Copiers { 127 | return { 128 | Arguments: methods.object, 129 | Array: methods.array, 130 | ArrayBuffer: methods.arrayBuffer, 131 | AsyncGenerator: methods.asyncGenerator, 132 | Blob: methods.blob, 133 | Boolean: copyPrimitiveWrapper, 134 | DataView: methods.dataView, 135 | Date: methods.date, 136 | Error: methods.error, 137 | Float32Array: methods.arrayBuffer, 138 | Float64Array: methods.arrayBuffer, 139 | Generator: methods.generator, 140 | Int8Array: methods.arrayBuffer, 141 | Int16Array: methods.arrayBuffer, 142 | Int32Array: methods.arrayBuffer, 143 | Map: methods.map, 144 | Number: copyPrimitiveWrapper, 145 | Object: methods.object, 146 | Promise: copySelf, 147 | RegExp: methods.regExp, 148 | Set: methods.set, 149 | String: copyPrimitiveWrapper, 150 | WeakMap: copySelf, 151 | WeakSet: copySelf, 152 | Uint8Array: methods.arrayBuffer, 153 | Uint8ClampedArray: methods.arrayBuffer, 154 | Uint16Array: methods.arrayBuffer, 155 | Uint32Array: methods.arrayBuffer, 156 | Uint64Array: methods.arrayBuffer, 157 | }; 158 | } 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fast-copy CHANGELOG 2 | 3 | ## 4.0.2 4 | 5 | - [#112](https://github.com/planttheidea/fast-copy/pull/112) - Prevent generators from attempting to be copied (fixes 6 | [#111](https://github.com/planttheidea/fast-copy/issues/111)) 7 | 8 | ## 4.0.1 9 | 10 | - [#110](https://github.com/planttheidea/fast-copy/pull/110) - Fix legacy types not aligning with types from build 11 | package 12 | 13 | ## 4.0.0 14 | 15 | ### BREAKING CHANGES 16 | 17 | - The default `copy` method is now a named export, and the default export has been removed. 18 | - Legacy environment support has been removed; `Symbol`, `WeakMap`, and `RegExp.prototype.flags` are now expected to be 19 | present. 20 | - `createCopier` now receives an object of options. The methods passed previously are namespaced under the `methods` key 21 | in that options object. 22 | - `createStrictCopier` has been removed; please use the `strict` option passed to `createCopier` 23 | 24 | ## 3.0.2 25 | 26 | - [#95](https://github.com/planttheidea/fast-copy/pull/95) - Add support for objects that have a prototype with no 27 | constructor 28 | 29 | ## 3.0.1 30 | 31 | - [#78](https://github.com/planttheidea/fast-copy/pull/78) - Work when running Node process with `--disable-proto=throw` 32 | (thanks [@castarco](https://github.com/castarco)) 33 | 34 | ## 3.0.0 35 | 36 | **Breaking changes** 37 | 38 | - Exports are now always named, so the `.default` suffix is required when accessing 39 | - CommonJS in Node => `const copy = require('fast-copy').default;` 40 | - UMD global via CDN => `const copy = globalThis['fast-copy'].default;` 41 | - `copy.strict` is no longer available; it is now available as the explicit `copyStrict` named import 42 | - Options have been removed 43 | - `isStrict` option has been replaced with importing the separate `copyStrict` method 44 | - `realm` has been removed entirely, as `instanceof` is no longer used internally 45 | - The `FastCopy` namespace in typings has been removed in favor of explicit import of available types 46 | 47 | **Enhancements** 48 | 49 | - Support `exports` option, to have bettern handling for different environments (ESM vs CJS vs UMD) and improve 50 | tree-shaking when supported 51 | - Can now create a custom copier (either standard or strict), allowing maximum performance for specific use-cases 52 | - Small speed improvements when handling certain object types 53 | 54 | **Bug fixes** 55 | 56 | - Correctly handle primitive wrappers, e.g. `new String('foo')` 57 | 58 | ## 2.1.7 59 | 60 | - Republish of [`2.1.6`](#216), as the release process failed mid-publish 61 | 62 | ## 2.1.6 63 | 64 | - Revert [#69](https://github.com/planttheidea/fast-copy/pull/69) and 65 | [#71](https://github.com/planttheidea/fast-copy/pull/71), as they broke the package for NodeJS consumption (will be 66 | reintroduced in v3, as breaking changes are required) 67 | 68 | ## 2.1.5 - DO NOT USE 69 | 70 | - Ensure `"type": "module"` is set to allow ESM in NodeJS to work 71 | [#71](https://github.com/planttheidea/fast-copy/pull/71) 72 | 73 | ## 2.1.4 - DO NOT USE 74 | 75 | - Provide `"exports"` definition in `package.json` [#69](https://github.com/planttheidea/fast-copy/pull/69) (thanks 76 | [@liteoood](https://github.com/ilteoood)) 77 | 78 | ## 2.1.3 79 | 80 | - Fix source maps not referencing source code [#65](https://github.com/planttheidea/fast-copy/pull/65) 81 | 82 | ## 2.1.2 83 | 84 | - Support `constructor` property override on object [#60](https://github.com/planttheidea/fast-copy/pull/60) 85 | - Provide better support for `constructor` override on non-plain object types 86 | [#61](https://github.com/planttheidea/fast-copy/pull/61) 87 | - Remove `tslint` in favor of `@typescript-eslint` [#62](https://github.com/planttheidea/fast-copy/pull/62) 88 | 89 | ## 2.1.1 90 | 91 | - Fix ESM-to-CommonJS issue when using TSC to consume [#37](https://github.com/planttheidea/fast-copy/issues/37) 92 | - Modify `Blob` cloning to use `blob.slice()` instead of `new Blob()` for speed 93 | 94 | ## 2.1.0 95 | 96 | - Support cloning `Blob` [#31](https://github.com/planttheidea/fast-copy/pull/31) (thanks 97 | [@fratzigner](https://github.com/fratzinger)) 98 | - Fix cloning descriptors that only are getters / setters in strict mode 99 | - Handle errors when defining properties in strict mode 100 | 101 | ## 2.0.5 102 | 103 | - Fix issue copying objects referenced multiple times in source [#28](https://github.com/planttheidea/fast-copy/pull/28) 104 | (thanks [@darkowic](https://github.com/darkowic)) 105 | 106 | ## 2.0.4 107 | 108 | - Cache length of arrays for faster iteration [#22](https://github.com/planttheidea/fast-copy/pull/22) 109 | - Update dev dependencies and types 110 | 111 | ## 2.0.3 112 | 113 | - Add safety to constructing native objects (fixes #19) 114 | 115 | ## 2.0.2 116 | 117 | - Manually coalesce options instead of use destructuring (performance) 118 | 119 | ## 2.0.1 120 | 121 | - Fix typings declarations - [#17](https://github.com/planttheidea/fast-copy/pull/17) 122 | 123 | ## 2.0.0 124 | 125 | - Rewrite in TypeScript 126 | - Add strict mode (for more accurate and thorough copying, at the expense of less performance) 127 | 128 | #### BREAKING CHANGES 129 | 130 | - Second parameter is now an object of [options](README.md#options) 131 | 132 | ## 1.2.4 133 | 134 | - Ensure `Date` copy uses realm-specific constructor 135 | 136 | ## 1.2.3 137 | 138 | - Support custom prototype applied to plain object via `Object.create()` 139 | 140 | ## 1.2.2 141 | 142 | - Support copy of extensions of native `Array` with alternative `push()` method 143 | 144 | ## 1.2.1 145 | 146 | - Under-the-hood optimizations per recommendations from #7 147 | 148 | ## 1.2.0 149 | 150 | - Add support for multiple realms 151 | 152 | ## 1.1.2 153 | 154 | - Optimize order of operations for common use cases 155 | 156 | ## 1.1.1 157 | 158 | - Fix cache using `WeakSet` when there was support for `WeakMap`s instead of `WeakSet`s (in case one was polyfilled but 159 | not the other) 160 | 161 | ## 1.1.0 162 | 163 | - Add TypeScript and FlowType bindings 164 | 165 | ## 1.0.1 166 | 167 | - Activate tree-shaking 168 | 169 | ## 1.0.0 170 | 171 | - Initial release 172 | -------------------------------------------------------------------------------- /src/copier.ts: -------------------------------------------------------------------------------- 1 | import { getCleanClone } from './utils.js'; 2 | import type { Cache } from './utils.ts'; 3 | 4 | export type InternalCopier = (value: Value, state: State) => Value; 5 | 6 | export interface State { 7 | Constructor: any; 8 | cache: Cache; 9 | copier: InternalCopier; 10 | prototype: any; 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/unbound-method 14 | const { hasOwnProperty, propertyIsEnumerable } = Object.prototype; 15 | 16 | function copyOwnDescriptor( 17 | original: Value, 18 | clone: Value, 19 | property: string | symbol, 20 | state: State, 21 | ): void { 22 | const ownDescriptor = Object.getOwnPropertyDescriptor(original, property) || { 23 | configurable: true, 24 | enumerable: true, 25 | value: original[property as keyof Value], 26 | writable: true, 27 | }; 28 | const descriptor = 29 | ownDescriptor.get || ownDescriptor.set 30 | ? ownDescriptor 31 | : { 32 | configurable: ownDescriptor.configurable, 33 | enumerable: ownDescriptor.enumerable, 34 | value: state.copier(ownDescriptor.value, state), 35 | writable: ownDescriptor.writable, 36 | }; 37 | 38 | try { 39 | Object.defineProperty(clone, property, descriptor); 40 | } catch { 41 | // The above can fail on node in extreme edge cases, so fall back to the loose assignment. 42 | clone[property as keyof Value] = descriptor.get ? descriptor.get() : descriptor.value; 43 | } 44 | } 45 | 46 | /** 47 | * Striclty copy all properties contained on the object. 48 | */ 49 | function copyOwnPropertiesStrict(value: Value, clone: Value, state: State): Value { 50 | const names = Object.getOwnPropertyNames(value); 51 | 52 | for (let index = 0; index < names.length; ++index) { 53 | copyOwnDescriptor(value, clone, names[index]!, state); 54 | } 55 | 56 | const symbols = Object.getOwnPropertySymbols(value); 57 | 58 | for (let index = 0; index < symbols.length; ++index) { 59 | copyOwnDescriptor(value, clone, symbols[index]!, state); 60 | } 61 | 62 | return clone; 63 | } 64 | 65 | /** 66 | * Deeply copy the indexed values in the array. 67 | */ 68 | export function copyArrayLoose(array: any[], state: State) { 69 | const clone = new state.Constructor(); 70 | 71 | // set in the cache immediately to be able to reuse the object recursively 72 | state.cache.set(array, clone); 73 | 74 | for (let index = 0; index < array.length; ++index) { 75 | clone[index] = state.copier(array[index], state); 76 | } 77 | 78 | return clone; 79 | } 80 | 81 | /** 82 | * Deeply copy the indexed values in the array, as well as any custom properties. 83 | */ 84 | export function copyArrayStrict(array: Value, state: State) { 85 | const clone = new state.Constructor() as Value; 86 | 87 | // set in the cache immediately to be able to reuse the object recursively 88 | state.cache.set(array, clone); 89 | 90 | return copyOwnPropertiesStrict(array, clone, state); 91 | } 92 | 93 | /** 94 | * Copy the contents of the ArrayBuffer. 95 | */ 96 | export function copyArrayBuffer(arrayBuffer: Value, _state: State): Value { 97 | return arrayBuffer.slice(0) as Value; 98 | } 99 | 100 | /** 101 | * Create a new Blob with the contents of the original. 102 | */ 103 | export function copyBlob(blob: Value, _state: State): Value { 104 | return blob.slice(0, blob.size, blob.type) as Value; 105 | } 106 | 107 | /** 108 | * Create a new DataView with the contents of the original. 109 | */ 110 | export function copyDataView(dataView: Value, state: State): Value { 111 | return new state.Constructor(copyArrayBuffer(dataView.buffer, state)); 112 | } 113 | 114 | /** 115 | * Create a new Date based on the time of the original. 116 | */ 117 | export function copyDate(date: Value, state: State): Value { 118 | return new state.Constructor(date.getTime()); 119 | } 120 | 121 | /** 122 | * Deeply copy the keys and values of the original. 123 | */ 124 | export function copyMapLoose>(map: Value, state: State): Value { 125 | const clone = new state.Constructor() as Value; 126 | 127 | // set in the cache immediately to be able to reuse the object recursively 128 | state.cache.set(map, clone); 129 | 130 | map.forEach((value, key) => { 131 | clone.set(key, state.copier(value, state)); 132 | }); 133 | 134 | return clone; 135 | } 136 | 137 | /** 138 | * Deeply copy the keys and values of the original, as well as any custom properties. 139 | */ 140 | export function copyMapStrict>(map: Value, state: State) { 141 | return copyOwnPropertiesStrict(map, copyMapLoose(map, state), state); 142 | } 143 | 144 | /** 145 | * Deeply copy the properties (keys and symbols) and values of the original. 146 | */ 147 | export function copyObjectLoose>(object: Value, state: State): Value { 148 | const clone = getCleanClone(state.prototype); 149 | 150 | // set in the cache immediately to be able to reuse the object recursively 151 | state.cache.set(object, clone); 152 | 153 | for (const key in object) { 154 | if (hasOwnProperty.call(object, key)) { 155 | clone[key] = state.copier(object[key], state); 156 | } 157 | } 158 | 159 | const symbols = Object.getOwnPropertySymbols(object); 160 | 161 | for (let index = 0; index < symbols.length; ++index) { 162 | const symbol = symbols[index]!; 163 | 164 | if (propertyIsEnumerable.call(object, symbol)) { 165 | clone[symbol] = state.copier((object as any)[symbol], state); 166 | } 167 | } 168 | 169 | return clone; 170 | } 171 | 172 | /** 173 | * Deeply copy the properties (keys and symbols) and values of the original, as well 174 | * as any hidden or non-enumerable properties. 175 | */ 176 | export function copyObjectStrict>(object: Value, state: State): Value { 177 | const clone = getCleanClone(state.prototype); 178 | 179 | // set in the cache immediately to be able to reuse the object recursively 180 | state.cache.set(object, clone); 181 | 182 | return copyOwnPropertiesStrict(object, clone, state); 183 | } 184 | 185 | /** 186 | * Create a new primitive wrapper from the value of the original. 187 | */ 188 | export function copyPrimitiveWrapper< 189 | // Specifically use the object constructor types 190 | // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 191 | Value extends Boolean | Number | String, 192 | >(primitiveObject: Value, state: State): Value { 193 | return new state.Constructor(primitiveObject.valueOf()); 194 | } 195 | 196 | /** 197 | * Create a new RegExp based on the value and flags of the original. 198 | */ 199 | export function copyRegExp(regExp: Value, state: State): Value { 200 | const clone = new state.Constructor(regExp.source, regExp.flags) as Value; 201 | 202 | clone.lastIndex = regExp.lastIndex; 203 | 204 | return clone; 205 | } 206 | 207 | /** 208 | * Return the original value (an identity function). 209 | * 210 | * @note 211 | * THis is used for objects that cannot be copied, such as WeakMap. 212 | */ 213 | export function copySelf(value: Value, _state: State): Value { 214 | return value; 215 | } 216 | 217 | /** 218 | * Deeply copy the values of the original. 219 | */ 220 | export function copySetLoose>(set: Value, state: State): Value { 221 | const clone = new state.Constructor() as Value; 222 | 223 | // set in the cache immediately to be able to reuse the object recursively 224 | state.cache.set(set, clone); 225 | 226 | set.forEach((value) => { 227 | clone.add(state.copier(value, state)); 228 | }); 229 | 230 | return clone; 231 | } 232 | 233 | /** 234 | * Deeply copy the values of the original, as well as any custom properties. 235 | */ 236 | export function copySetStrict>(set: Value, state: State): Value { 237 | return copyOwnPropertiesStrict(set, copySetLoose(set, state), state); 238 | } 239 | -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { copy, copyStrict } from '../src/index.js'; 4 | 5 | interface PlainObject { 6 | [key: string]: any; 7 | [index: number]: any; 8 | } 9 | 10 | function* generator() { 11 | yield 'foo'; 12 | } 13 | 14 | async function* asyncGenerator() { 15 | await Promise.resolve(); 16 | yield 'foo'; 17 | } 18 | 19 | const SIMPLE_TYPES: PlainObject = { 20 | boolean: true, 21 | error: new TypeError('boom'), 22 | fn() { 23 | return 'foo'; 24 | }, 25 | nan: NaN, 26 | nil: null, 27 | number: 123, 28 | promise: Promise.resolve('foo'), 29 | string: 'foo', 30 | undef: undefined, 31 | weakmap: new WeakMap([ 32 | [{}, 'foo'], 33 | [{}, 'bar'], 34 | ]), 35 | weakset: new WeakSet([{}, {}]), 36 | [Symbol('key')]: 'value', 37 | }; 38 | 39 | Object.defineProperties(SIMPLE_TYPES, { 40 | readonlyKey: { 41 | configurable: true, 42 | enumerable: false, 43 | value: 'readonly', 44 | writable: false, 45 | }, 46 | [Symbol('readonlySymbol')]: { 47 | configurable: true, 48 | enumerable: false, 49 | value: 'readonly', 50 | writable: false, 51 | }, 52 | }); 53 | 54 | const COMPLEX_TYPES: PlainObject = { 55 | arguments: (function (_foo, _bar, _baz) { 56 | // Specifically testing arguments object 57 | // eslint-disable-next-line prefer-rest-params 58 | return arguments; 59 | })('foo', 'bar', 'baz'), 60 | array: ['foo', { bar: 'baz' }], 61 | arrayBuffer: new ArrayBuffer(8), 62 | asyncGenerator: asyncGenerator(), 63 | blob: new Blob(['hey!'], { type: 'text/html' }), 64 | buffer: Buffer.from('this is a test buffer'), 65 | customPrototype: Object.create({ 66 | method() { 67 | return 'foo'; 68 | }, 69 | value: 'value', 70 | }), 71 | dataView: new DataView(new ArrayBuffer(16)), 72 | date: new Date(), 73 | float32Array: new Float32Array([1, 2]), 74 | float64Array: new Float64Array([3, 4]), 75 | generator: generator(), 76 | int8Array: new Int8Array([5, 6]), 77 | int16Array: new Int16Array([7, 8]), 78 | int32Array: new Int32Array([9, 10]), 79 | map: new Map().set('foo', { bar: { baz: 'quz' } }), 80 | object: { foo: { bar: 'baz' } }, 81 | regexp: /foo/, 82 | set: new Set().add('foo').add({ bar: { baz: 'quz' } }), 83 | uint8Array: new Uint8Array([11, 12]), 84 | uint8ClampedArray: new Uint8ClampedArray([13, 14]), 85 | uint16Array: new Uint16Array([15, 16]), 86 | uint32Array: new Uint32Array([17, 18]), 87 | }; 88 | 89 | Object.defineProperties(COMPLEX_TYPES, { 90 | readonlyKey: { 91 | configurable: true, 92 | enumerable: false, 93 | value: 'readonly', 94 | writable: false, 95 | }, 96 | [Symbol('readonlySymbol')]: { 97 | configurable: true, 98 | enumerable: false, 99 | value: 'readonly', 100 | writable: false, 101 | }, 102 | }); 103 | 104 | const CIRCULAR: PlainObject = { 105 | deeply: { 106 | nested: { 107 | reference: {}, 108 | }, 109 | }, 110 | other: { 111 | reference: {}, 112 | }, 113 | }; 114 | 115 | CIRCULAR.deeply.nested.reference = CIRCULAR; 116 | CIRCULAR.other.reference = CIRCULAR; 117 | 118 | class Foo { 119 | value: any; 120 | 121 | constructor(value: any) { 122 | this.value = value; 123 | } 124 | } 125 | 126 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 127 | class Bar { 128 | constructor(value: any) { 129 | this.constructor = value; 130 | } 131 | } 132 | 133 | const SPECIAL_TYPES: PlainObject = { 134 | foo: new Foo('value'), 135 | react: React.createElement('main', { 136 | children: [ 137 | React.createElement('h1', { children: 'Title' }), 138 | React.createElement('p', { children: 'Content' }), 139 | React.createElement('p', { children: 'Content' }), 140 | React.createElement('p', { children: 'Content' }), 141 | React.createElement('p', { children: 'Content' }), 142 | React.createElement('div', { 143 | children: [ 144 | React.createElement('div', { 145 | children: 'Item', 146 | style: { flex: '1 1 auto' }, 147 | }), 148 | React.createElement('div', { 149 | children: 'Item', 150 | style: { flex: '1 1 0' }, 151 | }), 152 | ], 153 | style: { display: 'flex' }, 154 | }), 155 | ], 156 | }), 157 | }; 158 | 159 | describe('copy', () => { 160 | it('will copy an empty object', () => { 161 | const object = {}; 162 | 163 | const result = copy(object); 164 | 165 | expect(result).not.toBe(object); 166 | expect(result).toEqual(object); 167 | }); 168 | 169 | it('will copy the simple types', () => { 170 | const result = copy(SIMPLE_TYPES); 171 | 172 | expect(result).not.toBe(SIMPLE_TYPES); 173 | expect(result).toEqual(SIMPLE_TYPES); 174 | 175 | const properties = ([] as Array).concat( 176 | Object.keys(SIMPLE_TYPES), 177 | Object.getOwnPropertySymbols(SIMPLE_TYPES).filter((symbol) => 178 | Object.prototype.propertyIsEnumerable.call(SIMPLE_TYPES, symbol), 179 | ), 180 | ); 181 | 182 | properties.forEach((property: string | symbol) => { 183 | // @ts-expect-error - Symbol not supported property type 184 | expect(result[property]).toEqual(SIMPLE_TYPES[property]); 185 | }); 186 | }); 187 | 188 | it('will copy the complex types', () => { 189 | const result = copy(COMPLEX_TYPES); 190 | 191 | expect(result).not.toBe(COMPLEX_TYPES); 192 | 193 | const complexTypes = { ...COMPLEX_TYPES }; 194 | complexTypes.arguments = { ...COMPLEX_TYPES.arguments }; 195 | 196 | const properties = [ 197 | ...Object.keys(COMPLEX_TYPES), 198 | ...Object.getOwnPropertySymbols(COMPLEX_TYPES).filter((symbol) => 199 | Object.prototype.propertyIsEnumerable.call(COMPLEX_TYPES, symbol), 200 | ), 201 | ]; 202 | 203 | properties.forEach((property: string | symbol) => { 204 | const value = result[property as string]; 205 | 206 | if (property === 'arguments') { 207 | expect(value.constructor).toBe(Object); 208 | expect({ ...value }).toEqual({ ...COMPLEX_TYPES[property] }); 209 | } else if (property === 'blob') { 210 | expect(value).toBeInstanceOf(Blob); 211 | expect(value.size).toBe(complexTypes[property].size); 212 | expect(value.type).toBe(complexTypes[property].type); 213 | } else if (property === 'customPrototype') { 214 | expect(Object.getPrototypeOf(value)).toBe(Object.getPrototypeOf(COMPLEX_TYPES[property])); 215 | expect(value).toEqual(COMPLEX_TYPES[property]); 216 | } else { 217 | // @ts-expect-error - Symbol not supported property type 218 | expect(value).toEqual(COMPLEX_TYPES[property]); 219 | } 220 | }); 221 | }); 222 | 223 | it('will copy the circular object', () => { 224 | const result = copy(CIRCULAR); 225 | 226 | expect(result).not.toBe(CIRCULAR); 227 | expect(result).toEqual(CIRCULAR); 228 | }); 229 | 230 | it('will copy the special types', () => { 231 | const result = copy(SPECIAL_TYPES); 232 | 233 | expect(result).not.toBe(SPECIAL_TYPES); 234 | expect(result).toEqual(SPECIAL_TYPES); 235 | }); 236 | 237 | it('will copy referenced objects', () => { 238 | const reusedObject = { 239 | name: 'I like trains!', 240 | }; 241 | 242 | const data = { 243 | a: reusedObject, 244 | b: reusedObject, 245 | array: [reusedObject, reusedObject], 246 | }; 247 | 248 | const result = copy(data); 249 | 250 | const cloneReusedObject = result.a; 251 | 252 | expect(result.a).not.toBe(reusedObject); 253 | expect(result.a).toEqual(reusedObject); 254 | expect(result.b).not.toBe(reusedObject); 255 | expect(result.b).toBe(cloneReusedObject); 256 | expect(result.array[0]).not.toBe(reusedObject); 257 | expect(result.array[0]).toBe(cloneReusedObject); 258 | expect(result.array[1]).not.toBe(reusedObject); 259 | expect(result.array[1]).toBe(cloneReusedObject); 260 | }); 261 | 262 | it('will copy a plain object with a constructor property', () => { 263 | const data = { 264 | constructor: 'I am unable to comply.', 265 | }; 266 | const result = copy(data); 267 | 268 | expect(result).not.toBe(data); 269 | expect(result).toEqual(data); 270 | expect(Object.getPrototypeOf(result)).toBe(Object.getPrototypeOf(data)); 271 | }); 272 | 273 | it('will copy a custom object with a constructor property', () => { 274 | const bar = new Bar('value'); 275 | const result = copy(bar); 276 | 277 | expect(result).not.toBe(bar); 278 | expect(result).toEqual(bar); 279 | expect(Object.getPrototypeOf(result)).toBe(Object.getPrototypeOf(bar)); 280 | }); 281 | 282 | it('will copy an array with a constructor property', () => { 283 | const data = ['foo']; 284 | 285 | // @ts-expect-error - Reassigning `constructor` to test extreme edge case. 286 | data.constructor = 'I am unable to comply.'; 287 | 288 | const result = copyStrict(data); 289 | 290 | expect(result).not.toBe(data); 291 | expect(result).toEqual(data); 292 | expect(Object.getPrototypeOf(result)).toBe(Object.getPrototypeOf(data)); 293 | }); 294 | }); 295 | 296 | describe('copyStrict', () => { 297 | it('will copy an empty object', () => { 298 | const object = {}; 299 | 300 | const result = copyStrict(object); 301 | 302 | expect(result).not.toBe(object); 303 | expect(result).toEqual(object); 304 | }); 305 | 306 | it('will copy the simple types', () => { 307 | const result = copyStrict(SIMPLE_TYPES); 308 | 309 | expect(result).not.toBe(SIMPLE_TYPES); 310 | expect(result).toEqual(SIMPLE_TYPES); 311 | 312 | const properties = ([] as Array).concat( 313 | Object.getOwnPropertyNames(SIMPLE_TYPES), 314 | Object.getOwnPropertySymbols(SIMPLE_TYPES), 315 | ); 316 | 317 | properties.forEach((property: string | symbol) => { 318 | // @ts-expect-error - Symbol not supported property type 319 | expect(result[property]).toEqual(SIMPLE_TYPES[property]); 320 | }); 321 | }); 322 | 323 | it('will copy the complex types', () => { 324 | const result = copyStrict(COMPLEX_TYPES); 325 | 326 | expect(result).not.toBe(COMPLEX_TYPES); 327 | 328 | const complexTypes = { ...COMPLEX_TYPES }; 329 | 330 | complexTypes.arguments = { ...COMPLEX_TYPES.arguments }; 331 | 332 | const properties = ([] as Array).concat( 333 | Object.getOwnPropertyNames(complexTypes), 334 | Object.getOwnPropertySymbols(complexTypes), 335 | ); 336 | 337 | properties.forEach((property: string | symbol) => { 338 | const value = result[property as string]; 339 | 340 | if (property === 'arguments') { 341 | expect(value.constructor).toBe(Object); 342 | expect({ ...value }).toEqual({ ...COMPLEX_TYPES[property] }); 343 | } else if (property === 'blob') { 344 | expect(value).toBeInstanceOf(Blob); 345 | expect(value.size).toBe(complexTypes[property].size); 346 | expect(value.type).toBe(complexTypes[property].type); 347 | } else if (property === 'customPrototype') { 348 | expect(Object.getPrototypeOf(value)).toBe(Object.getPrototypeOf(COMPLEX_TYPES[property])); 349 | expect(value).toEqual(COMPLEX_TYPES[property]); 350 | } else if (property === 'asyncGenerator' || property === 'generator') { 351 | expect(value).toBe(COMPLEX_TYPES[property]); 352 | } else { 353 | // @ts-expect-error - Symbol not supported property type 354 | expect(value).toEqual(COMPLEX_TYPES[property]); 355 | } 356 | }); 357 | }); 358 | 359 | it('will copy the circular object', () => { 360 | const result = copyStrict(CIRCULAR); 361 | 362 | expect(result).not.toBe(CIRCULAR); 363 | expect(result).toEqual(CIRCULAR); 364 | }); 365 | 366 | it('will copy the special types', () => { 367 | const result = copyStrict(SPECIAL_TYPES); 368 | 369 | expect(result).not.toBe(SPECIAL_TYPES); 370 | expect(result).toEqual(SPECIAL_TYPES); 371 | }); 372 | 373 | it('will copy referenced objects', () => { 374 | const reusedObject = { 375 | name: 'I like trains!', 376 | }; 377 | 378 | const data = { 379 | a: reusedObject, 380 | b: reusedObject, 381 | array: [reusedObject, reusedObject], 382 | }; 383 | 384 | const result = copyStrict(data); 385 | 386 | const cloneReusedObject = result.a; 387 | 388 | expect(result.a).not.toBe(reusedObject); 389 | expect(result.a).toEqual(reusedObject); 390 | expect(result.b).not.toBe(reusedObject); 391 | expect(result.b).toBe(cloneReusedObject); 392 | expect(result.array[0]).not.toBe(reusedObject); 393 | expect(result.array[0]).toBe(cloneReusedObject); 394 | expect(result.array[1]).not.toBe(reusedObject); 395 | expect(result.array[1]).toBe(cloneReusedObject); 396 | }); 397 | }); 398 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-copy 2 | 3 | 4 | 5 | 6 | 7 | A [blazing fast](#benchmarks) deep object copier 8 | 9 | ## Table of contents 10 | 11 | - [fast-copy](#fast-copy) 12 | - [Table of contents](#table-of-contents) 13 | - [Usage](#usage) 14 | - [API](#api) 15 | - [`copy`](#copy) 16 | - [`copyStrict`](#copystrict) 17 | - [`createCopier`](#createcopier) 18 | - [`createCache`](#createcache) 19 | - [`methods`](#methods) 20 | - [Copier state](#copier-state) 21 | - [`cache`](#cache) 22 | - [`copier`](#copier) 23 | - [`Constructor` / `prototype`](#constructor--prototype) 24 | - [`strict`](#strict) 25 | - [Types supported](#types-supported) 26 | - [Aspects of default copiers](#aspects-of-default-copiers) 27 | - [Error references are copied directly, instead of creating a new `*Error` object](#error-references-are-copied-directly-instead-of-creating-a-new-error-object) 28 | - [The constructor of the original object is used, instead of using known globals](#the-constructor-of-the-original-object-is-used-instead-of-using-known-globals) 29 | - [Benchmarks](#benchmarks) 30 | - [Simple objects](#simple-objects) 31 | - [Complex objects](#complex-objects) 32 | - [Big data](#big-data) 33 | - [Circular objects](#circular-objects) 34 | - [Special objects](#special-objects) 35 | 36 | ## Usage 37 | 38 | ```js 39 | import { copy } from 'fast-copy'; 40 | import { deepEqual } from 'fast-equals'; 41 | 42 | const object = { 43 | array: [123, { deep: 'value' }], 44 | map: new Map([ 45 | ['foo', {}], 46 | [{ bar: 'baz' }, 'quz'], 47 | ]), 48 | }; 49 | 50 | const copiedObject = copy(object); 51 | 52 | console.log(copiedObject === object); // false 53 | console.log(deepEqual(copiedObject, object)); // true 54 | ``` 55 | 56 | ## API 57 | 58 | ### `copy` 59 | 60 | Deeply copy the object passed. 61 | 62 | ```js 63 | import { copy } from 'fast-copy'; 64 | 65 | const copied = copy({ foo: 'bar' }); 66 | ``` 67 | 68 | ### `copyStrict` 69 | 70 | Deeply copy the object passed, but with additional strictness when replicating the original object: 71 | 72 | - Properties retain their original property descriptor 73 | - Non-enumerable keys are copied 74 | - Non-standard properties (e.g., keys on arrays / maps / sets) are copied 75 | 76 | ```js 77 | import { copyStrict } from 'fast-copy'; 78 | 79 | const object = { foo: 'bar' }; 80 | object.nonEnumerable = Object.defineProperty(object, 'bar', { 81 | enumerable: false, 82 | value: 'baz', 83 | }); 84 | 85 | const copied = copy(object); 86 | ``` 87 | 88 | **NOTE**: This method is significantly slower than [`copy`](#copy), so it is recommended to only use this when you have 89 | specific use-cases that require it. 90 | 91 | ### `createCopier` 92 | 93 | Create a custom copier based on the type-specific method overrides passed, as well as configuration options for how 94 | copies should be performed. This is useful if you want to squeeze out maximum performance, or perform something other 95 | than a standard deep copy. 96 | 97 | ```js 98 | import { createCopier } from 'fast-copy'; 99 | import { LRUCache } from 'lru-cache'; 100 | 101 | const copyShallowStrict = createCopier({ 102 | createCache: () => new LRUCache(), 103 | methods: { 104 | array: (array) => [...array], 105 | map: (map) => new Map(map.entries()), 106 | object: (object) => ({ ...object }), 107 | set: (set) => new Set(set.values()), 108 | }, 109 | strict: true, 110 | }); 111 | ``` 112 | 113 | #### `createCache` 114 | 115 | Method that creates the internal [`cache`](#cache) in the [Copier state](#copier-state). Defaults to creating a new 116 | `WeakMap` instance. 117 | 118 | #### `methods` 119 | 120 | Methods used for copying specific object types. A list of the methods and which object types they handle: 121 | 122 | - `array` => `Array` 123 | - `arrayBuffer`=> `ArrayBuffer`, `Float32Array`, `Float64Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Uint8Array`, 124 | `Uint8ClampedArray`, `Uint16Array`, `Uint32Array`, `Uint64Array` 125 | - `blob` => `Blob` 126 | - `dataView` => `DataView` 127 | - `date` => `Date` 128 | - `error` => `Error`, `AggregateError`, `EvalError`, `RangeError`, `ReferenceError`, `SyntaxError`, `TypeError`, 129 | `URIError` 130 | - `map` => `Map` 131 | - `object` => `Object`, or any custom constructor 132 | - `regExp` => `RegExp` 133 | - `set` => `Set` 134 | 135 | Each method has the following contract: 136 | 137 | ```js 138 | type InternalCopier = (value: Value, state: State) => Value; 139 | 140 | interface State { 141 | Constructor: any; 142 | cache: WeakMap; 143 | copier: InternalCopier; 144 | prototype: any; 145 | } 146 | ``` 147 | 148 | ##### Copier state 149 | 150 | ###### `cache` 151 | 152 | If you want to maintain circular reference handling, then you'll need the methods to handle cache population for future 153 | lookups: 154 | 155 | ```js 156 | function shallowlyCloneArray( 157 | value: Value, 158 | state: State 159 | ): Value { 160 | const clone = [...value]; 161 | 162 | state.cache.set(value, clone); 163 | 164 | return clone; 165 | } 166 | ``` 167 | 168 | ###### `copier` 169 | 170 | `copier` is provided for recursive calls with deeply-nested objects. 171 | 172 | ```js 173 | function deeplyCloneArray( 174 | value: Value, 175 | state: State 176 | ): Value { 177 | const clone = []; 178 | 179 | state.cache.set(value, clone); 180 | 181 | value.forEach((item) => state.copier(item, state)); 182 | 183 | return clone; 184 | } 185 | ``` 186 | 187 | Note above I am using `forEach` instead of a simple `map`. This is because it is highly recommended to store the clone 188 | in [`cache`](#cache) eagerly when deeply copying, so that nested circular references are handled correctly. 189 | 190 | ###### `Constructor` / `prototype` 191 | 192 | Both `Constructor` and `prototype` properties are only populated with complex objects that are not standard objects or 193 | arrays. This is mainly useful for custom subclasses of these globals, or maintaining custom prototypes of objects. 194 | 195 | ```js 196 | function deeplyCloneSubclassArray( 197 | value: Value, 198 | state: State 199 | ): Value { 200 | const clone = new state.Constructor(); 201 | 202 | state.cache.set(value, clone); 203 | 204 | value.forEach((item) => clone.push(item)); 205 | 206 | return clone; 207 | } 208 | 209 | function deeplyCloneCustomObject( 210 | value: Value, 211 | state: State 212 | ): Value { 213 | const clone = Object.create(state.prototype); 214 | 215 | state.cache.set(value, clone); 216 | 217 | Object.entries(value).forEach(([k, v]) => (clone[k] = v)); 218 | 219 | return clone; 220 | } 221 | ``` 222 | 223 | #### `strict` 224 | 225 | Enforces strict copying of properties, which includes properties that are not standard for that object. An example would 226 | be a named key on an array. 227 | 228 | **NOTE**: This creates a copier that is significantly slower than "loose" mode, so it is recommended to only use this 229 | when you have specific use-cases that require it. 230 | 231 | ## Types supported 232 | 233 | The following object types are deeply cloned when they are either properties on the object passed, or the object itself: 234 | 235 | - `Array` 236 | - `ArrayBuffer` 237 | - `Boolean` primitive wrappers (e.g., `new Boolean(true)`) 238 | - `Blob` 239 | - `Buffer` 240 | - `DataView` 241 | - `Date` 242 | - `Float32Array` 243 | - `Float64Array` 244 | - `Int8Array` 245 | - `Int16Array` 246 | - `Int32Array` 247 | - `Map` 248 | - `Number` primitive wrappers (e.g., `new Number(123)`) 249 | - `Object` 250 | - `RegExp` 251 | - `Set` 252 | - `String` primitive wrappers (e.g., `new String('foo')`) 253 | - `Uint8Array` 254 | - `Uint8ClampedArray` 255 | - `Uint16Array` 256 | - `Uint32Array` 257 | - `React` components 258 | - Custom constructors 259 | 260 | The following object types are copied directly, as they are either primitives, cannot be cloned, or the common use-case 261 | implementation does not expect cloning: 262 | 263 | - `AsyncFunction` 264 | - `AsyncGenerator` 265 | - `Boolean` primitives 266 | - `Error` 267 | - `Function` 268 | - `Generator` 269 | - `GeneratorFunction` 270 | - `Number` primitives 271 | - `Null` 272 | - `Promise` 273 | - `String` primitives 274 | - `Symbol` 275 | - `Undefined` 276 | - `WeakMap` 277 | - `WeakSet` 278 | 279 | Circular objects are supported out of the box. By default, a cache based on `WeakSet` is used, but if `WeakSet` is not 280 | available then a fallback is used. The benchmarks quoted below are based on use of `WeakSet`. 281 | 282 | ## Aspects of default copiers 283 | 284 | Inherently, what is considered a valid copy is subjective because of different requirements and use-cases. For this 285 | library, some decisions were explicitly made for the default copiers of specific object types, and those decisions are 286 | detailed below. If your use-cases require different handling, you can always create your own custom copier with 287 | [`createCopier`](#createcopier). 288 | 289 | ### Error references are copied directly, instead of creating a new `*Error` object 290 | 291 | While it would be relatively trivial to copy over the message and stack to a new object of the same `Error` subclass, it 292 | is a common practice to "override" the message or stack, and copies would not retain this mutation. As such, the 293 | original reference is copied. 294 | 295 | ### The constructor of the original object is used, instead of using known globals 296 | 297 | Starting in ES2015, native globals can be subclassed like any custom class. When copying, we explicitly reuse the 298 | constructor of the original object. However, the expectation is that these subclasses would have the same constructur 299 | signature as their native base class. This is a common community practice, but there is the possibility of inaccuracy if 300 | the contract differs. 301 | 302 | ## Benchmarks 303 | 304 | #### Simple objects 305 | 306 | _Small number of properties, all values are primitives_ 307 | 308 | ```bash 309 | ┌────────────────────┬────────────────┐ 310 | │ Name │ Ops / sec │ 311 | ├────────────────────┼────────────────┤ 312 | │ fast-copy │ 4606103.720559 │ 313 | ├────────────────────┼────────────────┤ 314 | │ lodash.cloneDeep │ 2575175.39241 │ 315 | ├────────────────────┼────────────────┤ 316 | │ clone │ 2172921.6353 │ 317 | ├────────────────────┼────────────────┤ 318 | │ ramda │ 1919715.448951 │ 319 | ├────────────────────┼────────────────┤ 320 | │ fast-clone │ 1576610.693318 │ 321 | ├────────────────────┼────────────────┤ 322 | │ deepclone │ 1173500.05884 │ 323 | ├────────────────────┼────────────────┤ 324 | │ fast-copy (strict) │ 1049310.47701 │ 325 | └────────────────────┴────────────────┘ 326 | Fastest was "fast-copy". 327 | ``` 328 | 329 | #### Complex objects 330 | 331 | _Large number of properties, values are a combination of primitives and complex objects_ 332 | 333 | ```bash 334 | ┌────────────────────┬───────────────┐ 335 | │ Name │ Ops / sec │ 336 | ├────────────────────┼───────────────┤ 337 | │ fast-copy │ 235511.4532 │ 338 | ├────────────────────┼───────────────┤ 339 | │ deepclone │ 142976.849406 │ 340 | ├────────────────────┼───────────────┤ 341 | │ clone │ 125026.837887 │ 342 | ├────────────────────┼───────────────┤ 343 | │ ramda │ 114216.98158 │ 344 | ├────────────────────┼───────────────┤ 345 | │ fast-clone │ 111388.215547 │ 346 | ├────────────────────┼───────────────┤ 347 | │ fast-copy (strict) │ 77683.900047 │ 348 | ├────────────────────┼───────────────┤ 349 | │ lodash.cloneDeep │ 71343.431983 │ 350 | └────────────────────┴───────────────┘ 351 | Fastest was "fast-copy". 352 | ``` 353 | 354 | #### Big data 355 | 356 | _Very large number of properties with high amount of nesting, mainly objects and arrays_ 357 | 358 | ```bash 359 | Testing big data object... 360 | ┌────────────────────┬────────────┐ 361 | │ Name │ Ops / sec │ 362 | ├────────────────────┼────────────┤ 363 | │ fast-copy │ 325.548627 │ 364 | ├────────────────────┼────────────┤ 365 | │ fast-clone │ 257.913886 │ 366 | ├────────────────────┼────────────┤ 367 | │ deepclone │ 158.228042 │ 368 | ├────────────────────┼────────────┤ 369 | │ lodash.cloneDeep │ 153.520966 │ 370 | ├────────────────────┼────────────┤ 371 | │ fast-copy (strict) │ 126.027381 │ 372 | ├────────────────────┼────────────┤ 373 | │ clone │ 123.383641 │ 374 | ├────────────────────┼────────────┤ 375 | │ ramda │ 35.507959 │ 376 | └────────────────────┴────────────┘ 377 | Fastest was "fast-copy". 378 | ``` 379 | 380 | #### Circular objects 381 | 382 | ```bash 383 | Testing circular object... 384 | ┌────────────────────┬────────────────┐ 385 | │ Name │ Ops / sec │ 386 | ├────────────────────┼────────────────┤ 387 | │ fast-copy │ 1344790.296938 │ 388 | ├────────────────────┼────────────────┤ 389 | │ deepclone │ 1127781.641192 │ 390 | ├────────────────────┼────────────────┤ 391 | │ lodash.cloneDeep │ 894679.711048 │ 392 | ├────────────────────┼────────────────┤ 393 | │ clone │ 892911.50594 │ 394 | ├────────────────────┼────────────────┤ 395 | │ fast-copy (strict) │ 821339.44828 │ 396 | ├────────────────────┼────────────────┤ 397 | │ ramda │ 615222.946985 │ 398 | ├────────────────────┼────────────────┤ 399 | │ fast-clone │ 0 │ 400 | └────────────────────┴────────────────┘ 401 | Fastest was "fast-copy". 402 | ``` 403 | 404 | #### Special objects 405 | 406 | _Custom constructors, React components, etc_ 407 | 408 | ```bash 409 | ┌────────────────────┬──────────────┐ 410 | │ Name │ Ops / sec │ 411 | ├────────────────────┼──────────────┤ 412 | │ fast-copy │ 86875.694416 │ 413 | ├────────────────────┼──────────────┤ 414 | │ clone │ 73525.671381 │ 415 | ├────────────────────┼──────────────┤ 416 | │ lodash.cloneDeep │ 63280.563976 │ 417 | ├────────────────────┼──────────────┤ 418 | │ fast-clone │ 52991.064016 │ 419 | ├────────────────────┼──────────────┤ 420 | │ ramda │ 31770.652317 │ 421 | ├────────────────────┼──────────────┤ 422 | │ deepclone │ 24253.795114 │ 423 | ├────────────────────┼──────────────┤ 424 | │ fast-copy (strict) │ 19112.538416 │ 425 | └────────────────────┴──────────────┘ 426 | Fastest was "fast-copy". 427 | ``` 428 | --------------------------------------------------------------------------------