I don't understand about spread syntax inside objects.
console.log(...false) // TypeError not iterable
console.log(...1) // TypeError not iterable
console.log(...null) // TypeError not iterable
console.log(...undefined) // TypeError not iterable
I understand above codes that occurs error because of none-iterator.
But these codes are working well.
console.log({...false}) // {}
console.log({...1}) // {}
console.log({...null}) // {}
console.log({...undefined}) // {}
Please let me know why the above codes are working.
This is quite important in order to understand what's happening, so I have to start with it.
There is no spread operator defined in the language. There is spread syntax but as a sub-category of other types of syntax. This sounds like just semantics but it has a very real impact on how and why ...
works.
Operators behave the same way every time. If you use the delete
operator as delete obj.x
, then you always get the same result regardless of context. Same with typeof
or perhaps even -
(minus). Operators define an action that will be done in the code. It's always the same action. Someimes operators might be overloaded like +
:
console.log("a" + "b"); //string concatenation
console.log(1 + 2); //number addition
But it still doesn't vary with the context - where you put this expression.
The ...
syntax is different - it's not the same operator in different places:
const arr = [1, 2, 3];
const obj = { foo: "hello", bar: "world" };
console.log(Math.max(...arr)); //spread arguments in a function call
function fn(first, ...others) {} //rest parameters in function definition
console.log([...arr]); //spread into an array literal
console.log({...obj}); //spread into an object literal
These are all different pieces of syntax that look similar and behave similar but definitely not the same. If ...
were an operator, you can change the operands and still be valid but that's not the case:
const obj = { foo: "hello", bar: "world" };
console.log(Math.max(...obj)); //spread arguments in a function call
//not valid with objects
function fn(...first, others) {} //rest parameters in function definition
//not valid for the first of multiple parameters
const obj = { foo: "hello", bar: "world" };
console.log([...obj]); //spread into an array literal
//not valid when spreading an arbitrary object into an array
So, each use of ...
has separate rules and works not like any other use.
The reason is simple: ...
is not one thing at all. The language defines syntax for different things, like function calls, function definitions, array literals, and objects. Let's focus on the last two:
This is valid syntax:
const arr = [1, 2, 3];
// ^^^^^^^^^
// |
// +--- array literal syntax
console.log(arr);
const obj = { foo: "hello", bar: "world!" };
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// |
// +--- object literal syntax
console.log(obj);
But these aren't:
const arr = [0: 1, 1: 2, 2: 3];
//invalid - you cannot have key-value pairs
const obj = { 1, 2, 3 };
//invalid - you need key-value pairs
Not surprising - different syntax has different rules.
Again, the same applies to using ...
— [...arr]
and {...obj}
are just two different types of code you can use in JavaScript but there is no overlap between the ...
usages, just how you can use 1
both as [1]
and { 1: "one" }
but it's not the same meaning both times.
This is the real question that needs answering. After all, these are different operations.
Your sample with console.log(...false)
and console.log({...false})
demonstrate a function call and an object literal usage in particular, so I'll talk about those two. Just as a note, an array literal spread syntax [...arr]
would behave very similar in terms of what is valid and what isn't but it's not quite relevant here. The important thing is why objects behave differently, so we just need one example to compare against.
fn(...args)
The specs don't even have a special name for this construct. It's just a type of ArgumentList
and in section 12.3.8.1 Runtime Semantics: ArgumentListEvaluation (ECMAScript language specification link) it defines essentially "If the argument list has ...
then evaluate the code like this". I'll save you the boring language used in the specs (feel free to visit the link, if you want to see it).
The key point from the steps to be taken is that with ...args
the engine will try to get the iterator of args
. In essence that is defined by the iteration protocol (MDN link). For that, it will try calling a method defined with @@iterator
(or @@asyncIterator
). This is where you get a TypeError — it happens when args
doesn't expose such a method. No method, means it's not an iterable, and thus the engine cannot continue calling the function.
Just for completeness, if args
is an iterable, then the engine will step through the entire iterator until exhausted and create the arguments from the results. That means that we can use any arbitrary iterable with spread syntax in function calls:
const iterable = {
[Symbol.iterator]() { //define an @@iterator method to be a valid iterable
const arr = ["!", "world", "hello"];
let index = arr.length;
return {
next() { //define a `next` method to be a valid iterator
return { //go through `arr` backwards
value: arr[--index],
done: index < 0
}
}
}
}
}
console.log(...iterable);
{...obj}
There is still no special name for this construct in the specs. It's a type of PropertyDefinition
for an object literal. Section 12.2.6.8 Runtime Semantics: PropertyDefinitionEvaluation (ECMAScript language specification link) defines how this is to be processed. I'll spare you the definition again.
The difference comes in how exactly the obj
element is handled when spreading its properties. To do that, the abstract operation CopyDataProperties ( target, source, excludedItems )
(ECMAScript language specification link) is performed. This one is probably worth reading to better understand exactly what happens. I'll just focus on the important details:
With the expression {...foo}
target
will be the new objectsource
will be foo
excludedItems
will be an empty list, so it's inconsequentialIf source
(reminder, this is foo
in the code) is null
or undefined
the operation concludes and target
is returned from the CopyDataProperties
operation. Otherwise, continue.
Next important thing is that foo
will be turned into an object. This will use the ToObject ( argument )
abstract operation which is defined like this (reminder again that you won't get null
or undefined
here):
Argument Type | Result |
---|---|
Undefined | Throw a TypeError exception. |
Null | Throw a TypeError exception. |
Boolean | Return a new Boolean object whose [[BooleanData]] internal slot is set to argument. See 19.3 for a description of Boolean objects. |
Number | Return a new Number object whose [[NumberData]] internal slot is set to argument. See 20.1 for a description of Number objects. |
String | Return a new String object whose [[StringData]] internal slot is set to argument. See 21.1 for a description of String objects. |
Symbol | Return a new Symbol object whose [[SymbolData]] internal slot is set to argument. See 19.4 for a description of Symbol objects. |
BigInt | Return a new BigInt object whose [[BigIntData]] internal slot is set to argument. See 20.2 for a description of BigInt objects. |
Object | Return argument. |
We'll call the result of this operation from
.
All own properties in from
that are enumerable are written to target
with their values.
The spread operation completes and target
is the new object defined using the object literal syntax. Finished!
To summarise even more, when you use spread syntax with an object literal, the source that is being spread will be turned into an object first, and then only own enumerable properties will actually be copied onto the object being instantiated. In the case of null
or undefined
being spread, the spreading is simply a no-op: no properties will be copied and the operation completes normally (no error is thrown).
This is very different from how spreading works in function calls, as there is no reliance on the iteration protocol. The item you spread does not have to be an iterable at all.
Since the primitive wrappers like Number
and Boolean
don't produce any own properties, there is nothing to copy from them:
const numberWrapper = new Number(1);
console.log(
Object.getOwnPropertyNames(numberWrapper), //nothing
Object.getOwnPropertySymbols(numberWrapper), //nothing
Object.getOwnPropertyDescriptors(numberWrapper), //nothing
);
const booleanWrapper = new Boolean(false);
console.log(
Object.getOwnPropertyNames(booleanWrapper), //nothing
Object.getOwnPropertySymbols(booleanWrapper), //nothing
Object.getOwnPropertyDescriptors(booleanWrapper), //nothing
);
However, a string object does have own properties and some of them are enumerable. Which means that you can spread a string into an object:
const string = "hello";
const stringWrapper = new String(string);
console.log(
Object.getOwnPropertyNames(stringWrapper), //indexes 0-4 and `length`
Object.getOwnPropertySymbols(stringWrapper), //nothing
Object.getOwnPropertyDescriptors(stringWrapper), //indexes are enumerable, `length` is not
);
console.log({...string}) // { "0": "h", "1": "e", "2": "l", "3": "l", "4": "o" }
Here is a better illustration of how values would behave when spread into an object:
function printProperties(source) {
//convert to an object
const from = Object(source);
const descriptors = Object.getOwnPropertyDescriptors(from);
const spreadObj = {...source};
console.log(
`own property descriptors:`, descriptors,
`\nproduct when spread into an object:`, spreadObj
);
}
const boolean = false;
const number = 1;
const emptyObject = {};
const object1 = { foo: "hello" };
const object2 = Object.defineProperties({}, {
//do a more fine-grained definition of properties
foo: {
value: "hello",
enumerable: false
},
bar: {
value: "world",
enumerable: true
}
});
console.log("--- boolean ---");
printProperties(boolean);
console.log("--- number ---");
printProperties(number);
console.log("--- emptyObject ---");
printProperties(emptyObject);
console.log("--- object1 ---");
printProperties(object1);
console.log("--- object2 ---");
printProperties(object2);
The object spread is quite different. It maps to Object.assign()
internally.
So const a = {...1}
is same as const a = Object.assign({}, 1)
Here Object.assign({},1)
has treated 1
as object
not as number
. Therefore, you did not get any exception thrown.
Additionally if you have tried same thing for arrays [...1]
it should have thrown error, since it does not treats 1
as object
and you get the same behavior as ..1
.
To summarize:
console.log({...false}) => console.log(Object.assign({}, false))
console.log({...1}) => console.log(Object.assign({}, 1))
console.log({...null}) => console.log(Object.assign({}, null))
console.log({...undefined}) => console.log(Object.assign({}, undefined))
PS: Object.assign() spec
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With