I am sure there is a name for what I am trying to do and I am sure this has been asked before, but I am struggling to find an existing answer - probably because I am not searching for the correct term. I have a JavaScript function called extend()
which is similar to Object.assign()
to merge Objects together - I don't have access to Object.assign
in my current implementation.
function extend(target) {
for (var i = 1; i < arguments.length; ++i) {
if (typeof arguments[i] !== 'object') {
continue;
}
for (var j in arguments[i]) {
if (arguments[i].hasOwnProperty(j)) {
if (typeof arguments[i][j] === 'object') {
target[j] = extend({}, target[j], arguments[i][j]);
}
else if (!target.hasOwnProperty(j)) {
target[j] = arguments[i][j];
}
}
}
}
return target;
}
This works fine if I call it using extend(target, defaults)
which will merge the contents of defaults
into target
without overwriting any existing keys in target
(unless they are Objects and then they are merged).
I am looking for a way to actually call this using target.extend(defaults)
, so the first target
parameter to extend
is actually implied as this
. I have tried to rewrite the function by replacing target
with this
, but it doesn't have the desired result (I think the way I have changed the recursion is wrong).
target.extend = function() {
...
this[j] = this.extend(this[j], arguments[i][j]);
...
return this;
};
Any pointers (or references to existing answers) would be gratefully appreciated.
You can assign the function to Object.prototype
to achieve the functionality you're looking for:
Object.defineProperty(
Object.prototype,
'extend',
{
enumerable: false,
value: function() {
const target = this;
for (var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] !== 'object') {
continue;
}
for (var j in arguments[i]) {
if (arguments[i].hasOwnProperty(j)) {
if (typeof arguments[i][j] === 'object') {
target[j] = extend({}, target[j], arguments[i][j]);
}
else if (!target.hasOwnProperty(j)) {
target[j] = arguments[i][j];
}
}
}
}
return target;
}
}
);
const obj = { foo: 'foo' };
obj.extend({ bar: 'bar' });
console.log(obj);
Or, cleaning up the code a bunch:
Object.defineProperty(
Object.prototype,
'extend',
{
enumerable: false,
value: function(...args) {
const target = this;
for (const arg of args) {
for (const [key, val] of Object.entries(arg)) {
if (typeof val === 'object') extend(target[key], val);
else target[key] = val;
}
}
}
}
);
const obj = { foo: 'foo' };
obj.extend({ bar: 'bar' });
console.log(obj);
But this is a bad idea, because it can result in name collisions and confusing code. What if there's an object like
const obj = { extend: true };
What does obj.extend
do then? (It'll refer to the own property, but this isn't something a script-writer should have to worry about. Better to have a standalone function.)
This is exactly the same reason why many of the Object helper methods exist on the Object
constructor, and not on Object.prototype
. For example, you call Object.assign
, or Object.defineProperty
, not obj.assign
or obj.defineProperty
.
If you want this functionality only on certain objects, you can avoid the prototype pollution by calling Object.defineProperty
on those objects:
const addExtend = obj => {
Object.defineProperty(
obj,
'extend',
{
enumerable: false,
value: function(...args) {
const target = this;
for (const arg of args) {
for (const [key, val] of Object.entries(arg)) {
if (typeof val === 'object') extend(target[key], val);
else target[key] = val;
}
}
}
}
);
};
const obj = { foo: 'foo' };
addExtend(obj);
obj.extend({ bar: 'bar' });
console.log(obj);
Or, if you have control over where the object is created, you can use Object.create
so that extend
is a method inherited from the prototype:
const objExtendProto = {};
Object.defineProperty(
objExtendProto,
'extend',
{
enumerable: false,
value: function(...args) {
const target = this;
for (const arg of args) {
for (const [key, val] of Object.entries(arg)) {
if (typeof val === 'object') extend(target[key], val);
else target[key] = val;
}
}
}
}
);
const obj = Object.create(objExtendProto);
obj.foo = 'foo';
obj.extend({ bar: 'bar' });
console.log(obj);