While most of the modern frontend engineers use object spread syntax a lot in their code, we all overcome some simple details and underlying mechanisms of how it actually works.
With a first look, this code looks like something that would break right?
/*
object? = {...123} // => {} Wait what? Is this valid??
object? = {...undefined} // => {} Um, wat?
object? = {...null} => // {} Please stop
object? = {...false} => // {} Ok I'm done, bye javascript
object? = {...'Smallpdf'} // => {0: "S", 1: "m", 2: "a", 3: "l", 4: "l", 5: "p", 6: "d", 7: "f"}
*/
// Did we break javascript??!
Probably we would expect a TypeError
here. But we must not forget that ...
is a syntax code, not an operator. So the result of it depends on the surrounding context. It behaves differently if it's in an array ([...myArr]
), in an object ({...myObj}
), or in a function argument list (myFunc(arg1, ..restArgs
)
So let's see what happens exactly when it is used inside an object.
According to TC39, object spread initializer is a syntactic sugar on top of Object.assign
. So the next logical step is to see how the Object.assign
should work, as instructed by the ECMAscript spec.
In our case, when using the {...something}
syntax, the object expression ({}
) is the target
so it's a newly created object and sources
is whatever we pass after the ...
syntax, so in our case it's something
Now if something
is null
or undefined
we can see an explicit instruction of how Object.assign
should handle this, treat it like an empty List
so our end result will just ignore it. This explains why {...undefined}
and {...null}
returns an empty object and doesn't crash in any way.
But what happens with false
123
and 'Smallpdf'
? Let's go back to the ECMAscript spec
After explicitly handling undefined
and null
cases it concludes with the next steps:
So we see that for other types of arguments, (except null
or undefined
) the spec uses the ToObject
abstract operation, to convert the value to an object and if the return value is not undefined
it will try to use the enumerable properties of the result. Keep in mind that ToObject
conversions are described in the table below:
If we try to code this we will get the following results:
// ToObject conversion
const NumberObject = new Number(123);
const BooleanObject = new Boolean(false);
const StringObject = new String('Smallpdf');
// Get properties for each items, and return enumerable properties to our object
Object.getOwnPropertyDescriptors(NumberObject)
// => {}
// So object? = {...123} => {} makes sense
Object.getOwnPropertyDescriptors(BooleanObject)
// => {}
// object? = {...false} => {} yup
Object.getOwnPropertyDescriptors(StringObject)
/* =>
0: {value: "S", writable: false, enumerable: true, configurable: false}
1: {value: "m", writable: false, enumerable: true, configurable: false}
2: {value: "a", writable: false, enumerable: true, configurable: false}
3: {value: "l", writable: false, enumerable: true, configurable: false}
4: {value: "l", writable: false, enumerable: true, configurable: false}
5: {value: "p", writable: false, enumerable: true, configurable: false}
6: {value: "d", writable: false, enumerable: true, configurable: false}
7: {value: "f", writable: false, enumerable: true, configurable: false}
length: {value: 8, writable: false, enumerable: false, configurable: false}
*/
// So according to the spec, we take only the `enumerable: true` properties
// from this object. Finally we use their `keys` (0, 1, 2, 3, 4, 5, 6, 7)
and their `value` ('S', 'm', 'a', 'l', 'l', 'p', 'd', 'f') and add them
into our new object.
// object? = {...'Smallpdf'} // => {0: "S", 1: "m", 2: "a", 3: "l", 4: "l", 5: "p", 6: "d", 7: "f"}
// it all makes sense now
Javascript surely is weird, but if we follow the spec, it all makes sense! 🌈 🎉