How does object spread work if it is not an iterable?

But is object spreading some kind of different creature than iterable spreading?

Yes. Property spread doesn't use iteration at all. It's new primary syntax, the runtime semantics of which are defined by the spec, and not in terms of iterables/iteration:

PropertyDefinition:...AssignmentExpression

  1. Let exprValue be the result of evaluating AssignmentExpression.
  2. Let fromValue be ? GetValue(exprValue).
  3. Let excludedNames be a new empty List.
  4. Return ? CopyDataProperties(object, fromValue, excludedNames).

Property spread is specifically for object properties, there's no additional generalization of it like there is with iterable spread. (Nor it is immediately obvious how there would be. :-) )

For your const arr2 = [...obj]; use-case, you'd probably want Object.entries:

const arr2 = Object.entries(obj);

Example:

const obj = {
  name: "doug",
  age: 234,
  profession: "seeker of Chthulhu"
};
const arr2 = Object.entries(obj);
console.log(arr2);

...or Object.keys if you just want property names, or Object.values if you just want values.

Of course, you can make an object iterable if you like: Just give it an iterator. For instance:

const obj = {
  name: "doug",
  age: 234,
  profession: "seeker of Chthulhu",
  * [Symbol.iterator]() {
    return yield* Object.entries(this);
  }
};
const arr2 = [...obj];
console.log(arr2);

And you can make instances of any classes you create iterable by defining an appropriate iterator for them and providing a Symbol.iterator-named property on the prototype:

class ExampleList {
    constructor() {
        this.head = this.tail = null;
    }

    add(value) {
        const entry = {value, next: null};
        if (!this.tail) {
            this.head = this.tail = entry;
        } else {
            this.tail = this.tail.next = entry;
        }
    }

    * [Symbol.iterator]() {
        for (let current = this.head; current; current = current.next) {
            yield current.value;
        }
    }
}

const list = new ExampleList();
list.add("a");
list.add("b");
list.add("c");

for (const value of list) {
    console.log(value);
}

That is it will work in some circumstances but not others...

Well, that's true of spread notation in general. Property spread is only defined within object initializers, and only works when the operand is some kind of object. (And its counterpart, the new property rest notation, is defined within destructuring assignment patterns.) Iterable spread is only defined in array initializers and function argument lists, and only works when its operand is some kind of iterable. (And its counterpart, iterable rest notation, is defined within destructuring assignment patterns that create arrays.)


T.J. Crowder's answer is correct but this is too long for a comment and hopefully gives you a better appreciation for the utility: consider the frequent case that you want a couple of properties from an object and put them in a new object. Several third party libraries like Ramda and lodash implement utility functions that do this, but with a combination of shorthand properties, destructuring, and object spread it can be done succinctly in vanilla JS:

const foo = { a: 1, b: 2, c: 3, d: 4 };
const { a, d } = foo;
const bar = { a, d };
console.log(bar); // { a: 1, d: 4 }

If you don't mind abusing the comma operator you can shorten this even further:

let a, d, bar, foo = { a: 1, b: 2, c: 3, d: 4 };
bar = ({a, d} = foo, {a, d});