Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I don't understand about spread syntax inside objects

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.

like image 315
kkangil Avatar asked Oct 30 '20 06:10

kkangil


2 Answers

There is no spread operator!

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.

What actually happens when you use spread in function call and spread into an object?

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.

Function call spread 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);

Object spread {...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:

  1. With the expression {...foo}

    • target will be the new object
    • source will be foo
    • excludedItems will be an empty list, so it's inconsequential
  2. If 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.

  3. 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.

  1. All own properties in from that are enumerable are written to target with their values.

  2. 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);
like image 142
VLAZ Avatar answered Sep 19 '22 08:09

VLAZ


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

like image 41
bUff23 Avatar answered Sep 20 '22 08:09

bUff23