Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add constructorHandlers option #40

Merged
merged 6 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 40 additions & 41 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ function copyBuffer (cur) {

function rfdc (opts) {
opts = opts || {}

if (opts.circles) return rfdcCircles(opts)

const constructorHandlers = new Map([
[Date, (o) => new Date(o)],
[Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))],
[Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))]
].concat(opts.constructorHandlers || []))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const constructorHandlers = new Map([
[Date, (o) => new Date(o)],
[Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))],
[Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))]
].concat(opts.constructorHandlers || []))
const constructorHandlers = new Map()
constructorHandlers.add(Date, (o) => new Date(o))
constructorHandlers.add(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn)))
constructorHandlers.add(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn)))
if (opts.constructorHandlers) {
opts.constructorHandlers.forEach((name, handler) => constructorHandlers.add(name, handler)
}

It is faster to work around the additional array allocation. That could also be done inside of the Map and Set handlers itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see much difference in the benchmark, but will make the change anyway

benchRfdc*100: 206.686ms
benchRfdcProto*100: 218.722ms
benchRfdcCircles*100: 228.578ms
benchRfdcCirclesProto*100: 239.244ms


let handler = null

return opts.proto ? cloneProto : clone

function cloneArray (a, fn) {
Expand All @@ -23,8 +31,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 {
Expand All @@ -36,22 +44,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 {
Expand All @@ -63,21 +67,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 {
Expand All @@ -92,6 +92,13 @@ function rfdcCircles (opts) {
const refs = []
const refsNew = []

const constructorHandlers = new Map([
[Date, (o) => new Date(o)],
[Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))],
[Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))]
].concat(opts.constructorHandlers || []))

let handler = null
return opts.proto ? cloneProto : clone

function cloneArray (a, fn) {
Expand All @@ -102,8 +109,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 {
Expand All @@ -120,10 +127,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)
Expand All @@ -132,12 +139,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 {
Expand All @@ -156,23 +159,19 @@ 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)
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 {
Expand Down
25 changes: 24 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Loading