Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ deepmerge(options)

- `symbols` (`boolean`, optional) - should also merge object-keys which are symbols, default is false
- `all` (`boolean`, optional) - merges all parameters, default is false
- `nullPrototype` (`boolean, optional) - created Objects have no prototype so keys like constructor and prototype are allowed without having the risk of a prototype pollution but has a ca. 20-30 % performance penalty, default is false

```js
const deepmerge = require('@fastify/deepmegre')()
Expand All @@ -42,22 +43,32 @@ The benchmarks are available in the benchmark-folder.

`npm run bench` - benchmark various use cases of deepmerge:
```
@fastify/deepmerge: merge regex with date x 1,266,447,885 ops/sec ±0.14% (97 runs sampled)
@fastify/deepmerge: merge object with a primitive x 1,266,435,016 ops/sec ±0.33% (97 runs sampled)
@fastify/deepmerge: merge two arrays containing strings x 25,591,739 ops/sec ±0.24% (98 runs sampled)
@fastify/deepmerge: two merge arrays containing objects x 976,182 ops/sec ±0.46% (98 runs sampled)
@fastify/deepmerge: merge two flat objects x 10,027,879 ops/sec ±0.36% (94 runs sampled)
@fastify/deepmerge: merge nested objects x 5,341,227 ops/sec ±0.67% (94 runs sampled)
@fastify/deepmerge: merge regex with date x 1,245,263,207 ops/sec ±0.41% (99 runs sampled)
@fastify/deepmerge: merge object with a primitive x 1,241,361,757 ops/sec ±0.40% (99 runs sampled)
@fastify/deepmerge: merge two arrays containing strings x 24,765,578 ops/sec ±0.50% (94 runs sampled)
@fastify/deepmerge: merge two arrays containing objects x 1,600,598 ops/sec ±0.64% (96 runs sampled)
@fastify/deepmerge: merge two flat objects x 14,327,729 ops/sec ±0.57% (93 runs sampled)
@fastify/deepmerge: merge nested objects x 7,319,512 ops/sec ±0.45% (96 runs sampled)
```

`npm run bench:nullprototype` - benchmark deepmerge with nullPrototype set to true:
```
@fastify/deepmerge: merge regex with date x 1,241,641,471 ops/sec ±0.19% (99 runs sampled)
@fastify/deepmerge: merge object with a primitive x 1,232,239,900 ops/sec ±0.49% (93 runs sampled)
@fastify/deepmerge: merge two arrays containing strings x 24,893,133 ops/sec ±0.56% (95 runs sampled)
@fastify/deepmerge: merge two arrays containing objects x 1,521,465 ops/sec ±0.83% (94 runs sampled)
@fastify/deepmerge: merge two flat objects x 9,414,407 ops/sec ±0.61% (97 runs sampled)
@fastify/deepmerge: merge nested objects x 5,865,123 ops/sec ±0.50% (93 runs sampled)
```

`npm run bench:compare` - comparison of @fastify/deepmerge with other popular deepmerge implementation:
```
@fastify/deepmerge x 403,777 ops/sec ±0.22% (98 runs sampled)
deepmerge x 21,143 ops/sec ±0.83% (93 runs sampled)
merge-deep x 89,447 ops/sec ±0.59% (95 runs sampled)
ts-deepmerge x 185,601 ops/sec ±0.59% (96 runs sampled)
deepmerge-ts x 185,310 ops/sec ±0.50% (92 runs sampled)
lodash.merge x 89,053 ops/sec ±0.37% (99 runs sampled)
@fastify/deepmerge x 618,229 ops/sec ±0.19% (99 runs sampled)
deepmerge x 21,326 ops/sec ±0.43% (95 runs sampled)
merge-deep x 86,034 ops/sec ±0.49% (98 runs sampled)
ts-deepmerge x 183,827 ops/sec ±0.41% (99 runs sampled)
deepmerge-ts x 179,950 ops/sec ±0.62% (95 runs sampled)
lodash.merge x 89,655 ops/sec ±0.44% (99 runs sampled)
```

## License
Expand Down
2 changes: 1 addition & 1 deletion benchmark/bench.all.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ new Benchmark.Suite()
.add('@fastify/deepmerge: merge two arrays containing strings', function () {
deepmerge(simpleArrayTarget, simpleArraySource)
})
.add('@fastify/deepmerge: two merge arrays containing objects', function () {
.add('@fastify/deepmerge: merge two arrays containing objects', function () {
deepmerge(complexArrayTarget, complexArraySource)
})
.add('@fastify/deepmerge: merge two flat objects', function () {
Expand Down
12 changes: 6 additions & 6 deletions benchmark/bench.compare.detailed.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ new Benchmark.Suite()
.add('@fastify/deepmerge: merge two arrays containing strings', function () {
fastifyDeepmerge(simpleArrayTarget, simpleArraySource)
})
.add('@fastify/deepmerge: two merge arrays containing objects', function () {
.add('@fastify/deepmerge: merge two arrays containing objects', function () {
fastifyDeepmerge(complexArrayTarget, complexArraySource)
})
.add('@fastify/deepmerge: merge two flat objects', function () {
Expand All @@ -61,7 +61,7 @@ new Benchmark.Suite()
.add('deepmerge: merge two arrays containing strings', function () {
deepmerge(simpleArrayTarget, simpleArraySource)
})
.add('deepmerge: two merge arrays containing objects', function () {
.add('deepmerge: merge two arrays containing objects', function () {
deepmerge(complexArrayTarget, complexArraySource)
})
.add('deepmerge: merge two flat objects', function () {
Expand All @@ -79,7 +79,7 @@ new Benchmark.Suite()
.add('merge-deep: merge two arrays containing strings', function () {
mergedeep(simpleArrayTarget, simpleArraySource)
})
.add('merge-deep: two merge arrays containing objects', function () {
.add('merge-deep: merge two arrays containing objects', function () {
mergedeep(complexArrayTarget, complexArraySource)
})
.add('merge-deep: merge two flat objects', function () {
Expand All @@ -97,7 +97,7 @@ new Benchmark.Suite()
.add('ts-deepmerge: merge two arrays containing strings', function () {
tsDeepmerge(simpleArrayTarget, simpleArraySource)
})
.add('ts-deepmerge: two merge arrays containing objects', function () {
.add('ts-deepmerge: merge two arrays containing objects', function () {
tsDeepmerge(complexArrayTarget, complexArraySource)
})
.add('ts-deepmerge: merge two flat objects', function () {
Expand All @@ -115,7 +115,7 @@ new Benchmark.Suite()
.add('deepmerge-ts: merge two arrays containing strings', function () {
deepmergeTs(simpleArrayTarget, simpleArraySource)
})
.add('deepmerge-ts: two merge arrays containing objects', function () {
.add('deepmerge-ts: merge two arrays containing objects', function () {
deepmergeTs(complexArrayTarget, complexArraySource)
})
.add('deepmerge-ts: merge two flat objects', function () {
Expand All @@ -133,7 +133,7 @@ new Benchmark.Suite()
.add('lodash.merge: merge two arrays containing strings', function () {
lodashMerge(simpleArrayTarget, simpleArraySource)
})
.add('lodash.merge: two merge arrays containing objects', function () {
.add('lodash.merge: merge two arrays containing objects', function () {
lodashMerge(complexArrayTarget, complexArraySource)
})
.add('lodash.merge: merge two flat objects', function () {
Expand Down
2 changes: 1 addition & 1 deletion benchmark/bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ new Benchmark.Suite()
.add('@fastify/deepmerge: merge two arrays containing strings', function () {
deepmerge(simpleArrayTarget, simpleArraySource)
})
.add('@fastify/deepmerge: two merge arrays containing objects', function () {
.add('@fastify/deepmerge: merge two arrays containing objects', function () {
deepmerge(complexArrayTarget, complexArraySource)
})
.add('@fastify/deepmerge: merge two flat objects', function () {
Expand Down
53 changes: 53 additions & 0 deletions benchmark/bench.nullPrototype.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict'

const Benchmark = require('benchmark')
const deepmerge = require('..')({ nullPrototype: true })

const sourceSimple = { key1: 'changed', key2: 'value2' }
const targetSimple = { key1: 'value1', key3: 'value3' }

const sourceNested = {
key1: {
subkey1: 'subvalue1',
subkey2: 'subvalue2'
}
}
const targetNested = {
key1: 'value1',
key2: 'value2'
}

const primitive = 'primitive'

const date = new Date()
const regex = /a/g

const simpleArrayTarget = ['a1', 'a2', 'c1', 'f1', 'p1']
const simpleArraySource = ['t1', 's1', 'c2', 'r1', 'p2', 'p3']

const complexArraySource = [{ ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }]
const complexArrayTarget = [{ ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }]

new Benchmark.Suite()
.add('@fastify/deepmerge: merge regex with date', function () {
deepmerge(regex, date)
})
.add('@fastify/deepmerge: merge object with a primitive', function () {
deepmerge(targetSimple, primitive)
})
.add('@fastify/deepmerge: merge two arrays containing strings', function () {
deepmerge(simpleArrayTarget, simpleArraySource)
})
.add('@fastify/deepmerge: merge two arrays containing objects', function () {
deepmerge(complexArrayTarget, complexArraySource)
})
.add('@fastify/deepmerge: merge two flat objects', function () {
deepmerge(targetSimple, sourceSimple)
})
.add('@fastify/deepmerge: merge nested objects', function () {
deepmerge(targetNested, sourceNested)
})
.on('cycle', function (event) {
console.log(String(event.target))
})
.run()
3 changes: 2 additions & 1 deletion benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"bench": "node ./bench.js",
"bench:all": "node ./bench.all.js",
"bench:compare": "node ./bench.compare.js",
"bench:compare:detailed": "node ./bench.compare.detailed.js"
"bench:compare:detailed": "node ./bench.compare.detailed.js",
"bench:nullprototype": "node ./bench.nullPrototype.js"
},
"author": "",
"license": "ISC",
Expand Down
96 changes: 84 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@
// Copyright (c) 2012 - 2022 James Halliday, Josh Duff, and other contributors of deepmerge

function deepmergeConstructor (options) {
const prototypeKeys = ['constructor', '__proto__', 'prototype']
function isPrototypeKey (value) {
return (
value === 'constructor' ||
value === 'prototype' ||
value === '__proto__'
)
}

function isNotPrototypeKey (value) {
return prototypeKeys.indexOf(value) === -1
return (
value !== 'constructor' &&
value !== 'prototype' &&
value !== '__proto__'
)
}

function cloneArray (value) {
Expand Down Expand Up @@ -65,30 +75,88 @@ function deepmergeConstructor (options) {
return isMergeableObject(entry)
? Array.isArray(entry)
? cloneArray(entry)
: mergeObject({}, entry)
: cloneObject(entry)
: entry
}

function mergeObject (target, source) {
function mergeObjectNullPrototype (target, source) {
const result = Object.create(null)
const targetKeys = getKeys(target)
const sourceKeys = getKeys(source)
let i, il, key
for (i = 0, il = targetKeys.length; i < il; ++i) {
(key = targetKeys[i], true) &&
(sourceKeys.indexOf(key) === -1) &&
(result[key] = clone(target[key]))
}

for (i = 0, il = sourceKeys.length; i < il; ++i) {
key = sourceKeys[i]
if (key in target) {
if (targetKeys.indexOf(key) !== -1) {
result[key] = _deepmerge(target[key], source[key])
} else if (isPrototypeKey(key)) {
result[key] = clone(source[key])
}
} else {
result[key] = clone(source[key])
}
}
return result
}

function cloneObjectWithPrototype (target) {
const result = {}

const targetKeys = getKeys(target)
let i, il, key
for (i = 0, il = targetKeys.length; i < il; ++i) {
isNotPrototypeKey(key = targetKeys[i]) &&
(result[key] = clone(target[key]))
}
return result
}

function cloneObjectNullPrototype (target) {
const result = Object.create(null)

const targetKeys = getKeys(target)
let i, il, key
for (i = 0, il = targetKeys.length; i < il; ++i) {
key = targetKeys[i]
result[key] = clone(target[key])
}
return result
}

const cloneObject = options && options.nullPrototype === true
? cloneObjectNullPrototype
: cloneObjectWithPrototype

function mergeObjectWithPrototype (target, source) {
const result = {}

const targetKeys = getKeys(target)
const sourceKeys = getKeys(source)
let i, il, key
for (i = 0, il = targetKeys.length; i < il; ++i) {
isNotPrototypeKey(key = targetKeys[i]) &&
(sourceKeys.indexOf(key) === -1) &&
(result[key] = clone(target[key]))
(sourceKeys.indexOf(key) === -1) &&
(result[key] = clone(target[key]))
}

for (i = 0, il = sourceKeys.length; i < il; ++i) {
isNotPrototypeKey(key = sourceKeys[i]) &&
(
key in target && (targetKeys.indexOf(key) !== -1 && (result[key] = _deepmerge(target[key], source[key])), true) || // eslint-disable-line no-mixed-operators
(result[key] = clone(source[key]))
)
(
key in target && (targetKeys.indexOf(key) !== -1 && (result[key] = _deepmerge(target[key], source[key])), true) || // eslint-disable-line no-mixed-operators
(result[key] = clone(source[key]))
)
}
return result
}
const mergeObject = options && options.nullPrototype === true
? mergeObjectNullPrototype
: mergeObjectWithPrototype

function _deepmerge (target, source) {
const sourceIsArray = Array.isArray(source)
Expand All @@ -107,12 +175,16 @@ function deepmergeConstructor (options) {
}
}

const createObject = options && options.nullPrototype === true
? function () { return Object.create(null) }
: function () { return {} }

function _deepmergeAll () {
switch (arguments.length) {
case 0:
return {}
return createObject()
case 1:
return clone(arguments[0])
return _deepmerge({}, arguments[0])
case 2:
return _deepmerge(arguments[0], arguments[1])
}
Expand Down
Loading