diff --git a/index.js b/index.js index 41a49c1..a964561 100644 --- a/index.js +++ b/index.js @@ -11,8 +11,20 @@ function copyBuffer (cur) { function rfdc (opts) { opts = opts || {} - if (opts.circles) return rfdcCircles(opts) + + const constructorHandlers = new Map() + constructorHandlers.set(Date, (o) => new Date(o)) + constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) + constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) + if (opts.constructorHandlers) { + for (const handler of opts.constructorHandlers) { + constructorHandlers.set(handler[0], handler[1]) + } + } + + let handler = null + return opts.proto ? cloneProto : clone function cloneArray (a, fn) { @@ -23,8 +35,8 @@ function rfdc (opts) { const cur = a[k] if (typeof cur !== 'object' || cur === null) { a2[k] = cur - } else if (cur instanceof Date) { - a2[k] = new Date(cur) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + a2[k] = handler(cur, fn) } else if (ArrayBuffer.isView(cur)) { a2[k] = copyBuffer(cur) } else { @@ -36,22 +48,18 @@ function rfdc (opts) { function clone (o) { if (typeof o !== 'object' || o === null) return o - if (o instanceof Date) return new Date(o) if (Array.isArray(o)) return cloneArray(o, clone) - if (o instanceof Map) return new Map(cloneArray(Array.from(o), clone)) - if (o instanceof Set) return new Set(cloneArray(Array.from(o), clone)) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, clone) + } const o2 = {} for (const k in o) { if (Object.hasOwnProperty.call(o, k) === false) continue const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur instanceof Date) { - o2[k] = new Date(cur) - } else if (cur instanceof Map) { - o2[k] = new Map(cloneArray(Array.from(cur), clone)) - } else if (cur instanceof Set) { - o2[k] = new Set(cloneArray(Array.from(cur), clone)) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, clone) } else if (ArrayBuffer.isView(cur)) { o2[k] = copyBuffer(cur) } else { @@ -63,21 +71,17 @@ function rfdc (opts) { function cloneProto (o) { if (typeof o !== 'object' || o === null) return o - if (o instanceof Date) return new Date(o) if (Array.isArray(o)) return cloneArray(o, cloneProto) - if (o instanceof Map) return new Map(cloneArray(Array.from(o), cloneProto)) - if (o instanceof Set) return new Set(cloneArray(Array.from(o), cloneProto)) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, cloneProto) + } const o2 = {} for (const k in o) { const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur instanceof Date) { - o2[k] = new Date(cur) - } else if (cur instanceof Map) { - o2[k] = new Map(cloneArray(Array.from(cur), cloneProto)) - } else if (cur instanceof Set) { - o2[k] = new Set(cloneArray(Array.from(cur), cloneProto)) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, cloneProto) } else if (ArrayBuffer.isView(cur)) { o2[k] = copyBuffer(cur) } else { @@ -92,6 +96,17 @@ function rfdcCircles (opts) { const refs = [] const refsNew = [] + const constructorHandlers = new Map() + constructorHandlers.set(Date, (o) => new Date(o)) + constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) + constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) + if (opts.constructorHandlers) { + for (const handler of opts.constructorHandlers) { + constructorHandlers.set(handler[0], handler[1]) + } + } + + let handler = null return opts.proto ? cloneProto : clone function cloneArray (a, fn) { @@ -102,8 +117,8 @@ function rfdcCircles (opts) { const cur = a[k] if (typeof cur !== 'object' || cur === null) { a2[k] = cur - } else if (cur instanceof Date) { - a2[k] = new Date(cur) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + a2[k] = handler(cur, fn) } else if (ArrayBuffer.isView(cur)) { a2[k] = copyBuffer(cur) } else { @@ -120,10 +135,10 @@ function rfdcCircles (opts) { function clone (o) { if (typeof o !== 'object' || o === null) return o - if (o instanceof Date) return new Date(o) if (Array.isArray(o)) return cloneArray(o, clone) - if (o instanceof Map) return new Map(cloneArray(Array.from(o), clone)) - if (o instanceof Set) return new Set(cloneArray(Array.from(o), clone)) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, clone) + } const o2 = {} refs.push(o) refsNew.push(o2) @@ -132,12 +147,8 @@ function rfdcCircles (opts) { const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur instanceof Date) { - o2[k] = new Date(cur) - } else if (cur instanceof Map) { - o2[k] = new Map(cloneArray(Array.from(cur), clone)) - } else if (cur instanceof Set) { - o2[k] = new Set(cloneArray(Array.from(cur), clone)) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, clone) } else if (ArrayBuffer.isView(cur)) { o2[k] = copyBuffer(cur) } else { @@ -156,10 +167,10 @@ function rfdcCircles (opts) { function cloneProto (o) { if (typeof o !== 'object' || o === null) return o - if (o instanceof Date) return new Date(o) if (Array.isArray(o)) return cloneArray(o, cloneProto) - if (o instanceof Map) return new Map(cloneArray(Array.from(o), cloneProto)) - if (o instanceof Set) return new Set(cloneArray(Array.from(o), cloneProto)) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, cloneProto) + } const o2 = {} refs.push(o) refsNew.push(o2) @@ -167,12 +178,8 @@ function rfdcCircles (opts) { const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur instanceof Date) { - o2[k] = new Date(cur) - } else if (cur instanceof Map) { - o2[k] = new Map(cloneArray(Array.from(cur), cloneProto)) - } else if (cur instanceof Set) { - o2[k] = new Set(cloneArray(Array.from(cur), cloneProto)) + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, cloneProto) } else if (ArrayBuffer.isView(cur)) { o2[k] = copyBuffer(cur) } else { diff --git a/readme.md b/readme.md index 104bc81..892b11f 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ clone({a: 1, b: {c: 2}}) // => {a: 1, b: {c: 2}} ## API -### `require('rfdc')(opts = { proto: false, circles: false }) => clone(obj) => obj2` +### `require('rfdc')(opts = { proto: false, circles: false, constructorHandlers: [] }) => clone(obj) => obj2` #### `proto` option @@ -48,6 +48,29 @@ object. If performance is important, try removing the circular reference from the object (set to `undefined`) and then add it back manually after cloning instead of using this option. +#### `constructorHandlers` option + +Sometimes consumers may want to add custom clone behaviour for particular classes +(for example `RegExp` or `ObjectId`, which aren't supported out-of-the-box). + +This can be done by passing `constructorHandlers`, which takes an array of tuples, +where the first item is the class to match, and the second item is a function that +takes the input and returns a cloned output: + +```js +const clone = require('rfdc')({ + constructorHandlers: [ + [RegExp, (o) => new RegExp(o)], + ] +}) + +clone({r: /foo/}) // => {r: /foo/} +``` + +**NOTE**: For performance reasons, the handlers will only match an instance of the +*exact* class (not a subclass). Subclasses will need to be added separately if they +also need special clone behaviour. + ### `default` import It is also possible to directly import the clone function with all options set to their default: diff --git a/test/index.js b/test/index.js index a8e3cf4..5bdd9ac 100644 --- a/test/index.js +++ b/test/index.js @@ -108,6 +108,24 @@ test('circles and proto option – deep circular array', async ({ is(c.nest[2], c, 'circular references point to copied parent') isNot(c.nest[2], o, 'circular references do not point to original parent') }) +test('custom constructor handler', async ({ same, ok, isNot }) => { + class Foo { + constructor (s) { + this.s = s + } + } + const data = { foo: new Foo('foo') } + const cloned = rfdc({ constructorHandlers: [[Foo, (o) => new Foo(o.s)]] })(data) + ok(cloned.foo instanceof Foo) + same(cloned.foo.s, data.foo.s, 'same values') + isNot(cloned.foo, data.foo, 'different objects') +}) +test('custom RegExp handler', async ({ same, ok, isNot }) => { + const data = { regex: /foo/ } + const cloned = rfdc({ constructorHandlers: [[RegExp, (o) => new RegExp(o)]] })(data) + isNot(cloned.regex, data.regex, 'different objects') + ok(cloned.regex.test('foo')) +}) function types (clone, label) { test(label + ' – number', async ({ is }) => {