Reputation: 13
I want to be able to generate a dynamic property on objects and I have tried doing this by creating a function that takes an input object to then return a function that takes a parameter. This parameter is used to set the dynamic property.
My issue is that once the function is created, I don't seem to be getting a new object each time and instead the function is setting the property on a previously assigned object.
I have tried re-working the assigning of an object but to no avail, I have tested out alternative (less ideal code) which works but I want to know why my initial solution does not work.
/* Returns a function which will assign a 'required' property to all objects within the given object */
const generateSchemaField = obj => {
obj = Object.assign({}, obj);
return function(required = false) {
Object.keys(obj).forEach(key => {
Object.assign(obj[key], {
required,
});
});
return obj;
};
};
/* The way the above function would be invoked*/
const userEmailUsingGenerateSchemaField = generateSchemaField({
user_email: {
type: 'string',
description: 'A user email',
},
});
/* The below function does not encounter the same problem */
const userEmailNotUsingGenerateSchemaField = function(required = false) {
let obj = {
user_email: {
type: 'string',
description: 'A user email',
},
};
Object.keys(obj).forEach(key => {
Object.assign(obj[key], {
required,
});
});
return obj;
};
let firstResultUsing = userEmailUsingGenerateSchemaField();
let secondResultUsing = userEmailUsingGenerateSchemaField(true);
console.log(firstResultUsing);
console.log(secondResultUsing);
Expected Output
{
user_email: { type: 'string', description: 'A user email', required: false }
}
{
user_email: { type: 'string', description: 'A user email', required: true }
}
Actual
{
user_email: { type: 'string', description: 'A user email', required: true }
}
{
user_email: { type: 'string', description: 'A user email', required: true }
}
Upvotes: 1
Views: 66
Reputation: 29086
You call generateSchemaField
only once when you do
const userEmailUsingGenerateSchemaField = generateSchemaField({
user_email: {
type: 'string',
description: 'A user email',
},
});
So, when if you invoke the function it returns multiple times, you would still be using the same object obj
captured by the closure generated by generateSchemaField
If you want to keep your code mostly the same, need to do the cloning of the object (obj = Object.assign({}, obj);
in your code) inside the inner function, then you will be generating a fresh copy of the object each invocation. However, be aware that that only does a shallow copy, so even though you clone an object with Object.assign
, any objects that are properties of it would still be shared between each clone:
const obj = {
foo: 1,
subObj: { bar : 2 }
}
const a = Object.assign({}, obj);
const b = Object.assign({}, obj);
//this is not shared
a.foo += "a";
b.foo += "b";
//so changes in the cloned objects remain there
console.log("obj.foo", obj.foo);
console.log("a.foo", a.foo);
console.log("b.foo", b.foo);
//this object is shared
a.subObj.bar += "a";
b.subObj.bar += "b";
//so changes affect all of them
console.log("obj.subObj.bar", obj.subObj.bar);
console.log("a.subObj.bar", a.subObj.bar);
console.log("b.subObj.bar", b.subObj.bar);
You need to do some form of deep clone mechanism to avoid that. I will use cloneDeep
from Lodash to illustrate this:
/* Returns a function which will assign a 'required' property to all objects within the given object */
const generateSchemaField = obj => {
return function(required = false) {
//make a fresh copy
const result = _.cloneDeep(obj);
Object.keys(result).forEach(key => {
Object.assign(result[key], {
required,
});
});
return result;
};
};
/* The way the above function would be invoked*/
const userEmailUsingGenerateSchemaField = generateSchemaField({
user_email: {
type: 'string',
description: 'A user email',
},
});
let firstResultUsing = userEmailUsingGenerateSchemaField();
let secondResultUsing = userEmailUsingGenerateSchemaField(true);
console.log(firstResultUsing);
console.log(secondResultUsing);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
You can check the link for deep cloning for other methods to deep clone, if you don't want to use Lodash.
Upvotes: 0
Reputation: 2626
Short Story
It's a simple issue of referencing the same object.
To prove this compare the two objects
console.log(firstResultUsing === secondResultUsing)
You'll see that it prints true
which proves that they are both referencing the same object.
Scroll down for solution!
Long Story
At this line:
const userEmailUsingGenerateSchemaField = generateSchemaField({
user_email: {
type: 'string',
description: 'A user email',
},
})
What's happening here is that your generateSchemaField
function is returning a function which has a closure over obj
which is nothing but
{
user_email: {
type: 'string',
description: 'A user email',
},
}
Now at this line:
const firstResultUsing = userEmailUsingGenerateSchemaField()
The function gets evaluated and returns the modified object
{
user_email: {
type: 'string',
description: 'A user email',
required: false
},
}
Remember the returned object still has the same reference as obj
Now again at line:
const secondResultUsing = userEmailUsingGenerateSchemaField(true)
What's happening here is the same referenced obj
object is modified and it's updated with the property required: true
That's why when you console.log
both are showing required: true
because they both reference the same object.
Solution
const generateSchemaField = obj => {
return function(required = false) {
const objClone = JSON.parse(JSON.stringify(obj));
Object.keys(objClone).forEach(key => {
Object.assign(objClone[key], {
required,
});
});
return objClone;
};
};
Let's break this down.
I removed obj = Object.assign({}, obj);
as it doesn't do any good. It seems a redundant line.
Next, I did a deep clone of the obj
. Remember Object.assign
will not work as it just creates a shallow copy/clone and here it won't work as the key email_id
holds a reference to an object.
Beware that deep cloning using JSON.parse(JSON.stringify(obj))
will work only for objects that have JSON-safe values(No functions or undefined
etc...).
Then, I am manipulating this cloned object and returning it. Now there is no threat of manipulating the same referenced object.
Let me know if this helps or you need a better explanation.
Upvotes: 1