Reputation: 1024
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.
Upvotes: 0
Views: 206
Reputation: 4650
Like @CertainPerformance has pointed out, what you're looking for will still create name collisions. But this is exactly what you wanted. You wanted to add a custom extend
method in your target object right, then that's what we will do.
Explanation in comments
function extend() {
for (let i = 0; i < arguments.length; i++) { //loop through arguments
for (k in arguments[i]) { //loop argument props
if (typeof arguments[i][k] === 'object') {
this[k].extend = this.extend; //attach extend function to object prop
this[k].extend(arguments[i][k]) //execute extend recursively
}
else this[k] = this[k] || arguments[i][k]; //assign property to target while avoiding overwriting target object
}
}
delete this.extend; //delete extend method
return this; //return target object OR Object.create(this) to avoid mutating
}
//test
const trgt = { a: { a1: 2 }, b: 3, c: 5 };
const defaults = { b: 2, a: { a2: 1, a1: 55 }, d: 10 };
trgt.extend = extend; //assign extend method to target object
const result = trgt.extend(defaults); //execute
console.log(result);
Upvotes: 1
Reputation: 370729
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);
Upvotes: 5