Array destructuring and spread operator - javascript

It's not 100% clear to me how this piece of code works:
var a = [1, 2, 3];
[x, y, ...a ] = [0, ...a, 4];
// OUTPUT:  [0, 1, 2, 3, 4]
I'm deconstructing the array a using the ... operator.
I am expecting that in second line a bunch of assignments will take place.
The x will be assigned to 0, y will be assigned to ...a (which passes the elements in the array a as individual values).
It's not clear to me, though, how the ...a get assigned to 4. In fact, JS throws an exception when doing:
...a = 4;
// Uncaught SyntaxError: Rest parameter may not have a default initializer
Why does this code output the modified array with the end 4, instead of throwing an exception? How does this work exactly?

It is executed like following
var a = [1, 2, 3];
[x, y, ...a ] = [0, ...a, 4];
[x, y, ...a ] = [0, 1, 2, 3, 4];
which means first value in RHS array is assigned to x, second value in RHS array is assigned to y and the remaining values are assigned to a.
Hence, value of x is 0, y is 1 and a is [2, 3, 4]

It's not clear to me, though, how the ...a get assigned to 4.
It's not.
Lets split things up a little:
On the right hand side of the assignment you are using an array literal with a spread element. The value of a is "flattened" into the new array.
Thus, [0, ...a, 4] is is equivalent to [0].concat(a, [4]). The result is a new array.
var a = [1, 2, 3];
console.log('spread element', [0, ...a, 4]);
console.log('concat', [0].concat(a, [4]));
On the left hand side you are using array destructuring with a rest element. [x, y, ...a ] means
assign the first value of the iterable to x
assign the second value of the iterable to y
assign the remaining values of the iterable as an array to a
These two are equivalent:
var a = [1,2,3,4];
var [x, y, ...z] = a;
console.log('destructuring', x, y, z);
var x = a[0];
var y = a[1];
var z = a.slice(2);
console.log('manual + slice', x, y, z);
Of course combining these two is perfectly fine. In an assignment, the left hand side doesn't care what how the right hand side is computed and vice versa.
What's confusing about your example is that you are using a again in the destructuring assignment, but that's the same as overriding the value of a with a new value. However the end result is
[0, ...a, 4] results in [0,1,2,3,4] therefor
x has value 0
y has value 1
a has value [2,3,4]
In fact, JS throws an exception when doing: ...a = 4;
The error message you are getting is strange. It should really be just a syntax error.
... by itself doesn't mean anything. It's not an operator, it's a punctuator (like ; or ,) that has a different meaning depending on the context it is used (and allowed).
See also What is SpreadElement in ECMAScript documentation? Is it the same as Spread operator at MDN?

...a is either equal to .slice(start, end) (left, destructuring) or to .concat(a) (right, spreading):
[0, ...a, 4]
is equal to:
[0].concat(a).concat([4]) // [0,1,2,3,4]
Whereas:
[x, y, ...a] = array
Is equal to:
x = array[0];
y = array[1];
a = array.slice(2);

In the first example, spread ie ... in LHS acts as gatherer whereas on the RHS it acts as spread/rest. IE you are assigning value to variable a when it is on LHS.
var a = [1, 2, 3];
[x, y, ...a ] = [0, ...a, 4];
console.log(a)
Let's go step by step:
Let's start with RHS. Doing [0, ...a, 4] will generate [0, 1, 2, 3, 4]. See for yourself:
var a = [1, 2, 3];
console.log([0, ...a, 4]);
Now, the LHS is the side where assignment is taking place. On RHS, imagine any variable with spread operator as an array ready to be assigned new values.
So, we are trying to assign [0, 1, 2, 3, 4] to a variable x, then to y and the rest to array a (in that order). So, array a will have whatever will be left after first two assignments (ie 2, 3, 4).
var a = [1, 2, 3];
// a will get overwritten
[x, y, ...a ] = [0, 1, 2, 3, 4];
// same as [x, y, ...a ] = [0, ...a, 4];
console.log(a);
Finally, coming to your last question: "It's not clear to me, though, how the ...a get assigned to 4? "
Answer: It is not. But if you do something like [...a] = [4], it will result in an array named a containing [4].
You can read more about spread syntax here (MDN) and here (YDKJS).

Related

Getting array length by destructuring assignment

