How spread syntax in object works? - javascript

Came across the concept of creating new object using spread syntax as below
const human = { age: 20 };
const john = { ...human };
john.age = 10;
console.log(human.age); // 20
console.log(john.age); // 10
As shown above human object get to retain it's original value. Now have a look at below code:
const human = { age: 20, cars: ["toyota", "honda"] };
const john = { ...human };
john.cars[1] = "camero";
console.log(human.cars); // ["toyota", "camero"]
console.log(john.cars); // ["toyota", "camero"]
Can anyone explain to me why the above scenario happened? Why human's cars object get changed? It looks to me that it's very likely for developer to make mistakes without understanding how to avoid the inconsistent behaviour

The object human only contains a reference to the array which contains ["toyota", "honda"]. When you duplicate the object using the spread operator, you also duplicate the reference, which means that john has an identical reference and therefore john.cars is the same array as human.cars.
Because of this, if you modify john.cars, you also modify human.cars because they are the same array. If you want to clone an array you can also do this using the spread operator:
const human = { age: 20, cars: ["toyota", "honda"] };
const john = { ...human };
john.cars = [ ... human.cars ];
john.cars[1] = "camero";
console.log(human.cars); // ["toyota", "honda"]
console.log(john.cars); // ["toyota", "camero"]
You will also see this type of behaviour if you clone an object that has properties which are objects:
const human = { name: { first: "John", last: "Jackson" } };
const human2 = { ... human };
human2.name.first = "Ellen";
console.log(human.name.first); // Ellen
That's because the spread operator only copies a reference to the name object, not the name object itself. Therefore, modifying one modifies the other because they are the same object. This is referred to as shallow cloning. If you want to avoid this confusion, you need to perform a deep clone.
The easiest way to do this is to convert to JSON and then convert back:
const human = { name: { first: "John", last: "Jackson" } };
const human2 = JSON.parse(JSON.stringify(human));
human2.name.first = "Ellen";
console.log(human.name.first); // John
console.log(human2.name.first); // Ellen

Related

Javascript Map() object has confusing behaviour [duplicate]

This question already has answers here:
Object copy using Spread operator actually shallow or deep?
(4 answers)
Closed 6 months ago.
I have a use case that I thought the Map object would be perfect for but there is confusing behaviour that appears to me as a bug, but Im after any information as to why this happens and possibly a solution to the behaviour.
For instance, say I have 2 objects:
const obj1 = { name: "John" };
const obj2 = { name: "Jane", age: 35 };
And I have defined and extra object for extra properties to add to both objects later:
const extProps = { gender: "unspecified", children: [] };
Create a new Map object and add the 2 objects:
const map = new Map();
map.set(obj1.name, obj1);
map.set(obj2.name, obj2);
Due to the objects being reference types I can assign the extra props like so:
Object.assign(obj1, { ...extProps });
Object.assign(obj2, { ...extProps });
Now I can get the values from the map using the keys like:
const johnObj = map.get("John");
const janeObj = map.get("Jane");
And the object have all the extra props like expected. The following statements update the individual objects in the map:
janeObj.gender = "female";
johnObj.age = 45;
Here is where the confusing behaviour I see is happening...
If I add an entry to the children array of either objects, it updates both
johnObj.children.push("jack");
obj2.children.push("jenny");
name: "John"
gender: "unspecified"
children: ["jack", "jenny"]
age: 45
name: "Jane"
age: 35
gender: "female"
children: ["jack", "jenny"]
What am I missing??
Like Konrad said in his comment, "arrays are also objects and are reference types".
The issue is that the spread operator (...) only goes on level deep, so for the array in extProps, is it not copied, it is the same one.
To solve this you can use a recursive function to "deep copy" an object.
Here is an example of a deep copy function:
const deepCopy = objIn => {
if (typeof objIn !== 'object' || objIn === null) return objIn;
let objOut = Array.isArray(objIn) ? [] : {};
for (const key in objIn) {
objOut[key] = deepCopy(objIn[key]);
}
return objOut;
}

Setting multiple values to a object

