23

Why does spread syntax convert my string into an array?

var v = 'hello';
var [, ...w] = v; // ["e", "l", "l", "o"]

Why is w not a string?

Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • I guess this has something to do with the fact that strings are similar to arrays in JS? – enguerranws Jul 04 '17 at 08:02
  • 3
    In this case, the word "punctuator" refers only to the fact that `...` is not an identifier or literal, it also describes other tokens in the same category like `=` and `;` and so it's not a particularly descriptive or useful term when you're not writing a JavaScript parser. So "punctuator" is a low-level term with no semantic meaning. Just as we don't refer to `+` as a punctuator in `x + y`, we call it an operator, we don't call `...` a punctuator in `... x`, we call it spread syntax. The standard only names things that it needs to refer to elsewhere. – Dietrich Epp Jul 04 '17 at 09:51
  • 1
    Can you explain why you did expect `w` to be a string? – Bergi Jul 04 '17 at 11:23
  • I misunderstood the semantic to be "remainder" or "rest". The remainder of a string would therefore be... a string. The actual semantic is spread the rest into an `Array`. Actually none of the answers adequately address the type conversion thrust of my question, but together I now understand! – Ben Aston Jul 04 '17 at 11:34
  • @Bergi But why this type coercion in the context of destructuring? In a way the rest syntax is already ad-hoc polymorphic, because it allows any type that implements the iterable protocol. But then half way it implicitly converts the iterable to `Array`. This seems unnecessary. –  Jul 04 '17 at 12:28
  • @ftor what should the language do in this instance without the type change? `[,...w] = { [Symbol.iterator]: function*() { yield 1; yield 2; } }` – Ben Aston Jul 04 '17 at 12:35
  • 1
    Following the logic, the first iterator object `Object {value: 1, done: false}` should be discarded and `w` would have to reference the generator function suspended after the first `yield` expression. –  Jul 04 '17 at 13:08
  • 1
    @ftor I guess they could have made the rest variable be initialised with the remaining iterator, but then you'd have to convert it explicitly to a collection data structure if you want to use it more than once. The iterable protocol does not contain any `Type.fromIterable` logic. It's converted to an array because it uses array syntax, and because the index collection is the lowest common denominator indeed. – Bergi Jul 04 '17 at 15:16
  • @Bergi `if you want to use it more than once` another reason why stateful is bad –  Jul 04 '17 at 15:34

5 Answers5

24