After reading the MDN doc with examples of Destructuring assignment, and there is an example like:
const [a, b, ...{ pop, push }] = [1, 2];
console.log(a, b); // 1 2
console.log(pop, push); // [Function pop] [Function push]
and it says: This allows you to simultaneously unpack the properties and indices of arrays.
I took a second thought, isn't it will be fascinating if I could get the length of the array by doing this assignment at the same time getting the indices values out, so I tried this in my console:
const arr = [1, 2]
const [x, y, ...{length}] = arr
console.log(x) // 1
console.log(y) // 2
console.log(length) // 0 | ??? isn't it should give me 2?
// how about this...
const arr = [1, 2]
const {length} = arr
console.log(length) // 2 | ok well, it did work this way, but why not the destructuring assignment?
So, does anybody know why the length did not got assigned with value 2 in the first destructuring assignment? or did I found a bug in JavaScript?
What this does
[a, b, ...something] = someArray
is it takes the rest of the elements (index 2 and after) from someArray and puts them into a single variable as an array.
Doing
...{ pop, push }
takes two properties from that array: Array.prototype.pop and Array.prototype.push. But there aren't any more elements, because the original array had only 2 items, so doing
...{ length }
will give you 0.
If the original array did have more items, you'd see a length. For example:
const arr = [1, 2, 3, 4, 5]
const [x, y, ...{length}] = arr;
console.log(length);
The first two items are put into x and y. The last three items are taken as an array and then the length property is destructured from that array - so the length is 3.
Either add the number of destructured items previously to the length to get the actual length
const arr = [1, 2]
const [x, y, ...{length}] = arr
const actualLength = length + 2; // because 2 items were destructured; x and y
console.log(x) // 1
console.log(y) // 2
console.log(actualLength);
Or use object destructuring syntax instead
const arr = [1, 2]
const { 0: x, 1: y, length } = arr;
console.log(x) // 1
console.log(y) // 2
console.log(length);
Or, more understandable at a glance than either of the above - just do const { length } = arr on a separate line.
Let's see what happens when you only take one element off
const arr = [1, 2]
const [x, ...{length}] = arr
console.log("x", x) // 1
console.log("length", length) // 1
Notice how the length is one, because you start with 2 elements, then get rid of 1, and 2 - 1 is 1.
Why does this happen? ... means "rest", so it's the rest of the properties.
const arr = [1, 2]
const [x, ...rest] = arr
console.log("x", x) // 1
console.log("rest", rest) // [2]
When you use { } you get a property off of the rest, so it's the same as saying
const arr = [1, 2]
const [x, ...rest] = arr
console.log("x", x) // 1
const {length} = rest;
console.log("rest", rest) // [2]
console.log("length", length) // 1

Counting common elements of two Arrays in JavaScript (reduce function)

I met an unexpected result when I count the number of common elements in two arrays. When I use reduce function, it doesn't work. But filter version returns correct result. I want to know what's wrong with my reduce version.
var ls = [1, 2, 3, 4, 5];
var ms = [4, 5, 6, 1, 2];
console.log(ls.reduce((acc, e) => (ms.includes(e) ? 1 + acc : 0), 0));
// 2, incorrect
console.log(ls.filter(e => ms.includes(e)).length);
// 4, correct
Because in your reduce version, when the element is not found, you reset the acc back to zero instead of returning it as is
var ls = [1, 2, 3, 4, 5];
var ms = [4, 5, 6, 1, 2];
console.log(ls.reduce((acc, e) => (ms.includes(e) ? 1 + acc : acc), 0));
// -----------------------------------------------------------^ here
// 4, now corrected
When your callback function that's the first parameter of the reduce function hits the third item in the ls array, "3", it finds that it's not a member of the ms array. This causes the ternary operator to return the right hand expression 0, which resets your accumulator variable, acc. This will restart the count at 0.
Instead, you should return the current value of the accumulator variable instead of 0 like this:
console.log(ls.reduce(acc,e) => (ms.includes(e) ? 1 + acc: acc), 0));
That will give you the correct count of 4 matching elements.

Update array at different indexes with the same value in one go

