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?
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 ]
Let iterator be GetIterator(value).
ReturnIfAbrupt(iterator).
Let iteratorRecord be Record {[[iterator]]: iterator, [[done]]: false}.
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).
Let result be the result of performing IteratorDestructuringAssignmentEvaluation of AssignmentRestElement with iteratorRecord as the argument.
If iteratorRecord.[[done]] is false, return IteratorClose(iterator, result).
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.
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.
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.
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.
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.
Related
I have the following piece of code that defines an array, then an iterator object with a generator yields the value from this array and I output each value with the spread operator:
const arr = ['0', '1', '4', 'a', '9'];
const my_obj = {
[Symbol.iterator]: function*() {
for(let index of arr) {
yield `${index}`;
}
}
};
const all = [...my_obj]
console.log(...my_obj)
The result is:
0
1
4
a
9
What I don’t understand is how is the spread operator variable “...my_obj” getting the values of the array if “my_obj” is an object, not an array. From what I understand: “my_obj” is reciving an object and if you apply the spread operator it should get the “key:value”.
Could someone explain how did it obtain the values?
The spread operator and for...of statements call the iterable protocol of an object. Some objects, like Array, String, Set and Map have built in iterable protocols. That means that they have the ##iterator method.
You yourself just created an object and gave it a property of [Symbol.iterator]. Now your object knows what to do when the spread syntax of for...of is called on this object, which is to call this iterator and loop over an iterable object created by the generator function in the [Symbol.iterator] key.
And in your generator function you've specified that on each iteration a value of the arr should be yielded until that loop is complete.
MDN showns an example here where it says:
Some built-in constructs—such as the spread syntax—use the same iteration protocol under the hood:
console.log([...someString]); // ["h", "i"]
A pattern you will see sometimes is that an objects has a values method. Most of the times this is actually a generator function. When the [Symbol.iterator] is called it returns the generator function which then loops over the values in the object.
Down here I've created a little demo which takes in a string and loops over the letters with for...of and lays them out with the spread operator. The [Symbol.iterator] will call a generator function that looks up each letter's position in the alphabet. Both are using the same iterable protocol.
class LettersObjects {
*values() {
// Here we use the iterable protocol of a string.
for (const letter of this.letters) {
const position = this.alphabet.indexOf(letter.toLowerCase()) + 1;
yield position.toString().padStart(2, '0');
}
}
// This is called with for...of and ...spread.
[Symbol.iterator]() {
return this.values();
}
constructor(letters) {
this.letters = letters;
this.alphabet = 'abcdefghijklmnopqrstuvwxyz';
}
}
const letters = new LettersObjects('Hello world');
// Call the iterable protocol on our LetterObjects instance.
for (const letter of letters) {
console.log('loop:', letter);
}
// Call the iterable protocol on our LetterObjects instance.
console.log('Spread:', ...letters);
This happens because an object is an iterable.
The spread operator spreads iterables and not arrays specifically. it can spread objects, even strings for that matter.
Spread operator documentation
The spread operator allows an iterable such as an array expression or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.
We can easily grasp the concept of iterables by making one of our own.
For instance, we have an object that is not an array, but looks suitable for for..of.
When for..of starts, it calls that method once (or errors if not found). The method must return an iterator – an object with the method next.
Onward, for..of works only with that returned object.
When for..of wants the next value, it calls next() on that object.
The result of next() must have the form {done: Boolean, value: any}, where done=true means that the iteration is finished, otherwise value is the next value.
If the object satisfies for iterable, i.e. for..of loop. It can be spread using the spread operator and can be assigned in array because array is also an iterable.
Ref:
iterables in javascript
spread operator
What kind of magic is that, when you Object.keys() on an object with one key only, it will be treated as a string when you reference later using square brackets?
See below for example:
let chosenProducts = [
{
toys: 20
}
];
let toys = "toys";
chosenProducts.forEach(product => {
let name = Object.keys(product);
console.log(Array.isArray(name)); // true
console.log(name) // ['toys']
console.log(toys) // "toys" - string
console.log(product[name]); // 20 - array used = wtf?
console.log(product[toys]); // 20 - string used
});
Object.keys returns an array.
All objects have a toString method.
If you use an object in string context, toString is called implicitly.
Square-bracket property accessor notation is, essentially, string context (unless you pass a Symbol)
The default toString method on an array looks something like: function () { return this.join(","); }
const example1 = ["foo", "bar"];
console.log("" + example1);
const example2 = ["baz"];
console.log("" + example2);
Object.keys returns an array of keys. Your name variable is an array with one element ['toys']. When you access product[name] implicit coercion of name array to string happens which results in product["toys"]
In javascript there are two types of coercions implicit and explicit coercion
When you access with Object's properties they will be string, in case of an array they will be indexes (numbers), but array is also an object, when you add any property in array it can be act as object property(string) and will not get counted towards the length of array.
let arr = [1,3];
arr.val = 20;
console.log(arr.length); // 2
In your example you are accessing object's property product[name] here name is an array. What will happen here is implicit coercion of array to string.
Javascript will automatically call toString method on your object which is an array in this case. There is toString method for Array which is linked to their prototype Array.prototype.toString. You can override that if you want to get different result.
In simple terms when you try to access variable in if conditional expression javascript automatically coerce to Boolean value.
let a = 10;
if(a){
console.log("implicit coercion");
}
if(Boolean(a)){
console.log("explicit coercion");
}
To learn more about Javascript grammar and types.
We all know Strings somewhat behaves like an Array.You can even apply some of the array methods to it and take benefits, like the below example.
[].filter.call('abcdef',function(val){
return val<'e';
});
Also,
var a='xyz';
I can access first element using a[0] or I can call a.length like an Array
My question is why String behaves like an Array. and if it does, why do I get false below when I check if it is an instance of Array. Is String Array-like?
'a' instanceof Array
All that Array.prototype.filter really requires is that the variable being iterated over has a length property, and that the variable has numeric-indexed values. See (part of) the polyfill:
var len = this.length >>> 0,
res = new Array(len), // preallocate array
t = this, c = 0, i = -1;
if (thisArg === undefined){
while (++i !== len){
// checks to see if the key was set
if (i in this){
if (func(t[i], i, t)){
res[c++] = t[i];
}
}
}
}
Strings fulfill this condition - strings have a length property, and numeric indicies accessed on the string resolve to individual characters.
But you can do the same thing with arbitrary objects:
const obj = {
0: 'foo',
1: 'bar',
length: 2
};
const result = [].filter.call(obj, val => val.startsWith('f'));
console.log(result);
You could say that obj is array-like as well, since it has a length property and numeric indicies. Most array methods like .filter, .reduce, etc can be .called on array-like objects, even if those objects aren't actual arrays.
(Technically, you can also call array methods on non-array-like objects too, it just won't do anything useful - no iterations may be performed)
To use instanceof you need to create an instance of an Object and a is not an instance of one. It is a primitive or also known as string literal:
String literals (denoted by double or single quotes) and strings
returned from String calls in a non-constructor context (i.e., without
using the new keyword) are primitive strings. JavaScript automatically
converts primitives to String objects, so that it's possible to use
String object methods for primitive strings. In contexts where a
method is to be invoked on a primitive string or a property lookup
occurs, JavaScript will automatically wrap the string primitive and
call the method or perform the property lookup.
For example:
let foo = 'abc' // primitive
let boo = new String() // object
console.log(foo instanceof String) // false
console.log(boo instanceof String) // true
console.log(typeof foo) // 'string' <-- notice not String
console.log(typeof boo) // object
This is simply due to:
The instanceof operator tests the presence of constructor.prototype in
object's prototype chain
But as we explained above we are dealing with string literals which are created without a constructor call (new keyword) and only boxed for our convenience when operating on them. They are not actual instances of String and thus instanceof returns false.
The reason you can use Array.filter on the string primitive is simply due to the fact that it was boxed for you to a String from where it got the length property at the time of execution.
For example in the case of V8 engine string primitive is parsed/boxed to String and a String to a StringObject. Notice they are actually different instances.
Array.filter only cares about that length property and numeric indicies as pointed nicely by CertainPerformance which are provided by the boxing to String. Example:
console.log(Object.getOwnPropertyNames('a')) // ["0", "length"]
However String is not StringObject and thus instanceof would return false.
On the one hand, a lack of an equivalent to Python 3's range is an annoyance in ES6. On the other hand, there are lots of workarounds. My question is why one workaround I tried actually works. To illustrate:
[...Array(10).keys()];
In case the reason I find this mysterious is not obvious, note that Array(10).keys() is at least apparently empty.
I'm aware this wastefully creates two arrays, as do most of the popular workarounds, and, that (at the cost of creating a generator function) using a generator can avoid that. E.g.,
[...(function*(){let i = 0; while(i<10) yield i++;})()];
My question is only about why the first workaround produces the desired result.
Edit:
Judging from the answers, some people believe that the evaluation of Array(10) is equivalent to the evaluation of Array.apply(null,Array(10)). They are not. For example, .hasOwnProperty(0) is false for the former but true for the latter. However, I am open to being persuaded they are the same in some way that matters here, since my understanding is clearly lacking at some key point. I suspect that the answer is that the result of iterating over the keys is determine by the length property, which both share, rather than the actual array indexes that have been defined. If so, I would like to know that this behavior is normative.
Array#keys returns an Array iterator. Spread syntax then completely exhausts that iterator by accessing the next value in the iterator until there are no more values. Then it collects all the values from the iterator and spreads them into a new array.
Array(10) creates an array exotic object that does not have any actual integer-indexed keys, but just a length property -- so it would 'empty' but Array(10).keys() is not. The fact is that using Array#keys doesn't depend on actual elements, just the length property. The internal operation CreateArrayIterator creates a key iterator from an array by creating an iterator via the intrinsic %ArrayIteratorPrototype% object. Looking at %ArrayIteratorPrototype%.next(), you'll see that the length of the array is used. For Array#keys the index is continually incremented until it reaches the length of the array. That's how the iterator is created that gives you all the keys of the array without actually having said integer keys in the first place.
If you're curious about the abstract steps, see Section 12.2.5.2 ArrayAcculumation of the ECMAScript Language Specification, particularly the SpreadElement : ... AssignmentExpression production which outlines the process of stepping through an iterator that is used in conjunction with spread syntax.
To see the abstract steps for collecting these values into a new array, see Section 12.2.5.3 Evaluation. Specifically, the ArrayLiteral : [ ElementList ] production is the production [...Array.keys()] falls under. The aforementioned ArrayAcculumation process is performed which aggregates iterates through the iterator and sets them into the new array.
Array.prototype.keys is returning a new Array Iterator.
Array(10) is an array of 10 (empty) slots.
When you spread the array's keys, you are iterating over it and creating a new array. But this time the new array's items are the slots from the first array.
You can see it with for of loop:
const arr = Array(10).keys();
for (let key of arr) {
console.log(key);
}
By the way, you can use Array.from which takes a map function as it's second arguments. so you can just return the index or whatever you want:
const arr = Array.from(Array(10), (_, idx) => idx);
console.log(arr);
Edit
As a followup to your edited question about the Array(10) result:
The DOCS mentions:
If the only argument passed to the Array constructor is an integer
between 0 and 232-1 (inclusive), this returns a new JavaScript array
with its length property set to that number (Note: this implies an
array of arrayLength empty slots, not slots with actual undefined
values). If the argument is any other number, a RangeError exception
is thrown.
So you actually get:
A new array
The length property is updated with the number provided to the
function
You get n-slots
Now .keys will create an Iterator, which will return each slot index as the value.
for example:
let it = Array(10).keys();
and calling:
it.next();
Will return:
{value: 0, done: false}
Calling it again and again will yield
{value: n, done: false}
up until it will get to the last slot:
{value: 9, done: false}
then the next .next call will return
{value: undefined, done: true}
which will flag that this is the end of the iterations.
running example:
const it = Array(10).keys();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next()); // done: true
Array(10) creates a length-10 array, each element of which is undefined.
Array(10).keys() returns an iterator, which iterates over the keys of the array...the numbers 0 through 9.
When the spread syntax is used to define an array literal (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator), it fully-iterates any iterator it's given and adds the returns from the iterator to the created array (in order).
I had a generic question about JavaScript arrays. Are array indices in JavaScript internally handled as strings?
I read somewhere that because arrays are objects in JavaScript, the index is actually a string. I am a bit confused about this, and would be glad for any explanation.
Formally, all property names are strings. That means that array-like numeric property names really aren't any different from any other property names.
If you check step 6 in the relevant part of the spec, you'll see that property accessor expressions are always coerced to strings before looking up the property. That process is followed (formally) regardless of whether the object is an array instance or another sort of object. (Again, it just has to seem like that's what's happening.)
Now, internally, the JavaScript runtime is free to implement array functionality any way it wants.
edit — I had the idea of playing with Number.toString to demonstrate that a number-to-string conversion happens, but it turns out that the spec explicitly describes that specific type conversion as taking place via an internal process, and not by an implicit cast followed by a call to .toString() (which probably is a good thing for performance reasons).
That is correct so:
> var a = ['a','b','c']
undefined
> a
[ 'a', 'b', 'c' ]
> a[0]
'a'
> a['0']
'a'
> a['4'] = 'e'
'e'
> a[3] = 'd'
'd'
> a
[ 'a', 'b', 'c', 'd', 'e' ]
Yes, technically array-indexes are strings, but as Flanagan elegantly put it in his 'Definitive guide':
"It is helpful to clearly distinguish an array index from an object property name. All indexes are property names, but only property names that are integers between 0 and 232-1 are indexes."
Usually you should not care what the browser (or more in general 'script-host') does internally as long as the outcome conforms to a predictable and (usually/hopefully) specified result. In fact, in case of JavaScript (or ECMAScript 262) is only described in terms of what conceptual steps are needed. That (intentionally) leaves room for script-host (and browsers) to come up with clever smaller and faster way's to implement that specified behavior.
In fact, modern browsers use a number of different algorithms for different types of arrays internally: it matters what they contain, how big they are, if they are in order, if they are fixed and optimizable upon (JIT) compile-time or if they are sparse or dense (yes it often pays to do new Array(length_val) instead of ninja []).
In your thinking-concept (when learning JavaScript) it might help to know that arrays are just special kind of objects. But they are not always the same thing one might expect, for example:
var a=[];
a['4294967295']="I'm not the only one..";
a['4294967296']="Yes you are..";
alert(a); // === I'm not the only one..
although it is easy and pretty transparent to the uninformed programmer to have an array (with indexes) and attach properties to the array-object.
The best answer (I think) is from the specification (15.4) itself:
Array Objects
Array objects give special treatment to a certain class of property
names. A property name P (in the form of a String value) is an array
index if and only if ToString(ToUint32(P)) is equal to P and
ToUint32(P) is not equal to 232−1. A property whose property name is
an array index is also called an element. Every Array object has a
length property whose value is always a nonnegative integer less than
232. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a
property of an Array object is created or changed, other properties
are adjusted as necessary to maintain this invariant. Specifically,
whenever a property is added whose name is an array index, the length
property is changed, if necessary, to be one more than the numeric
value of that array index; and whenever the length property is
changed, every property whose name is an array index whose value is
not smaller than the new length is automatically deleted. This
constraint applies only to own properties of an Array object and is
unaffected by length or array index properties that may be inherited
from its prototypes.
An object, O, is said to be sparse if the following algorithm returns
true:
Let len be the result of calling the [[Get]] internal method of O with argument "length".
For each integer i in the range 0≤i<ToUint32(len)
a. Let elem be the result of calling the [[GetOwnProperty]] internal method of O with argument ToString(i).
b. If elem is undefined, return true.
Return false.
Effectively the ECMAScript 262 spec just ensures to the JavaScript-programmer unambiguous array-references regardless of getting/setting arr['42'] or arr[42] up to 32-bit unsigned.
The main difference is for example (auto-updating of) array.length, array.push and other array-sugar like array.concat, etc.
While, yes, JavaScript also lets one loop over the properties one has set to an object, we can not read how much we have set (without a loop). And yes, to the best of my knowledge, modern browsers (especially chrome in what they call (but don't exactly specify)) 'small integers' are wicked fast with true (pre-initialized) small-int arrays.
Also see for example this related question.
Edit: as per #Felix Kling's test (from his comment above):
After arr[4294967294] = 42;, arr.length correctly shows 4294967295. However, calling arr.push(21); throws a RangeError: Invalid array length. arr[arr.length] = 21 works, but doesn't change length.
The explanation for this (predictable and intended) behavior should be clear after this answer.
Edit2:
Now, someone gave the comment:
for (var i in a) console.log(typeof i) shows 'string' for all indexes.
Since for in is the (unordered I must add) property iterator in JavaScript, it is kind of obvious it returns a string (I'd be pretty darned if it didn't).
From MDN:
for..in should not be used to iterate over an Array where index order
is important.
Array indexes are just enumerable properties with integer names and
are otherwise identical to general Object properties. There is no
guarantee that for...in will return the indexes in any particular
order and it will return all enumerable properties, including those
with non–integer names and those that are inherited.
Because the order of iteration is implementation dependent, iterating
over an array may not visit elements in a consistent order. Therefore
it is better to use a for loop with a numeric index (or Array.forEach
or the for...of loop) when iterating over arrays where the order of
access is important.
So.. what have we learned? If order is important to us (often is with arrays), then we need this quirky array in JavaScript, and having a 'length' is rather useful for looping in numerical order.
Now think of the alternative: Give your objects an id/order, but then you'd need to loop over your objects for every next id/order (property) once again...
Edit 3:
Someone answered along the lines of:
var a = ['a','b','c'];
a['4'] = 'e';
a[3] = 'd';
alert(a); // returns a,b,c,d,e
Now using the explanation in my answer: what happened is that '4' is coercible to integer 4 and that is in the range [0, 4294967295] making it into a valid array index also called element. Since var a is an array ([]), the array element 4 gets added as array element, not as property (what would have happened if var a was an object ({}).
An example to further outline the difference between array and object:
var a = ['a','b','c'];
a['prop']='d';
alert(a);
see how it returns a,b,c with no 'd' to be seen.
Edit 4:
You commented: "In that case, an integer index should be handled as a string, as it is a property of the array, which is a special type of JavaScript object."
That is wrong in terms of terminology because: (strings representing) integer indexes (between [0, 4294967295]) create array indexes or elements; not properties.
It's better to say: Both an actual integer and a string representing an integer (both between [0, 4294967295]) is a valid array index (and should conceptually be regarded as integer) and creates/changes array elements (the 'things'/values (only) that get returned when you do arr.join() or arr.concat() for example).
Everything else creates/changes a property (and should conceptually be regarded as string).
What the browser really does, usually shouldn't interest you, noting that the simpler and clearer specified you code, the better chance the browser has to recognize: 'oh, let’s optimize this to an actual array under the hood'.
Let's see:
[1]["0"] === 1 // true
Oh, but that's not conclusive, since the runtime could be coercing "0" to +"0" and +"0" === 0.
[1][false] === undefined // true
Now, +false === 0, so no, the runtime isn't coercing the value to a number.
var arr = [];
arr.false = "foobar";
arr[false] === "foobar" // true
So actually, the runtime is coercing the value to a string. So yep, it's a hash table lookup (externally).
In JavaScript there are two type of arrays: standard arrays and associative arrays (or an object with properies)
[ ] - standard array - 0 based integer indexes only
{ } - associative array - JavaScript objects where keys can be any strings
So ...
var arr = [ 0, 1, 2, 3 ];
... is defined as a standard array where indexes can only be integers. When you do arr["something"] since something (which is what you use as index) is not an integer you are basically defining a property to the arr object (everything is object in JavaScript). But you are not adding an element to the standard array.