├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── benchmark ├── fixture.json ├── index.js └── shallow.js ├── default.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── package.json ├── readme.md └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | node-version: [8.x, 10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run ci 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | .vscode 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .editorconfig 3 | coverage 4 | benchmark 5 | .travis.yml -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 "David Mark Clements " 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 9 | of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 12 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 13 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 14 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 15 | IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /benchmark/fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "WebInspector", 6 | "version": "537.36" 7 | }, 8 | "pages": [ 9 | { 10 | "startedDateTime": "2015-02-10T07:33:17.146Z", 11 | "id": "page_1", 12 | "title": "http://mockbin.com/", 13 | "pageTimings": { 14 | "onContentLoad": 627.4099349975586, 15 | "onLoad": 1266.5300369262695 16 | } 17 | } 18 | ], 19 | "entries": [ 20 | { 21 | "startedDateTime": "2015-02-10T07:33:17.146Z", 22 | "time": 181.59985542297363, 23 | "request": { 24 | "method": "GET", 25 | "url": "http://mockbin.com/", 26 | "httpVersion": "HTTP/1.1", 27 | "headers": [ 28 | { 29 | "name": "DNT", 30 | "value": "1" 31 | }, 32 | { 33 | "name": "Accept-Encoding", 34 | "value": "gzip, deflate, sdch" 35 | }, 36 | { 37 | "name": "Host", 38 | "value": "mockbin.com" 39 | }, 40 | { 41 | "name": "Connection", 42 | "value": "keep-alive" 43 | } 44 | ], 45 | "queryString": [], 46 | "cookies": [ 47 | { 48 | "name": "foo", 49 | "expires": "2015-02-10T07:33:17.146Z", 50 | "value": "bar", 51 | "httpOnly": false, 52 | "secure": false 53 | } 54 | ], 55 | "headersSize": 482, 56 | "bodySize": 0 57 | }, 58 | "response": { 59 | "status": 200, 60 | "statusText": "OK", 61 | "httpVersion": "HTTP/1.1", 62 | "headers": [ 63 | { 64 | "name": "X-Response-Time", 65 | "value": "3.419ms" 66 | }, 67 | { 68 | "name": "Date", 69 | "value": "Tue, 10 Feb 2015 07:33:16 GMT" 70 | }, 71 | { 72 | "name": "Vary", 73 | "value": "Accept, Accept-Encoding" 74 | }, 75 | { 76 | "name": "X-Powered-By", 77 | "value": "mockbin.com" 78 | }, 79 | { 80 | "name": "Transfer-Encoding", 81 | "value": "chunked" 82 | }, 83 | { 84 | "name": "Content-Type", 85 | "value": "text/html; charset=utf-8" 86 | }, 87 | { 88 | "name": "Content-Encoding", 89 | "value": "gzip" 90 | }, 91 | { 92 | "name": "Connection", 93 | "value": "keep-alive" 94 | } 95 | ], 96 | "cookies": [], 97 | "content": { 98 | "size": 30, 99 | "mimeType": "text/html", 100 | "compression": 0, 101 | "text": "ALL YOUR BASE ARE BELONG TO US" 102 | }, 103 | "redirectURL": "", 104 | "headersSize": 430, 105 | "bodySize": 30 106 | }, 107 | "cache": {}, 108 | "timings": { 109 | "blocked": 0.381000001652865, 110 | "dns": -1, 111 | "connect": -1, 112 | "send": 0.05899999996472599, 113 | "wait": 179.1829999983744, 114 | "receive": 1.9768554229816289, 115 | "ssl": -1 116 | }, 117 | "connection": "161767", 118 | "pageref": "page_1" 119 | } 120 | ] 121 | } 122 | } -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global structuredClone */ 4 | 5 | const bench = require('fastbench') 6 | const deepCopy = require('deep-copy') 7 | const lodashCloneDeep = require('lodash.clonedeep') 8 | const cloneDeep = require('clone-deep') 9 | const fastCopy = require('fast-copy').default 10 | const obj = require('./fixture.json') 11 | const clone = require('..') 12 | const copyFastestJsonCopy = require('fastest-json-copy').copy 13 | const plainObjectClone = require('plain-object-clone') 14 | const nanoCopy = require('nano-copy') 15 | const ramdaClone = require('ramda').clone 16 | const cloneDefaults = clone() 17 | const cloneProto = clone({ proto: true }) 18 | const cloneCircles = clone({ circles: true }) 19 | const cloneCirclesProto = clone({ circles: true, proto: true }) 20 | const max = 1000 21 | 22 | const run = bench([ 23 | function benchDeepCopy (cb) { 24 | for (let i = 0; i < max; i++) { 25 | deepCopy(obj) 26 | } 27 | setImmediate(cb) 28 | }, 29 | function benchLodashCloneDeep (cb) { 30 | for (let i = 0; i < max; i++) { 31 | lodashCloneDeep(obj) 32 | } 33 | setImmediate(cb) 34 | }, 35 | function benchCloneDeep (cb) { 36 | for (let i = 0; i < max; i++) { 37 | cloneDeep(obj) 38 | } 39 | setImmediate(cb) 40 | }, 41 | function benchFastCopy (cb) { 42 | for (let i = 0; i < max; i++) { 43 | fastCopy(obj) 44 | } 45 | setImmediate(cb) 46 | }, 47 | function benchFastestJsonCopy (cb) { 48 | for (let i = 0; i < max; i++) { 49 | copyFastestJsonCopy(obj) 50 | } 51 | setImmediate(cb) 52 | }, 53 | function benchPlainObjectClone (cb) { 54 | for (let i = 0; i < max; i++) { 55 | plainObjectClone(obj) 56 | } 57 | setImmediate(cb) 58 | }, 59 | function benchNanoCopy (cb) { 60 | for (let i = 0; i < max; i++) { 61 | nanoCopy(obj) 62 | } 63 | setImmediate(cb) 64 | }, 65 | function benchRamdaClone (cb) { 66 | for (let i = 0; i < max; i++) { 67 | ramdaClone(obj) 68 | } 69 | setImmediate(cb) 70 | }, 71 | function benchJsonParseJsonStringify (cb) { 72 | for (let i = 0; i < max; i++) { 73 | JSON.parse(JSON.stringify(obj)) 74 | } 75 | setImmediate(cb) 76 | }, 77 | function benchRfdc (cb) { 78 | for (let i = 0; i < max; i++) { 79 | cloneDefaults(obj) 80 | } 81 | setImmediate(cb) 82 | }, 83 | function benchRfdcProto (cb) { 84 | for (let i = 0; i < max; i++) { 85 | cloneProto(obj) 86 | } 87 | setImmediate(cb) 88 | }, 89 | function benchRfdcCircles (cb) { 90 | for (let i = 0; i < max; i++) { 91 | cloneCircles(obj) 92 | } 93 | setImmediate(cb) 94 | }, 95 | function benchRfdcCirclesProto (cb) { 96 | for (let i = 0; i < max; i++) { 97 | cloneCirclesProto(obj) 98 | } 99 | setImmediate(cb) 100 | }, 101 | function benchStructuredClone (cb) { 102 | for (let i = 0; i < max; i++) { 103 | structuredClone(obj) 104 | } 105 | setImmediate(cb) 106 | } 107 | ], 100) 108 | 109 | run(run) 110 | -------------------------------------------------------------------------------- /benchmark/shallow.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const bench = require('fastbench') 3 | const deepCopy = require('deep-copy') 4 | const lodashCloneDeep = require('lodash.clonedeep') 5 | const cloneDeep = require('clone-deep') 6 | const fastCopy = require('fast-copy').default 7 | const obj = { a: 'a', b: 'b', c: 'c' } 8 | const clone = require('..') 9 | const copyFastestJsonCopy = require('fastest-json-copy').copy 10 | const plainObjectClone = require('plain-object-clone') 11 | const nanoCopy = require('nano-copy') 12 | const ramdaClone = require('ramda').clone 13 | const cloneDefaults = clone() 14 | const cloneProto = clone({ proto: true }) 15 | const cloneCircles = clone({ circles: true }) 16 | const cloneCirclesProto = clone({ circles: true, proto: true }) 17 | const max = 1000 18 | /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^x$" }] */ 19 | const run = bench([ 20 | function benchDeepCopy (cb) { 21 | for (let i = 0; i < max; i++) { 22 | deepCopy(obj) 23 | } 24 | setImmediate(cb) 25 | }, 26 | function benchLodashCloneDeep (cb) { 27 | for (let i = 0; i < max; i++) { 28 | lodashCloneDeep(obj) 29 | } 30 | setImmediate(cb) 31 | }, 32 | function benchCloneDeep (cb) { 33 | for (let i = 0; i < max; i++) { 34 | cloneDeep(obj) 35 | } 36 | setImmediate(cb) 37 | }, 38 | function benchFastCopy (cb) { 39 | for (let i = 0; i < max; i++) { 40 | fastCopy(obj) 41 | } 42 | setImmediate(cb) 43 | }, 44 | function benchFastestJsonCopy (cb) { 45 | for (let i = 0; i < max; i++) { 46 | copyFastestJsonCopy(obj) 47 | } 48 | setImmediate(cb) 49 | }, 50 | function benchPlainObjectClone (cb) { 51 | for (let i = 0; i < max; i++) { 52 | plainObjectClone(obj) 53 | } 54 | setImmediate(cb) 55 | }, 56 | function benchNanoCopy (cb) { 57 | for (let i = 0; i < max; i++) { 58 | nanoCopy(obj) 59 | } 60 | setImmediate(cb) 61 | }, 62 | function benchRamdaClone (cb) { 63 | for (let i = 0; i < max; i++) { 64 | ramdaClone(obj) 65 | } 66 | setImmediate(cb) 67 | }, 68 | function benchObjectAssign (cb) { 69 | for (let i = 0; i < max; i++) { 70 | const x = Object.assign({}, obj) 71 | } 72 | setImmediate(cb) 73 | }, 74 | function benchObjectSpread (cb) { 75 | for (let i = 0; i < max; i++) { 76 | const x = { ...obj } 77 | } 78 | setImmediate(cb) 79 | }, 80 | function benchRfdc (cb) { 81 | for (let i = 0; i < max; i++) { 82 | cloneDefaults(obj) 83 | } 84 | setImmediate(cb) 85 | }, 86 | function benchRfdcProto (cb) { 87 | for (let i = 0; i < max; i++) { 88 | cloneProto(obj) 89 | } 90 | setImmediate(cb) 91 | }, 92 | function benchRfdcCircles (cb) { 93 | for (let i = 0; i < max; i++) { 94 | cloneCircles(obj) 95 | } 96 | setImmediate(cb) 97 | }, 98 | function benchRfdcCirclesProto (cb) { 99 | for (let i = 0; i < max; i++) { 100 | cloneCirclesProto(obj) 101 | } 102 | setImmediate(cb) 103 | } 104 | ], 100) 105 | 106 | run(run) 107 | -------------------------------------------------------------------------------- /default.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./index.js')() 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace rfdc { 2 | interface Options { 3 | proto?: boolean; 4 | circles?: boolean; 5 | constructorHandlers?: ConstructorHandlerConfig[]; 6 | } 7 | } 8 | type Constructor = {new(...args: any[]): T}; 9 | type ConstructorHandlerConfig = [Constructor, (o: T) => T]; 10 | 11 | declare function rfdc(options?: rfdc.Options): (input: T) => T; 12 | 13 | export = rfdc; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = rfdc 3 | 4 | function copyBuffer (cur) { 5 | if (cur instanceof Buffer) { 6 | return Buffer.from(cur) 7 | } 8 | 9 | return new cur.constructor(cur.buffer.slice(), cur.byteOffset, cur.length) 10 | } 11 | 12 | function rfdc (opts) { 13 | opts = opts || {} 14 | if (opts.circles) return rfdcCircles(opts) 15 | 16 | const constructorHandlers = new Map() 17 | constructorHandlers.set(Date, (o) => new Date(o)) 18 | constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) 19 | constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) 20 | if (opts.constructorHandlers) { 21 | for (const handler of opts.constructorHandlers) { 22 | constructorHandlers.set(handler[0], handler[1]) 23 | } 24 | } 25 | 26 | let handler = null 27 | 28 | return opts.proto ? cloneProto : clone 29 | 30 | function cloneArray (a, fn) { 31 | const keys = Object.keys(a) 32 | const a2 = new Array(keys.length) 33 | for (let i = 0; i < keys.length; i++) { 34 | const k = keys[i] 35 | const cur = a[k] 36 | if (typeof cur !== 'object' || cur === null) { 37 | a2[k] = cur 38 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 39 | a2[k] = handler(cur, fn) 40 | } else if (ArrayBuffer.isView(cur)) { 41 | a2[k] = copyBuffer(cur) 42 | } else { 43 | a2[k] = fn(cur) 44 | } 45 | } 46 | return a2 47 | } 48 | 49 | function clone (o) { 50 | if (typeof o !== 'object' || o === null) return o 51 | if (Array.isArray(o)) return cloneArray(o, clone) 52 | if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { 53 | return handler(o, clone) 54 | } 55 | const o2 = {} 56 | for (const k in o) { 57 | if (Object.hasOwnProperty.call(o, k) === false) continue 58 | const cur = o[k] 59 | if (typeof cur !== 'object' || cur === null) { 60 | o2[k] = cur 61 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 62 | o2[k] = handler(cur, clone) 63 | } else if (ArrayBuffer.isView(cur)) { 64 | o2[k] = copyBuffer(cur) 65 | } else { 66 | o2[k] = clone(cur) 67 | } 68 | } 69 | return o2 70 | } 71 | 72 | function cloneProto (o) { 73 | if (typeof o !== 'object' || o === null) return o 74 | if (Array.isArray(o)) return cloneArray(o, cloneProto) 75 | if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { 76 | return handler(o, cloneProto) 77 | } 78 | const o2 = {} 79 | for (const k in o) { 80 | const cur = o[k] 81 | if (typeof cur !== 'object' || cur === null) { 82 | o2[k] = cur 83 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 84 | o2[k] = handler(cur, cloneProto) 85 | } else if (ArrayBuffer.isView(cur)) { 86 | o2[k] = copyBuffer(cur) 87 | } else { 88 | o2[k] = cloneProto(cur) 89 | } 90 | } 91 | return o2 92 | } 93 | } 94 | 95 | function rfdcCircles (opts) { 96 | const refs = [] 97 | const refsNew = [] 98 | 99 | const constructorHandlers = new Map() 100 | constructorHandlers.set(Date, (o) => new Date(o)) 101 | constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) 102 | constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) 103 | if (opts.constructorHandlers) { 104 | for (const handler of opts.constructorHandlers) { 105 | constructorHandlers.set(handler[0], handler[1]) 106 | } 107 | } 108 | 109 | let handler = null 110 | return opts.proto ? cloneProto : clone 111 | 112 | function cloneArray (a, fn) { 113 | const keys = Object.keys(a) 114 | const a2 = new Array(keys.length) 115 | for (let i = 0; i < keys.length; i++) { 116 | const k = keys[i] 117 | const cur = a[k] 118 | if (typeof cur !== 'object' || cur === null) { 119 | a2[k] = cur 120 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 121 | a2[k] = handler(cur, fn) 122 | } else if (ArrayBuffer.isView(cur)) { 123 | a2[k] = copyBuffer(cur) 124 | } else { 125 | const index = refs.indexOf(cur) 126 | if (index !== -1) { 127 | a2[k] = refsNew[index] 128 | } else { 129 | a2[k] = fn(cur) 130 | } 131 | } 132 | } 133 | return a2 134 | } 135 | 136 | function clone (o) { 137 | if (typeof o !== 'object' || o === null) return o 138 | if (Array.isArray(o)) return cloneArray(o, clone) 139 | if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { 140 | return handler(o, clone) 141 | } 142 | const o2 = {} 143 | refs.push(o) 144 | refsNew.push(o2) 145 | for (const k in o) { 146 | if (Object.hasOwnProperty.call(o, k) === false) continue 147 | const cur = o[k] 148 | if (typeof cur !== 'object' || cur === null) { 149 | o2[k] = cur 150 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 151 | o2[k] = handler(cur, clone) 152 | } else if (ArrayBuffer.isView(cur)) { 153 | o2[k] = copyBuffer(cur) 154 | } else { 155 | const i = refs.indexOf(cur) 156 | if (i !== -1) { 157 | o2[k] = refsNew[i] 158 | } else { 159 | o2[k] = clone(cur) 160 | } 161 | } 162 | } 163 | refs.pop() 164 | refsNew.pop() 165 | return o2 166 | } 167 | 168 | function cloneProto (o) { 169 | if (typeof o !== 'object' || o === null) return o 170 | if (Array.isArray(o)) return cloneArray(o, cloneProto) 171 | if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { 172 | return handler(o, cloneProto) 173 | } 174 | const o2 = {} 175 | refs.push(o) 176 | refsNew.push(o2) 177 | for (const k in o) { 178 | const cur = o[k] 179 | if (typeof cur !== 'object' || cur === null) { 180 | o2[k] = cur 181 | } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { 182 | o2[k] = handler(cur, cloneProto) 183 | } else if (ArrayBuffer.isView(cur)) { 184 | o2[k] = copyBuffer(cur) 185 | } else { 186 | const i = refs.indexOf(cur) 187 | if (i !== -1) { 188 | o2[k] = refsNew[i] 189 | } else { 190 | o2[k] = cloneProto(cur) 191 | } 192 | } 193 | } 194 | refs.pop() 195 | refsNew.pop() 196 | return o2 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import rfdc = require('.'); 3 | 4 | const clone = rfdc(); 5 | 6 | expectType(clone(5)); 7 | expectType<{ lorem: string }>(clone({ lorem: "ipsum" })); 8 | 9 | const cloneHandlers = rfdc({ 10 | constructorHandlers: [ 11 | [RegExp, (o) => new RegExp(o)], 12 | ], 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfdc", 3 | "version": "1.4.1", 4 | "description": "Really Fast Deep Clone", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "exports": { 8 | ".": "./index.js", 9 | "./default": "./default.js" 10 | }, 11 | "scripts": { 12 | "test": "tap -R min test && npm run lint", 13 | "bench": "node benchmark", 14 | "lint": "standard --fix", 15 | "cov": "tap --100 test", 16 | "cov-ui": "tap --coverage-report=html test", 17 | "ci": "standard && tap --100 --coverage-report=text-lcov test" 18 | }, 19 | "keywords": [ 20 | "object", 21 | "obj", 22 | "properties", 23 | "clone", 24 | "copy", 25 | "deep", 26 | "recursive", 27 | "key", 28 | "keys", 29 | "values", 30 | "prop", 31 | "deep-clone", 32 | "deepclone", 33 | "deep-copy", 34 | "deepcopy", 35 | "fast", 36 | "performance", 37 | "performant", 38 | "fastclone", 39 | "fastcopy", 40 | "fast-clone", 41 | "fast-deep-clone", 42 | "fast-copy", 43 | "fast-deep-copy" 44 | ], 45 | "author": "David Mark Clements ", 46 | "license": "MIT", 47 | "devDependencies": { 48 | "clone-deep": "^4.0.1", 49 | "deep-copy": "^1.4.2", 50 | "fast-copy": "^1.2.1", 51 | "fastbench": "^1.0.1", 52 | "fastest-json-copy": "^1.0.1", 53 | "lodash.clonedeep": "^4.5.0", 54 | "nano-copy": "^0.1.0", 55 | "plain-object-clone": "^1.1.0", 56 | "ramda": "^0.27.1", 57 | "standard": "^17.0.0", 58 | "tap": "^12.0.1", 59 | "tsd": "^0.7.4" 60 | }, 61 | "directories": { 62 | "test": "test" 63 | }, 64 | "dependencies": {}, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/davidmarkclements/rfdc.git" 68 | }, 69 | "bugs": { 70 | "url": "https://github.com/davidmarkclements/rfdc/issues" 71 | }, 72 | "homepage": "https://github.com/davidmarkclements/rfdc#readme" 73 | } 74 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rfdc 2 | 3 | Really Fast Deep Clone 4 | 5 | 6 | [![build status](https://img.shields.io/travis/davidmarkclements/rfdc.svg)](https://travis-ci.org/davidmarkclements/rfdc) 7 | [![coverage](https://img.shields.io/codecov/c/github/davidmarkclements/rfdc.svg)](https://codecov.io/gh/davidmarkclements/rfdc) 8 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 9 | 10 | ## Usage 11 | 12 | ```js 13 | const clone = require('rfdc')() 14 | clone({a: 1, b: {c: 2}}) // => {a: 1, b: {c: 2}} 15 | ``` 16 | 17 | ## API 18 | 19 | ### `require('rfdc')(opts = { proto: false, circles: false, constructorHandlers: [] }) => clone(obj) => obj2` 20 | 21 | #### `proto` option 22 | 23 | Copy prototype properties as well as own properties into the new object. 24 | 25 | It's marginally faster to allow enumerable properties on the prototype 26 | to be copied into the cloned object (not onto it's prototype, directly onto the object). 27 | 28 | To explain by way of code: 29 | 30 | ```js 31 | require('rfdc')({ proto: false })(Object.create({a: 1})) // => {} 32 | require('rfdc')({ proto: true })(Object.create({a: 1})) // => {a: 1} 33 | ``` 34 | 35 | Setting `proto` to `true` will provide an additional 2% performance boost. 36 | 37 | #### `circles` option 38 | 39 | Keeping track of circular references will slow down performance with an 40 | additional 25% overhead. Even if an object doesn't have any circular references, 41 | the tracking overhead is the cost. By default if an object with a circular 42 | reference is passed to `rfdc`, it will throw (similar to how `JSON.stringify` \ 43 | would throw). 44 | 45 | Use the `circles` option to detect and preserve circular references in the 46 | object. If performance is important, try removing the circular reference from 47 | the object (set to `undefined`) and then add it back manually after cloning 48 | instead of using this option. 49 | 50 | #### `constructorHandlers` option 51 | 52 | Sometimes consumers may want to add custom clone behaviour for particular classes 53 | (for example `RegExp` or `ObjectId`, which aren't supported out-of-the-box). 54 | 55 | This can be done by passing `constructorHandlers`, which takes an array of tuples, 56 | where the first item is the class to match, and the second item is a function that 57 | takes the input and returns a cloned output: 58 | 59 | ```js 60 | const clone = require('rfdc')({ 61 | constructorHandlers: [ 62 | [RegExp, (o) => new RegExp(o)], 63 | ] 64 | }) 65 | 66 | clone({r: /foo/}) // => {r: /foo/} 67 | ``` 68 | 69 | **NOTE**: For performance reasons, the handlers will only match an instance of the 70 | *exact* class (not a subclass). Subclasses will need to be added separately if they 71 | also need special clone behaviour. 72 | 73 | ### `default` import 74 | It is also possible to directly import the clone function with all options set 75 | to their default: 76 | 77 | ```js 78 | const clone = require("rfdc/default") 79 | clone({a: 1, b: {c: 2}}) // => {a: 1, b: {c: 2}} 80 | ``` 81 | 82 | ### Types 83 | 84 | `rfdc` clones all JSON types: 85 | 86 | * `Object` 87 | * `Array` 88 | * `Number` 89 | * `String` 90 | * `null` 91 | 92 | With additional support for: 93 | 94 | * `Date` (copied) 95 | * `undefined` (copied) 96 | * `Buffer` (copied) 97 | * `TypedArray` (copied) 98 | * `Map` (copied) 99 | * `Set` (copied) 100 | * `Function` (referenced) 101 | * `AsyncFunction` (referenced) 102 | * `GeneratorFunction` (referenced) 103 | * `arguments` (copied to a normal object) 104 | 105 | All other types have output values that match the output 106 | of `JSON.parse(JSON.stringify(o))`. 107 | 108 | For instance: 109 | 110 | ```js 111 | const rfdc = require('rfdc')() 112 | const err = Error() 113 | err.code = 1 114 | JSON.parse(JSON.stringify(e)) // {code: 1} 115 | rfdc(e) // {code: 1} 116 | 117 | JSON.parse(JSON.stringify({rx: /foo/})) // {rx: {}} 118 | rfdc({rx: /foo/}) // {rx: {}} 119 | ``` 120 | 121 | ## Benchmarks 122 | 123 | ```sh 124 | npm run bench 125 | ``` 126 | 127 | ``` 128 | benchDeepCopy*100: 671.675ms 129 | benchLodashCloneDeep*100: 1.574s 130 | benchCloneDeep*100: 936.792ms 131 | benchFastCopy*100: 822.668ms 132 | benchFastestJsonCopy*100: 363.898ms // See note below 133 | benchPlainObjectClone*100: 556.635ms 134 | benchNanoCopy*100: 770.234ms 135 | benchRamdaClone*100: 2.695s 136 | benchJsonParseJsonStringify*100: 2.290s // JSON.parse(JSON.stringify(obj)) 137 | benchRfdc*100: 412.818ms 138 | benchRfdcProto*100: 424.076ms 139 | benchRfdcCircles*100: 443.357ms 140 | benchRfdcCirclesProto*100: 465.053ms 141 | ``` 142 | 143 | It is true that [fastest-json-copy](https://www.npmjs.com/package/fastest-json-copy) may be faster, BUT it has such huge limitations that it is rarely useful. For example, it treats things like `Date` and `Map` instances the same as empty `{}`. It can't handle circular references. [plain-object-clone](https://www.npmjs.com/package/plain-object-clone) is also really limited in capability. 144 | 145 | ## Tests 146 | 147 | ```sh 148 | npm test 149 | ``` 150 | 151 | ``` 152 | 169 passing (342.514ms) 153 | ``` 154 | 155 | ### Coverage 156 | 157 | ```sh 158 | npm run cov 159 | ``` 160 | 161 | ``` 162 | ----------|----------|----------|----------|----------|-------------------| 163 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | 164 | ----------|----------|----------|----------|----------|-------------------| 165 | All files | 100 | 100 | 100 | 100 | | 166 | index.js | 100 | 100 | 100 | 100 | | 167 | ----------|----------|----------|----------|----------|-------------------| 168 | ``` 169 | 170 | ### `__proto__` own property copying 171 | 172 | `rfdc` works the same way as `Object.assign` when it comes to copying `['__proto__']` (e.g. when 173 | an object has an own property key called '__proto__'). It results in the target object 174 | prototype object being set per the value of the `['__proto__']` own property. 175 | 176 | For detailed write-up on how a way to handle this security-wise see https://www.fastify.io/docs/latest/Guides/Prototype-Poisoning/. 177 | 178 | ## Security 179 | 180 | Like `Object.assign()`, rdfc does not offer any protection against prototype poisoning. In other terms, 181 | if you clone an object that has a `__proto__` property, the target object will have the prototype set. 182 | 183 | ## License 184 | 185 | MIT 186 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const rfdc = require('..') 5 | const cloneDefault = require('../default') 6 | const clone = rfdc() 7 | const cloneProto = rfdc({ proto: true }) 8 | const cloneCircles = rfdc({ circles: true }) 9 | const cloneCirclesProto = rfdc({ circles: true, proto: true }) 10 | 11 | const rnd = (max) => Math.round(Math.random() * max) 12 | 13 | types(clone, 'default') 14 | types(cloneProto, 'proto option') 15 | types(cloneCircles, 'circles option') 16 | types(cloneCirclesProto, 'circles and proto option') 17 | 18 | test('default – does not copy proto properties', async ({ is }) => { 19 | is(clone(Object.create({ a: 1 })).a, undefined, 'value not copied') 20 | }) 21 | test('default – shorthand import', async ({ same }) => { 22 | same( 23 | clone(Object.create({ a: 1 })), 24 | cloneDefault(Object.create({ a: 1 })), 25 | 'import equals clone with default options' 26 | ) 27 | }) 28 | test('proto option – copies enumerable proto properties', async ({ is }) => { 29 | is(cloneProto(Object.create({ a: 1 })).a, 1, 'value copied') 30 | }) 31 | test('circles option - circular object', async ({ same, is, isNot }) => { 32 | const o = { nest: { a: 1, b: 2 } } 33 | o.circular = o 34 | same(cloneCircles(o), o, 'same values') 35 | isNot(cloneCircles(o), o, 'different objects') 36 | isNot(cloneCircles(o).nest, o.nest, 'different nested objects') 37 | const c = cloneCircles(o) 38 | is(c.circular, c, 'circular references point to copied parent') 39 | isNot(c.circular, o, 'circular references do not point to original parent') 40 | }) 41 | test('circles option – deep circular object', async ({ same, is, isNot }) => { 42 | const o = { nest: { a: 1, b: 2 } } 43 | o.nest.circular = o 44 | same(cloneCircles(o), o, 'same values') 45 | isNot(cloneCircles(o), o, 'different objects') 46 | isNot(cloneCircles(o).nest, o.nest, 'different nested objects') 47 | const c = cloneCircles(o) 48 | is(c.nest.circular, c, 'circular references point to copied parent') 49 | isNot( 50 | c.nest.circular, 51 | o, 52 | 'circular references do not point to original parent' 53 | ) 54 | }) 55 | test('circles option alone – does not copy proto properties', async ({ 56 | is 57 | }) => { 58 | is(cloneCircles(Object.create({ a: 1 })).a, undefined, 'value not copied') 59 | }) 60 | test('circles and proto option – copies enumerable proto properties', async ({ 61 | is 62 | }) => { 63 | is(cloneCirclesProto(Object.create({ a: 1 })).a, 1, 'value copied') 64 | }) 65 | test('circles and proto option - circular object', async ({ 66 | same, 67 | is, 68 | isNot 69 | }) => { 70 | const o = { nest: { a: 1, b: 2 } } 71 | o.circular = o 72 | same(cloneCirclesProto(o), o, 'same values') 73 | isNot(cloneCirclesProto(o), o, 'different objects') 74 | isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') 75 | const c = cloneCirclesProto(o) 76 | is(c.circular, c, 'circular references point to copied parent') 77 | isNot(c.circular, o, 'circular references do not point to original parent') 78 | }) 79 | test('circles and proto option – deep circular object', async ({ 80 | same, 81 | is, 82 | isNot 83 | }) => { 84 | const o = { nest: { a: 1, b: 2 } } 85 | o.nest.circular = o 86 | same(cloneCirclesProto(o), o, 'same values') 87 | isNot(cloneCirclesProto(o), o, 'different objects') 88 | isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') 89 | const c = cloneCirclesProto(o) 90 | is(c.nest.circular, c, 'circular references point to copied parent') 91 | isNot( 92 | c.nest.circular, 93 | o, 94 | 'circular references do not point to original parent' 95 | ) 96 | }) 97 | test('circles and proto option – deep circular array', async ({ 98 | same, 99 | is, 100 | isNot 101 | }) => { 102 | const o = { nest: [1, 2] } 103 | o.nest.push(o) 104 | same(cloneCirclesProto(o), o, 'same values') 105 | isNot(cloneCirclesProto(o), o, 'different objects') 106 | isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') 107 | const c = cloneCirclesProto(o) 108 | is(c.nest[2], c, 'circular references point to copied parent') 109 | isNot(c.nest[2], o, 'circular references do not point to original parent') 110 | }) 111 | test('custom constructor handler', async ({ same, ok, isNot }) => { 112 | class Foo { 113 | constructor (s) { 114 | this.s = s 115 | } 116 | } 117 | const data = { foo: new Foo('foo') } 118 | const cloned = rfdc({ constructorHandlers: [[Foo, (o) => new Foo(o.s)]] })(data) 119 | ok(cloned.foo instanceof Foo) 120 | same(cloned.foo.s, data.foo.s, 'same values') 121 | isNot(cloned.foo, data.foo, 'different objects') 122 | }) 123 | test('custom RegExp handler', async ({ same, ok, isNot }) => { 124 | const data = { regex: /foo/ } 125 | const cloned = rfdc({ constructorHandlers: [[RegExp, (o) => new RegExp(o)]] })(data) 126 | isNot(cloned.regex, data.regex, 'different objects') 127 | ok(cloned.regex.test('foo')) 128 | }) 129 | 130 | function types (clone, label) { 131 | test(label + ' – number', async ({ is }) => { 132 | is(clone(42), 42, 'same value') 133 | }) 134 | test(label + ' – string', async ({ is }) => { 135 | is(clone('str'), 'str', 'same value') 136 | }) 137 | test(label + ' – boolean', async ({ is }) => { 138 | is(clone(true), true, 'same value') 139 | }) 140 | test(label + ' – function', async ({ is }) => { 141 | const fn = () => {} 142 | is(clone(fn), fn, 'same function') 143 | }) 144 | test(label + ' – async function', async ({ is }) => { 145 | const fn = async () => {} 146 | is(clone(fn), fn, 'same function') 147 | }) 148 | test(label + ' – generator function', async ({ is }) => { 149 | const fn = function * () {} 150 | is(clone(fn), fn, 'same function') 151 | }) 152 | test(label + ' – date', async ({ is, isNot }) => { 153 | const date = new Date() 154 | is(+clone(date), +date, 'same value') 155 | isNot(clone(date), date, 'different object') 156 | }) 157 | test(label + ' – null', async ({ is }) => { 158 | is(clone(null), null, 'same value') 159 | }) 160 | test(label + ' – shallow object', async ({ same, isNot }) => { 161 | const o = { a: 1, b: 2 } 162 | same(clone(o), o, 'same values') 163 | isNot(clone(o), o, 'different object') 164 | }) 165 | test(label + ' – shallow array', async ({ same, isNot }) => { 166 | const o = [1, 2] 167 | same(clone(o), o, 'same values') 168 | isNot(clone(o), o, 'different arrays') 169 | }) 170 | test(label + ' – deep object', async ({ same, isNot }) => { 171 | const o = { nest: { a: 1, b: 2 } } 172 | same(clone(o), o, 'same values') 173 | isNot(clone(o), o, 'different objects') 174 | isNot(clone(o).nest, o.nest, 'different nested objects') 175 | }) 176 | test(label + ' – deep array', async ({ same, isNot }) => { 177 | const o = [{ a: 1, b: 2 }, [3]] 178 | same(clone(o), o, 'same values') 179 | isNot(clone(o), o, 'different arrays') 180 | isNot(clone(o)[0], o[0], 'different array elements') 181 | isNot(clone(o)[1], o[1], 'different array elements') 182 | }) 183 | test(label + ' – nested number', async ({ is }) => { 184 | is(clone({ a: 1 }).a, 1, 'same value') 185 | }) 186 | test(label + ' – nested string', async ({ is }) => { 187 | is(clone({ s: 'str' }).s, 'str', 'same value') 188 | }) 189 | test(label + ' – nested boolean', async ({ is }) => { 190 | is(clone({ b: true }).b, true, 'same value') 191 | }) 192 | test(label + ' – nested function', async ({ is }) => { 193 | const fn = () => {} 194 | is(clone({ fn }).fn, fn, 'same function') 195 | }) 196 | test(label + ' – nested async function', async ({ is }) => { 197 | const fn = async () => {} 198 | is(clone({ fn }).fn, fn, 'same function') 199 | }) 200 | test(label + ' – nested generator function', async ({ is }) => { 201 | const fn = function * () {} 202 | is(clone({ fn }).fn, fn, 'same function') 203 | }) 204 | test(label + ' – nested date', async ({ is, isNot }) => { 205 | const date = new Date() 206 | is(+clone({ d: date }).d, +date, 'same value') 207 | isNot(clone({ d: date }).d, date, 'different object') 208 | }) 209 | test(label + ' – nested date in array', async ({ is, isNot }) => { 210 | const date = new Date() 211 | is(+clone({ d: [date] }).d[0], +date, 'same value') 212 | isNot(clone({ d: [date] }).d[0], date, 'different object') 213 | is(+cloneCircles({ d: [date] }).d[0], +date, 'same value') 214 | isNot(cloneCircles({ d: [date] }).d, date, 'different object') 215 | }) 216 | test(label + ' – nested null', async ({ is }) => { 217 | is(clone({ n: null }).n, null, 'same value') 218 | }) 219 | test(label + ' – arguments', async ({ isNot, same }) => { 220 | function fn (...args) { 221 | same(clone(arguments), args, 'same values') 222 | isNot(clone(arguments), arguments, 'different object') 223 | } 224 | fn(1, 2, 3) 225 | }) 226 | test(`${label} copies buffers from object correctly`, async ({ ok, is, isNot }) => { 227 | const input = Date.now().toString(36) 228 | const inputBuffer = Buffer.from(input) 229 | const clonedBuffer = clone({ a: inputBuffer }).a 230 | ok(Buffer.isBuffer(clonedBuffer), 'cloned value is buffer') 231 | isNot(clonedBuffer, inputBuffer, 'cloned buffer is not same as input buffer') 232 | is(clonedBuffer.toString(), input, 'cloned buffer content is correct') 233 | }) 234 | test(`${label} copies buffers from arrays correctly`, async ({ ok, is, isNot }) => { 235 | const input = Date.now().toString(36) 236 | const inputBuffer = Buffer.from(input) 237 | const [clonedBuffer] = clone([inputBuffer]) 238 | ok(Buffer.isBuffer(clonedBuffer), 'cloned value is buffer') 239 | isNot(clonedBuffer, inputBuffer, 'cloned buffer is not same as input buffer') 240 | is(clonedBuffer.toString(), input, 'cloned buffer content is correct') 241 | }) 242 | test(`${label} copies TypedArrays from object correctly`, async ({ ok, is, isNot }) => { 243 | const [input1, input2] = [rnd(10), rnd(10)] 244 | const buffer = new ArrayBuffer(8) 245 | const int32View = new Int32Array(buffer) 246 | int32View[0] = input1 247 | int32View[1] = input2 248 | const cloned = clone({ a: int32View }).a 249 | ok(cloned instanceof Int32Array, 'cloned value is instance of class') 250 | isNot(cloned, int32View, 'cloned value is not same as input value') 251 | is(cloned[0], input1, 'cloned value content is correct') 252 | is(cloned[1], input2, 'cloned value content is correct') 253 | }) 254 | test(`${label} copies TypedArrays from array correctly`, async ({ ok, is, isNot }) => { 255 | const [input1, input2] = [rnd(10), rnd(10)] 256 | const buffer = new ArrayBuffer(16) 257 | const int32View = new Int32Array(buffer) 258 | int32View[0] = input1 259 | int32View[1] = input2 260 | const [cloned] = clone([int32View]) 261 | ok(cloned instanceof Int32Array, 'cloned value is instance of class') 262 | isNot(cloned, int32View, 'cloned value is not same as input value') 263 | is(cloned[0], input1, 'cloned value content is correct') 264 | is(cloned[1], input2, 'cloned value content is correct') 265 | }) 266 | test(`${label} copies complex TypedArrays`, async ({ ok, deepEqual, is, isNot }) => { 267 | const [input1, input2, input3] = [rnd(10), rnd(10), rnd(10)] 268 | const buffer = new ArrayBuffer(4) 269 | const view1 = new Int8Array(buffer, 0, 2) 270 | const view2 = new Int8Array(buffer, 2, 2) 271 | const view3 = new Int8Array(buffer) 272 | view1[0] = input1 273 | view2[0] = input2 274 | view3[3] = input3 275 | const cloned = clone({ view1, view2, view3 }) 276 | ok(cloned.view1 instanceof Int8Array, 'cloned value is instance of class') 277 | ok(cloned.view2 instanceof Int8Array, 'cloned value is instance of class') 278 | ok(cloned.view3 instanceof Int8Array, 'cloned value is instance of class') 279 | isNot(cloned.view1, view1, 'cloned value is not same as input value') 280 | isNot(cloned.view2, view2, 'cloned value is not same as input value') 281 | isNot(cloned.view3, view3, 'cloned value is not same as input value') 282 | deepEqual(Array.from(cloned.view1), [input1, 0], 'cloned value content is correct') 283 | deepEqual(Array.from(cloned.view2), [input2, input3], 'cloned value content is correct') 284 | deepEqual(Array.from(cloned.view3), [input1, 0, input2, input3], 'cloned value content is correct') 285 | }) 286 | test(`${label} - maps`, async ({ same, isNot }) => { 287 | const map = new Map([['a', 1]]) 288 | same(Array.from(clone(map)), [['a', 1]], 'same value') 289 | isNot(clone(map), map, 'different object') 290 | }) 291 | test(`${label} - sets`, async ({ same, isNot }) => { 292 | const set = new Set([1]) 293 | same(Array.from(clone(set)), [1]) 294 | isNot(clone(set), set, 'different object') 295 | }) 296 | test(`${label} - nested maps`, async ({ same, isNot }) => { 297 | const data = { m: new Map([['a', 1]]) } 298 | same(Array.from(clone(data).m), [['a', 1]], 'same value') 299 | isNot(clone(data).m, data.m, 'different object') 300 | }) 301 | test(`${label} - nested sets`, async ({ same, isNot }) => { 302 | const data = { s: new Set([1]) } 303 | same(Array.from(clone(data).s), [1], 'same value') 304 | isNot(clone(data).s, data.s, 'different object') 305 | }) 306 | } 307 | --------------------------------------------------------------------------------