Say I have an array a = [8, 3, true, 9, false], and an indices array b = [1, 3, 4]. Is it possible to do something like this:
a[b] = false;
So that a becomes [8, false, true, false, false]?
That syntax won't work here, since b is an array, JS will try and convert b to a primitive by calling toString() on that array. Since b.toString() results in "1,3,4" you'll end up adding "1,3,4" as a property in your array:
const a = [8, 3, true, 9, false];
const b = [1, 3, 4];
a[b] = false; // adds property "1,3,4" to the array `a`, sets it to false
console.log(a["1,3,4"]); // false
You would need to manually loop through the indexes in b, and for each index set that index within a to false. This could be done with a regular for loop or forEach():
const a = [8, 3, true, 9, false];
const b = [1, 3, 4];
b.forEach(idx => a[idx] = false);
console.log(a); // [8, false, true, false, false]
If you want to keep this immutable and not modify the original array, one idea could be to use Object.assign() to merge and overwrite the indexes specified in b:
const a = [8, 3, true, 9, false];
const b = [1, 3, 4];
const res = Object.assign([], a, ...b.map(idx => ({[idx]: false})));
console.log(res);
There is no native support from the language but you can easily write a function for this.
const at = (ii, y) => (x, i) => ii.includes(i) ? y : x;
It is a curried function which takes a list of indexes ii and a new value y for these indexes. Then it returns a function that takes a value x and an index i. If i belongs to ii then returns new value y otherwise keep value x.
This makes it convenient to inline it when mapping an array:
[8, 3, true, 9, false].map(at([1, 3, 4], false));
// ^ ^ ^
// 1 3 4
//
//=> [8, false, true, false, false]
If you want to be able to assign different values at different indexes, then we can use the following data structure:
[[xi, xv], [yi, yv], [zi, zv], ...]
It represents a list of pairs where the first element represents the index and the second element represents the new value.
To support this we need to rewrite at.
For each (x, i) it tries to find a pair where the 1st el. is equal to i. If there's a result return the pair, otherwise return a temporary pair [, x]. Finally access the 2nd element of the pair which will be the value at that index.
const at = (...ys) => (x, i) => (ys.find(([j]) => j === i) || [, x])[1];
We can still inline it as above:
[8, 3, true, 9, false].map(at([1, 'x'], [3, 'y'], [4, 'z']));
// ^ ^ ^
// 1 3 4
// x y z
//
// [8, "x", true, "y", "z"]

Lodash union won't work with the spread operator

I am using lodash and it's union function. But when I use a spread operator within union I am no longer getting the normal, expected result. What am I doing wrong and why won't lodash work with the spread operator? Thanks!
x = [1, 2, 3, 4]
y = [3, 5]
normalResult = _.union(x, y)
unexpectedResult = _.union(...x, y)
// normalResult = [1, 2, 3, 4, 5]
// unexpectedResult = [3, 5]
_.union expects each argument to be an array. When you use ...x you're spreading the array into separate arguments, which is not what it wants.
The spread operator would be useful if you had a 2-dimensional array and you wanted to merge each of the contained arrays with _.union, e.g.
x = [1, 2, 3, 4]
y = [3, 5]
a = [x, y];
result = _.union(...a);

Javascript object reference breaks after changing initial object's value

I am trying to understand a point of confusion I have with JavaScript objects. Specifically, I am interested in finding what, if anything, causes an object reference to break.
To demonstrate the phenomenon, I have included a copy of some output from Chrome's JavaScript console. Note that I am working with arrays here, but we would expect objects to behave similarly given the subtle distinction between arrays and objects in JS. I have added comments for clarity.
// Set x to some array literal
> x = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
// Set y to x
> y = x
[1, 2, 3, 4, 5]
> x
[1, 2, 3, 4, 5] // as expected
> y
[1, 2, 3, 4, 5] // as expected
As demonstrated above, both x and y output the expected value. Now I shuffle the values of x using a function called shuffle (specified at the bottom of this question).
// Shuffle x
> x = shuffle(x)
[5, 1, 4, 2, 3]
> x
[5, 1, 4, 2, 3] // x changes as expected
> y
[5, 1, 4, 2, 3] // y changes as expected
Again, everything works as expected above. The variables x and y have maintained reference to the same object. However, when we repeat this operation, the results are strange.
// Shuffle x
> x = shuffle(x)
[3, 1, 5, 4, 2]
> x
[3, 1, 5, 4, 2] // x changes as expected
> y
[5, 1, 4, 2, 3] // y didn't change this time
Below is the shuffle function, adapted from here. Its purpose is to shuffle the contents of an array (parameter r1) and to return the first n items of the mixed array.
function shuffle(r1,n) {
var i = r1.length, j, tempi, tempj, r2;
r2 = r1;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
tempi = r2[i];
tempj = r2[j];
r2[i] = tempj;
r2[j] = tempi;
}
return r2.slice(0,n);
}
I have since fixed the problem by rewriting my shuffle function based on this function. However, I would still like to understand what's going on. For a quick look at the code in action, I have made a jsFiddle.
Any ideas? I appreciate your time.
If you remove the .slice(0,n);, it will behave the way you expect. slice makes a new array.
So the first time you call shuffle, within your loop you modify the array x = y = r1 = r2. Then you make a copy of it on that last line and assign that to x. Now x !== y, but they contain the exact same elements. You can test that they are distinct objects after your first call to shuffle:.
The next time you call shuffle you are shuffling the copy of x you made and y is untouched.
.slice() makes a shallow copy of the Array, and so you're overwriting x with a new Array.
// The original was shuffled, but now `x` is a new Array
x = shuffle(x);
That's why y showed the first shuffle (because you hadn't sliced it yet), but none thereafter. The subsequent shuffle was on the overwritten x, and y still references the original.
If you wanted to truncate the original Array, just change its .length.
So instead of this:
return r2.slice(0,n);
Do this:
r2.length = n;
...though you're not passing anything to n currently.

Categories

Resources