Spread syntax (actually a punctuator as noted by RobG) allows for iterables to be spread into smaller bits. Since strings are iterables (they're character arrays internally, more specifically ordered sequences of integers representing characters), they can be spread into individual characters.

Next, destructuring assignment is performed on the array to unpack and group the spread values. Since you ommit the first element of the character array with , and don't assign a reference, it's lost, and the rest of the iterable object is saved into w, spread into it's individual parts, single characters of the character array.


The specific semantics of this operation are defined in the ECMAScript 2015 Specification by the ArrayAssignmentPattern : [ Elisionopt AssignmentRestElement ] production:

12.14.5.2 Runtime Semantics: DestructuringAssignmentEvaluation

with parameter value

[...]

ArrayAssignmentPattern : [ Elisionopt AssignmentRestElement ]

  1. Let iterator be GetIterator(value).
  2. ReturnIfAbrupt(iterator).
  3. Let iteratorRecord be Record {[[iterator]]: iterator, [[done]]: false}.
  4. If Elision is present, then
    a. Let status be the result of performing IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
    b. If status is an abrupt completion, then
        i. If iteratorRecord.[[done]] is false, return IteratorClose(iterator, status).
        ii. Return Completion(status).
  5. Let result be the result of performing IteratorDestructuringAssignmentEvaluation of AssignmentRestElement with iteratorRecord as the argument.
  6. If iteratorRecord.[[done]] is false, return IteratorClose(iterator, result).
  7. Return result.

Here, Elision refers to an omitted element when spreading with one or more commas (,), comparable to omitted syllables as the name suggests, and AssignmentRestElement refers to the target that will receive the spread and destructured values, w in this case.

What this does is first get the iterator of the object, from the internal @@iterator method and steps through that iterator, skipping however many elements indicated by the elision's width by the Elision production in IteratorDestructuringAssignmentEvaluation. Once that's done, it will step through the iterator of the AssignmentRestElement production, and assign a new array with all the spread values -- that's what w is. It receives the spread out single-character array, unpacked to exclude the first character.

The @@iterator method in which the iteration is gotten from is a well-known Symbol and changing it for an object can change how it's iterated, as in Emissary's answer. Specifically, the default implementation of the @@iterator method for String is as follows:

21.1.3.27 String.prototype [ @@iterator ]( )

When the @@iterator method is called it returns an Iterator object (25.1.1.2) that iterates over the code points of a String value, returning each code point as a String value.

Thus, the iterator allows for iteration through single code points, or characters of a string -- and consequently spreading the string will result in an array of its characters.

Community
  • 1
  • 1
Andrew Li
  • 55,805
  • 14
  • 125
  • 143
  • Does the spread operator ever return anything other than a single element or an array? – Ben Aston Jul 04 '17 at 08:19
  • @BenAston It doesn't. It always separates an iterable into its smallest parts them spreads them accordingly. – Andrew Li Jul 04 '17 at 08:24
  • 1
    @BenAston Actually it shouldn't even return anything other than an array. A single element is never returned IIRC. – Andrew Li Jul 04 '17 at 08:36
  • Yes, my mistake. Thanks. – Ben Aston Jul 04 '17 at 08:36
  • 1
    The example in my question would probably be more commonly be termed the "rest syntax" (not spread - my mistake). The rest syntax in this lexical position captures the remainder of an iterable and appears to perform a type conversion (if necessary) from the source iterable (eg a string in this instance) to an `Array`. I guess the reason for this is that creating a target iterable of the source type is actually not always possible (because an iterable is AFAIK just an object with a `Symbol.Iterator` property), so the common denominator `Array` is used. – Ben Aston Jul 04 '17 at 09:10
7

Spread syntax can be applied only to iterable objects. Since String is iterable Spread operator works fine and splits your char array(String) in to char's.

You can check that with below sample which demonstrate that String is iterable by default.

var s = 'test';    
for (k in s) {
  console.log(k);
}

And ECMAScript6 specification even mentioned about this specific String case.

Spread Operator

Spreading of elements of an iterable collection (like an array or even a string) into both literal elements and individual function parameters.

http://es6-features.org/#SpreadOperator

var str = "foo";
var chars = [ ...str ]; // [ "f", "o", "o" ]

And it is worth mentioning that this is a specific case and only happens when you use a direct String with spread operator. When you give a single String inside an array, the whole array will be treated as the iterable object and not the string inside.

var str = [ "hello" ,2 ];
var other = [  ...str ]; // [  "hello" ,2 ]

I know the above example doesn't make much sense, but just to convey the fact that the String will be treated differently in this case.

Community
  • 1
  • 1
Suresh Atta
  • 120,458
  • 37
  • 198
  • 307
  • 1
    Not my down vote, but there is no "spread operator", per the OP, it's "spread syntax" (edited). I guess the original MDN article got it wrong too, so the text has been updated but not the URL. – RobG Jul 04 '17 at 08:27
  • @RobG Well, the OP did originally have 'operator' but I edited it. Technically isn't an operator only supposed to evaluate to one singular result? Since spread syntax doesn't just evaluate to one item, it wouldn't be an operator. – Andrew Li Jul 04 '17 at 08:29
  • 1
    Axel Rauschmayer identifies both the spread and rest operator. http://exploringjs.com/es6/ch_destructuring.html#sec_rest-operator. The spec only mentions a `SpreadElement` AFAICT. – Ben Aston Jul 04 '17 at 08:31
  • Also: @Bergi thinks it qualifies as an operator. https://stackoverflow.com/questions/35029217/what-actually-is-and-is-not-an-operator – Ben Aston Jul 04 '17 at 08:38
  • @AndrewLi—I'm just going by ECMA-262. I certainly don't have enough technical knowledge to argue the case with Bergi. – RobG Jul 04 '17 at 08:46
  • 1
    @RobG I would go by ECMA-262. Honestly, I would prefer syntax over operator (just because I find it more correct) but if the spec says punctuator I'll go with that. – Andrew Li Jul 04 '17 at 08:48
  • @RobG - I just read the spec section you linked to. I'll go with punctuator then. – Ben Aston Jul 04 '17 at 08:49
  • @AndrewLi—in [*Ben Aston's (deleted) link*](https://stackoverflow.com/questions/35029217/what-actually-is-and-is-not-an-operator), Felix Kling has a good argument that it's not an operator. – RobG Jul 04 '17 at 08:50
  • @RobG I agree with Felix Kling here more than Bergi, but I have nowhere near their amount of knowledge so I'll go with the spec. – Andrew Li Jul 04 '17 at 08:51
  • JavaScript spec is a bit looser than others as to what "operator" is. E.g. in Ruby, `parse.y` tells you clearly what an operator (`op`) is (and all of them evaluate to an `arg`), and spread/rest isn't it. In JavaScript, `new` is an operator, parentheses are a "grouping operator"... I intuitively agree with @FelixKling's "special form of a function" (even though it fails with short-circuit ops like `&&`, `||` and `? :`), but for all its rigor in technical side, EcmaScript spec never actually defines "operator" properly. – Amadan Jul 04 '17 at 08:55
  • 1
    @BenAston Just a little update: I did some research, and I found it's *objectively* not an operator but syntax: [see my q&a here](https://stackoverflow.com/q/44934828/5647260). – Andrew Li Jul 05 '17 at 23:45
  • @RobG Just an update: I did go and ask on the ESDiscuss mailing list where (coincidentally) TJ Crowder responded. It seems as if there's a general consensus that it's syntax. I did some research myself and compiled into a [CW q&a](https://stackoverflow.com/q/44934828/5647260). – Andrew Li Jul 05 '17 at 23:54
7

In ES2015 the spread syntax is specifically acting against the internal String @@iterator property - any object can be iterated in this way by assigning your own iterator or generator / function* to the obj[Symbol.iterator] property.

For example you could change the default behaviour of your new array...

const a = [...'hello'];
a[Symbol.iterator] = function* (){
    for(let i=0; i<this.length; ++i)
        yield `${this[i]}!`;
};
console.log([...a]);

You could change your string iterator too but you'd have to explicitly create a String object.

Emissary
  • 9,954
  • 8
  • 54
  • 65
  • I thought the iterability of strings was new in ES2015? – Ben Aston Jul 04 '17 at 08:34
  • The spread operator was introduced in the latest spec. but you could iterate over a string like it was an array before, i.e. `s = 'foo'; s[0] === 'f';` – Emissary Jul 04 '17 at 08:36
  • That is not iteration. That is retrieval by index AFAIK. – Ben Aston Jul 04 '17 at 08:37
  • The point was the spread operator, `...thing` <- this notation is new and looks specifically for an iterator. – Emissary Jul 04 '17 at 08:39
  • The string is not iterable in that example. You are merely iterating through the string using the index notation. – Ben Aston Jul 04 '17 at 08:39
  • I think I know what you mean. It's just that "iterable" has a specific meaning in JavaScript. And according to that specific meaning, strings have not always been "iterable". Colloquially, you could say that you have always been able to iterate over the values in a string. – Ben Aston Jul 04 '17 at 08:41
  • In your `foreach.call` example you are merely using `foreach` as a generic method that leverages the integer indices (and presumably the `length` property). The string itself is not an `iterable` in the sense used in the specification. – Ben Aston Jul 04 '17 at 08:43
  • 1
    There is no "spread operator". `...` is a [*punctuator*](http://ecma-international.org/ecma-262/7.0/index.html#prod-Punctuator) that is used in spread syntax ([*SpreadElement*](http://ecma-international.org/ecma-262/7.0/index.html#prod-SpreadElement)) and rest parameters ([*FunctionRestParameter*](http://ecma-international.org/ecma-262/7.0/index.html#prod-FunctionRestParameter)). – RobG Jul 04 '17 at 08:44
0

There are two things going on here.

First, you're using array destructuring. This allows you to take an iterable and assign values from it to individual variables.

Second, you're using a rest parameter. This converts any remaining output of the iterable into an array.

So your string 'hello' is iterated into its individual characters, the first one is ignored (because you omitted the destination), then the remaining characters are turned into an array and assigned to w.

Neil
  • 54,642
  • 8
  • 60
  • 72
-1

Because a string in javascript is sort of treated like an array of characters.

For example, when you do a for each loop over a string (let's say hello), with lodash:

_.forEach('hello', function(i) {
    console.log(i)
})

it outputs:

h
e
l
l
o

Functions like slice() also work on both strings and arrays.

Jeffrey Roosendaal
  • 6,872
  • 8
  • 37
  • 55
  • Why do you need lodash in this example ? – Serge K. Jul 04 '17 at 08:05
  • 1
    Because I don't know how to do a regular javascript forEach loop anymore. – Jeffrey Roosendaal Jul 04 '17 at 08:07
  • 1
    A string in JavaScript is *not* an array of characters. – Amadan Jul 04 '17 at 08:30
  • 6
    A string is not "an array of characters". Arrays are a special kind of object, strings are primitives that provide convenient properties like length and whose characters can be accessed by index. So they fit the bill as [*"iterable"*](http://ecma-international.org/ecma-262/7.0/index.html#sec-iterable-interface), but are as much arrays as arguments, NodeLists and collections (none of which are arrays). – RobG Jul 04 '17 at 08:31
  • Ah, thanks for the clarification on that. I always assumed it was, since it can be iterated, has a `length` property, they can both use `slice()`, etc. – Jeffrey Roosendaal Jul 04 '17 at 08:35
  • `Array.prototype.forEach.call( "hello", x => { console.log( x ); } );` – MT0 Jul 04 '17 at 12:25