<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js'></script>
<script>
function stringifiable(obj) {
// Safely stringify Object.create(null)
/* istanbul ignore next */
return typeof obj === 'object' && !('toString' in obj) ?
Object.prototype.toString.call(obj).slice(8, -1) :
obj;
}
var isProduction = typeof process === 'object' && process.env.NODE_ENV === 'production';
function invariant(condition, message) {
if (!condition) {
/* istanbul ignore next */
if (isProduction) {
throw new Error('Invariant failed');
}
throw new Error(message());
}
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
var splice = Array.prototype.splice;
var toString = Object.prototype.toString;
function type(obj) {
return toString.call(obj).slice(8, -1);
}
var assign = Object.assign || /* istanbul ignore next */ (function (target, source) {
getAllKeys(source).forEach(function (key) {
if (hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
});
return target;
});
var getAllKeys = typeof Object.getOwnPropertySymbols === 'function'
? function (obj) { return Object.keys(obj).concat(Object.getOwnPropertySymbols(obj)); }
/* istanbul ignore next */
: function (obj) { return Object.keys(obj); };
function copy(object) {
return Array.isArray(object)
? assign(object.constructor(object.length), object)
: (type(object) === 'Map')
? new Map(object)
: (type(object) === 'Set')
? new Set(object)
: (object && typeof object === 'object')
? assign(Object.create(Object.getPrototypeOf(object)), object)
/* istanbul ignore next */
: object;
}
var Context = /** @class */ (function () {
function Context() {
this.commands = assign({}, defaultCommands);
this.update = this.update.bind(this);
// Deprecated: update.extend, update.isEquals and update.newContext
this.update.extend = this.extend = this.extend.bind(this);
this.update.isEquals = function (x, y) { return x === y; };
this.update.newContext = function () { return new Context().update; };
}
Object.defineProperty(Context.prototype, "isEquals", {
get: function () {
return this.update.isEquals;
},
set: function (value) {
this.update.isEquals = value;
},
enumerable: true,
configurable: true
});
Context.prototype.extend = function (directive, fn) {
this.commands[directive] = fn;
};
Context.prototype.update = function (object, $spec) {
var _this = this;
var spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec;
if (!(Array.isArray(object) && Array.isArray(spec))) {
invariant(!Array.isArray(spec), function () { return "update(): You provided an invalid spec to update(). The spec may " +
"not contain an array except as the value of $set, $push, $unshift, " +
"$splice or any custom command allowing an array value."; });
}
invariant(typeof spec === 'object' && spec !== null, function () { return "update(): You provided an invalid spec to update(). The spec and " +
"every included key path must be plain objects containing one of the " +
("following commands: " + Object.keys(_this.commands).join(', ') + "."); });
var nextObject = object;
getAllKeys(spec).forEach(function (key) {
if (hasOwnProperty.call(_this.commands, key)) {
var objectWasNextObject = object === nextObject;
nextObject = _this.commands[key](spec[key], nextObject, spec, object);
if (objectWasNextObject && _this.isEquals(nextObject, object)) {
nextObject = object;
}
}
else {
var nextValueForKey = type(object) === 'Map'
? _this.update(object.get(key), spec[key])
: _this.update(object[key], spec[key]);
var nextObjectValue = type(nextObject) === 'Map'
? nextObject.get(key)
: nextObject[key];
if (!_this.isEquals(nextValueForKey, nextObjectValue)
|| typeof nextValueForKey === 'undefined'
&& !hasOwnProperty.call(object, key)) {
if (nextObject === object) {
nextObject = copy(object);
}
if (type(nextObject) === 'Map') {
nextObject.set(key, nextValueForKey);
}
else {
nextObject[key] = nextValueForKey;
}
}
}
});
return nextObject;
};
return Context;
}());
var defaultCommands = {
$push: function (value, nextObject, spec) {
invariantPushAndUnshift(nextObject, spec, '$push');
return value.length ? nextObject.concat(value) : nextObject;
},
$unshift: function (value, nextObject, spec) {
invariantPushAndUnshift(nextObject, spec, '$unshift');
return value.length ? value.concat(nextObject) : nextObject;
},
$splice: function (value, nextObject, spec, originalObject) {
invariantSplices(nextObject, spec);
value.forEach(function (args) {
invariantSplice(args);
if (nextObject === originalObject && args.length) {
nextObject = copy(originalObject);
}
splice.apply(nextObject, args);
});
return nextObject;
},
$set: function (value, _nextObject, spec) {
invariantSet(spec);
return value;
},
$toggle: function (targets, nextObject) {
invariantSpecArray(targets, '$toggle');
var nextObjectCopy = targets.length ? copy(nextObject) : nextObject;
targets.forEach(function (target) {
nextObjectCopy[target] = !nextObject[target];
});
return nextObjectCopy;
},
$unset: function (value, nextObject, _spec, originalObject) {
invariantSpecArray(value, '$unset');
value.forEach(function (key) {
if (Object.hasOwnProperty.call(nextObject, key)) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
delete nextObject[key];
}
});
return nextObject;
},
$add: function (values, nextObject, _spec, originalObject) {
invariantMapOrSet(nextObject, '$add');
invariantSpecArray(values, '$add');
if (type(nextObject) === 'Map') {
values.forEach(function (_a) {
var key = _a[0], value = _a[1];
if (nextObject === originalObject && nextObject.get(key) !== value) {
nextObject = copy(originalObject);
}
nextObject.set(key, value);
});
}
else {
values.forEach(function (value) {
if (nextObject === originalObject && !nextObject.has(value)) {
nextObject = copy(originalObject);
}
nextObject.add(value);
});
}
return nextObject;
},
$remove: function (value, nextObject, _spec, originalObject) {
invariantMapOrSet(nextObject, '$remove');
invariantSpecArray(value, '$remove');
value.forEach(function (key) {
if (nextObject === originalObject && nextObject.has(key)) {
nextObject = copy(originalObject);
}
nextObject.delete(key);
});
return nextObject;
},
$merge: function (value, nextObject, _spec, originalObject) {
invariantMerge(nextObject, value);
getAllKeys(value).forEach(function (key) {
if (value[key] !== nextObject[key]) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
nextObject[key] = value[key];
}
});
return nextObject;
},
$apply: function (value, original) {
invariantApply(value);
return value(original);
},
};
var defaultContext = new Context();
var update = defaultContext.update;
// invariants
function invariantPushAndUnshift(value, spec, command) {
invariant(Array.isArray(value), function () { return "update(): expected target of " + stringifiable(command) + " to be an array; got " + stringifiable(value) + "."; });
invariantSpecArray(spec[command], command);
}
function invariantSpecArray(spec, command) {
invariant(Array.isArray(spec), function () { return "update(): expected spec of " + stringifiable(command) + " to be an array; got " + stringifiable(spec) + ". " +
"Did you forget to wrap your parameter in an array?"; });
}
function invariantSplices(value, spec) {
invariant(Array.isArray(value), function () { return "Expected $splice target to be an array; got " + stringifiable(value); });
invariantSplice(spec.$splice);
}
function invariantSplice(value) {
invariant(Array.isArray(value), function () { return "update(): expected spec of $splice to be an array of arrays; got " + stringifiable(value) + ". " +
"Did you forget to wrap your parameters in an array?"; });
}
function invariantApply(fn) {
invariant(typeof fn === 'function', function () { return "update(): expected spec of $apply to be a function; got " + stringifiable(fn) + "."; });
}
function invariantSet(spec) {
invariant(Object.keys(spec).length === 1, function () { return "Cannot have more than one key in an object with $set"; });
}
function invariantMerge(target, specValue) {
invariant(specValue && typeof specValue === 'object', function () { return "update(): $merge expects a spec of type 'object'; got " + stringifiable(specValue); });
invariant(target && typeof target === 'object', function () { return "update(): $merge expects a target of type 'object'; got " + stringifiable(target); });
}
function invariantMapOrSet(target, command) {
var typeOfTarget = type(target);
invariant(typeOfTarget === 'Map' || typeOfTarget === 'Set', function () { return "update(): " + stringifiable(command) + " expects a target of type Set or Map; got " + stringifiable(typeOfTarget); });
}
</script>
var myArr = Array.from({
length: 1_000_000
}, () => ({ id: Math.floor(Math.random() * 1000) }));
var myArr2 = Array.from({
length: 1_000_000
}, () => ({ id: Math.floor(Math.random() * 1000) }));
var myCopy = null;
myCopy = _.uniqBy([myArr, myArr2], 'id');
myCopy = (() => {
const existingIds = new Set(myArr.map((machine) => machine.id));
const newMachines = myArr2.filter((machine) => !existingIds.has(machine.id));
return update(myArr, {
$push: newMachines,
});
})();
--enable-precise-memory-info
flag.
Test case name | Result |
---|---|
uniqBy | |
Set with filter |
Test name | Executions per second |
---|---|
uniqBy | 25.7 Ops/sec |
Set with filter | 16.0 Ops/sec |
The provided benchmark compares two approaches to deduplicate an array of objects based on a specific property (id
): one using the Lodash library's _.uniqBy
method, and the other using a native JavaScript approach that leverages Set
and the filter
method.
Lodash's _.uniqBy
Method
myCopy = _.uniqBy([...myArr, ...myArr2], 'id');
myArr
and myArr2
into a new array and then deduplicates it by the id
property of the objects in the array.Set with Filter
myCopy = (() => {
const existingIds = new Set(myArr.map((machine) => machine.id));
const newMachines = myArr2.filter((machine) => !existingIds.has(machine.id));
return update(myArr, { $push: newMachines });
})();
id
s from myArr
into a Set
, then filters myArr2
to exclude any items with id
s already present in the set, and finally adds the filtered items to myArr
.Set
and Array.prototype.filter
, avoiding the need for an external library.id
is in a Set
is generally O(1) on average, which can lead to improved performance for certain datasets.update
(presumably a custom function) adds complexity. It may introduce its own performance costs depending on how it has been implemented.The benchmark results indicate:
uniqBy
: 25.74 executions per second.Set with filter
: 16.00 executions per second.The performance measurements show that Lodash's uniqBy
method performs better than the custom Set
and filter
approach in this case.
Set
in certain cases:const seen = {};
const result = [...myArr, ...myArr2].filter(item => {
if (seen[item.id]) {
return false;
} else {
seen[item.id] = true;
return true;
}
});
reduce
: Perform deduplication in a single pass:const result = [...myArr, ...myArr2].reduce((acc, item) => {
if (!acc.find(i => i.id === item.id)) {
acc.push(item);
}
return acc;
}, []);
In scenarios requiring deduplication in JavaScript, both Lodash's _.uniqBy
and native Set
with filter
are viable options. Each approach has its pros and cons, and the decision may depend on the specific use case, performance considerations, and code readability preferences. Additionally, examining alternative methods can provide further optimization opportunities, especially when avoiding external library dependencies is a priority.