I want to set multiple objects as multiple values of another object. So for example
let dataObj ={}
const personData = {name:"sai",age:"20"}
const ids = {id1:"1231455",id2:"425325232"}
dataObj.personData = personData;
dataObj.ids = ids
console.log(dataObj);
'personData' is a object and 'ids' is a object.Im setting these 2 objects as 2 values for 'dataObj'.
I was wondering is there a shorter way to achieve this like:
let dataObj ={}
const personData = {name:"sai",age:"20"}
const ids = {id1:"1231455",id2:"425325232"}
dataObj = personData;
dataObj = ids
console.log(dataObj);
The problem with the alternative I suggested is that it wont set 'key' of 'dataObj' object.It will just set the values of 'personData' and 'ids' to the value of dataObj.Is there any way I can achieve this ? Specifying multiple objects as values to another object with keys?
You can achieve this using the spread syntax:
let dataObj = {};
const personData = {name:"sai",age:"20"}
const ids = {id1:"1231455",id2:"425325232"}
dataObj = { ...dataObj, personData, ids };
console.log(dataObj);
/*
output :
value of dataObj:
{
personData: {
name: "sai",
age" "20"
},
ids: {
id1: "1231455",
id2: "425325232"
}
}
*/
I'll try and explain spread using the above example. First we start with an empty object dataObj: {}. We want to add new properties to that object: personData with the personData object as value and ids with the ids object as value.
ES2015 added a shorthand syntax for setting property definitions in objects. That's what been used here:
dataObj = { personData, ids };
This is simply a cleaner way of writing
dataObj = {
personData: personData,
ids: ids
};
More info on this can be found on MDN.
Because you want to initialise your object before, we can't simply overwrite the object, but we need to add the new properties to the existing object. This can be done using the spread syntax.
const oldObject = { key: 'value', anotherKey: 'another value' };
let newObject = { ...oldObject };
// newObject value: { key: 'value', anotherKey: 'another value' };
The oldObject was 'spreaded' in newObject. Resulting it in having the same key-value pairs as oldObject.
In your code example we assign a new object to dataObj. In that new object we spread all properties which are in the current dataObj: { ...dataObj and then add the new properties: , personData, ids }.
I hope this explanation is clear, feel free to ask for more info. The spread syntax is very powerful and can be used for a lot of things. So be sure to read up on that.
You can merge two objects as below. You can use '...' spread object. the spread object works for objects as well. Clone an object as shown in below code. for more information please visit https://flaviocopes.com/javascript-spread-operator/
let dataObj = {}
const personData = { name: "sai", age: "20" }
const ids = { id1: "1231455", id2: "425325232" }
dataObj = { ...dataObj, personData, ids }
console.log(dataObj);

Creating a JavaScript function that filters out duplicate in-memory objects?

Okay, so I am trying to create a function that allows you to input an array of Objects and it will return an array that removed any duplicate objects that reference the same object in memory. There can be objects with the same properties, but they must be different in-memory objects. I know that objects are stored by reference in JS and this is what I have so far:
const unique = array => {
let set = new Set();
return array.map((v, index) => {
if(set.has(v.id)) {
return false
} else {
set.add(v.id);
return index;
}
}).filter(e=>e).map(e=>array[e]);
}
Any advice is appreciated, I am trying to make this with a very efficient Big-O. Cheers!
EDIT: So many awesome responses. Right now when I run the script with arbitrary object properties (similar to the answers) and I get an empty array. I am still trying to wrap my head around filtering everything out but on for objects that are referenced in memory. I am not positive how JS handles objects with the same exact key/values. Thanks again!
Simple Set will do the trick
let a = {'a':1}
let b = {'a': 1,'b': 2, }
let c = {'a':1}
let arr = [a,b,c,a,a,b,b,c];
function filterSameMemoryObject(input){
return new Set([...input])
}
console.log(...filterSameMemoryObject(arr))
I don't think you need so much of code as you're just comparing memory references you can use === --> equality and sameness .
let a = {'a':1}
console.log(a === a ) // return true for same reference
console.log( {} === {}) // return false for not same reference
I don't see a good reason to do this map-filter-map combination. You can use only filter right away:
const unique = array => {
const set = new Set();
return array.filter(v => {
if (set.has(v.id)) {
return false
} else {
set.add(v.id);
return true;
}
});
};
Also if your array contains the objects that you want to compare by reference, not by their .id, you don't even need to the filtering yourself. You could just write:
const unique = array => Array.from(new Set(array));
The idea of using a Set is nice, but a Map will work even better as then you can do it all in the constructor callback:
const unique = array => [...new Map(array.map(v => [v.id, v])).values()]
// Demo:
var data = [
{ id: 1, name: "obj1" },
{ id: 3, name: "obj3" },
{ id: 1, name: "obj1" }, // dupe
{ id: 2, name: "obj2" },
{ id: 3, name: "obj3" }, // another dupe
];
console.log(unique(data));
Addendum
You speak of items that reference the same object in memory. Such a thing does not happen when your array is initialised as a plain literal, but if you assign the same object to several array entries, then you get duplicate references, like so:
const obj = { id: 1, name: "" };
const data = [obj, obj];
This is not the same thing as:
const data = [{ id: 1, name: "" }, { id: 1, name: "" }];
In the second version you have two different references in your array.
I have assumed that you want to "catch" such duplicates as well. If you only consider duplicate what is presented in the first version (shared references), then this was asked before.

