I'm reading an introduction to Redux reducers () which contains the following example of a reducer:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
pleted: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
pleted: true
})
}
return todo
})
default:
return state
}
}
It seems from its documentation () that Object.assign()
will 'merge together' all the objects passed into it. In this case, however, todo
and {pleted: true}
are already objects, so I don't see the point of passing an empty object literal, {}
, as the first argument to Object.assign()
. Can anybody clarify this?
I'm reading an introduction to Redux reducers (https://redux.js/introduction/three-principles) which contains the following example of a reducer:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
pleted: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
pleted: true
})
}
return todo
})
default:
return state
}
}
It seems from its documentation (https://developer.mozilla/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) that Object.assign()
will 'merge together' all the objects passed into it. In this case, however, todo
and {pleted: true}
are already objects, so I don't see the point of passing an empty object literal, {}
, as the first argument to Object.assign()
. Can anybody clarify this?
- 1 Having absolutely no knowledge of Redux, I assume it's to avoid mutating the original objects and to return a new object. – juanpa.arrivillaga Commented Jul 13, 2018 at 21:41
4 Answers
Reset to default 9When you use Object.assign
, the first object you give it will have all the rest of the objects merged into it. That is to say, the first object will be mutated.
If you want to avoid mutating the objects you're merging, it's helpful to pass in the empty object as the first parameter to prevent any of the ponent objects from changing.
Here's an example demonstrating the difference:
const obj1 = {
foo: "bar"
}
const obj2 = {
key: "value"
}
// Here, obj1 is the same after the Object.assign call
console.log(Object.assign({}, obj1, obj2));
console.log(obj1)
console.log(obj2)
console.log("\n\n")
// Note that after this call, obj1 holds both keys. So this will mutate it:
console.log(Object.assign(obj1, obj2));
console.log(obj1) // This is different now
console.log(obj2)
If you don't pass an empty object in, the original todo
object will be modified. This may be what you want, but more often than not it isn't.
This is due to the way objects are all references, and are not cloned by default.
Short answer: Objects and Arrays are assignment by reference.
In this example, changing one will change the other, they are not immutable:
let x = {param:1}
const foo = (a) => {
a.param +=1;
console.log('response', x, a)
}
foo(x);
To fix that, we use Object.assign()
let x = {param:1}
const foo = (a) => {
let b = Object.assign({}, a);
b.param +=1;
console.log('response', b, x)
}
foo(x);
As CRice pointed out in their answer, this syntax is useful for "dumping"/copying all properties from multiple objects into one output without modifying the original objects.
I wanted to elaborate on an ES6 equivalent for readers who may be finding this from researching Object.assign
after seeing it laying around your codebase.
Background on References
First and foremost, it's important to understand that JavaScript Objects are passed by reference (technically referred to as passed by "sharing," but we can think of them the same for the purposes of this example). The 30,000-foot idea is that if the typeof yourVariable === 'object'
, any function that modifies yourVariable
(even if passed in as an argument) will also affect the outside world's copy. The reference is shared.
In the following code, let's create an object of some primitive types and an object of objects (remember, arrays also have typeof 'object'
).
const values = {
a: 0,
b: true,
c: 'abc',
d: undefined,
e: null,
};
const references = {
obj: {
f: "hello world"
},
arr: [1, 2, 3]
};
If we want to merge the properties of both values
and references
into one output object, we can use Object.assign
with the first argument (the target
) as an empty object:
const assign = Object.assign({}, values, references);
console.log(assign);
// Object {
// a: 0,
// b: true,
// c: "abc",
// d: undefined,
// e: null,
// obj: Object {
// f: "hello world"
// },
// arr: Array [1, 2, 3]
//}
Note that Object.assign
is not suitable for doing a deep merge as it only copies the topmost properties. That is, the first level of properties are copied while any nested properties that have a value that is a reference type are shared with the original object. We can more easily see this by modifying the original nested object and paring its reference with our assign
ed object reference. Note the unmodified array is also a shared reference:
references.obj.f = 'something else';
console.log(references.obj === assign.obj, references.arr === assign.arr);
// true, true
ES6 Version
For those of you familiar with ES6, you may already realize that the spread syntax does exactly what assign
did in the above example, including the same limitation of only copying top level properties. We can take the same two objects, and do a shallow clone of them using the spread syntax as follows:
const spread = { ...values, ...references };
console.log(spread);
// Object {
// a: 0,
// b: true,
// c: "abc",
// d: undefined,
// e: null,
// obj: Object {
// f: "something else"
// },
// arr: Array [1, 2, 3]
//}
And when updating the original reference, we still have a shared reference between the objects:
references.obj.f = 'another edit';
console.log(references.obj === spread.obj, references.arr === spread.arr);
// true, true