burlzad
burlzad

Reputation: 13

Function that returns a function setting property on existing object. JS

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

Answers (2)

VLAZ
VLAZ

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

Ramaraja
Ramaraja

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

Related Questions