what's difference between Object.assign() and '= assign' to realize shallow copy in array or object?

To realize shallow copy in object in below code, but the different outputs confuse me:
Object.assign:
var obj = {
name: 'wsscat',
age: 0,
add: {
a: 'beijing'
}
}
var obj2 = Object.assign({}, obj);
obj2.age = 18;
obj2.add.a = 'shanghai';
console.log(obj)
console.log(obj2)
output:
{ name: 'wsscat', age: 0, add: { a: 'shanghai' } }
{ name: 'wsscat', age: 18, add: { a: 'shanghai' } }
while use = "assign" to realize shallow copy:
var obj = {
name: 'wsscat',
age: 0,
add: {
a: 'beijing'
}
}
// var obj2 = Object.assign({}, obj);
var obj2 = obj;
obj2.age = 18;
obj2.add.a = 'shanghai';
console.log(obj)
console.log(obj2)
output:
{ name: 'wsscat', age: 18, add: { a: 'shanghai' } }
{ name: 'wsscat', age: 18, add: { a: 'shanghai' } }
I think you meant 'shallow copy', not 'shadow copy', so I will reference to the former in my answer.
By using assignment operator = you just copy reference, so obj2 points to the same object as obj, so changing property is reflected on both.
The way you used Object.assign, creates shallow clone by copying all own properties from source object obj to target object (empty) which is then assigned to variable obj2.
Primitive data types (null, undefined, String, Number, Boolean) are copied by value, so keys name and age on both objects contain different values in memory). Objects types (Object, Arrray, Function) are copied by reference, so object under add property is shared between both obj and obj2. Any change in obj2.add.a will be reflected on obj.add.a.
Take a look at polyfill implementation of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assigne/
EDIT: Another link with polyfill implementation: https://gist.github.com/spiralx/68cf40d7010d829340cb
EDIT2: Clarified difference between copying primitive types and objects.
Well answered question from Smyk but i want to mention a simple thing.
as he mentioned :
first attempt was a shallow copy of an object which is a good way to avoid pointing to the same reference with flat objects (not nested).
second attempt clearly was pointing to a reference (Array and Object are passing the reference of the variable).
More about that :
there is a problem with shallow copy of objects, because nested object/array is a reference again so in order to fix this, you may have heard about deep copy which is a way to allocate a separate memory location/address for the new object.
an example for that :
var student1 ={
name : "Ahmed",
company : "Tech",
address: {
city: "Tokyo"
}
}
var student2 = JSON.parse(JSON.stringify(student1))
student1.address.city = "Erbil";
console.log(student1.address.city);
console.log(student2.address.city);
More information, here's a link
also to go in depth read this

A more aesthetic way of copying an object literal?

This is client side. Webpack, Babel and Babel Imports.
My project has a folder called "models" which contains object literals as definitions of the expected JSON results from endpoints.
The objects only contain strings, ints, booleans and arrays/objects which contain those data types
eg:
{
name: "String"
age: 35,
active: true,
permissions: [
{ news: true }
]
}
When I want to use a model definition, in order to ensure I don't have issues with references, I must use:
let newObject1 = Object.assign({}, originalObj )
or
let newObject2 = JSON.parse( JSON.stringify( originalObj ))
I find this a bit ugly and it pollutes my code a bit.
I would love the ability to use the new keyword on object literals, but of course that's not a thing.
let clone = new targetObj
What's the most aesthetic way to handle the cloning of an object literal without creating a reference?
The JavaScript way of implementing such object "templates" are constructors:
function Original() {
this.name = "String";
this.age = 18;
this.active = true;
this.permissions = [
{ news: true }
];
}
var obj = new Original();
console.log(obj);
Or, in ES6 class syntax:
class Original {
constructor() {
this.name = "String";
this.age = 18;
this.active = true;
this.permissions = [
{ news: true }
];
}
}
let obj = new Original();
console.log(obj);
Be aware that Object.assign will only create a shallow copy, so it would not copy the permissions array, but provide a reference to the same array as in the original object.
const originalObject = {
name: "String",
age: 35,
active: true,
permissions: [
{ news: true }
]
};
let obj1 = Object.assign({}, originalObject);
let obj2 = Object.assign({}, originalObject);
// change a permission:
obj1.permissions[0].news = false;
// See what permissions are in obj2:
console.log(obj1.permissions);
const model = () => ({
name: 'string',
age: 20,
array: [ 1, 2, 3 ]
});
let newObject = model();
You won't have the pleasure of using new - see trincot's answer for that - but you don't have to worry about nested objects (assign) or feel gross (stringify + parse).

Categories