Unexpected behavior from the spread syntax in JavaScript - javascript

I was experimenting with the spread syntax and am having difficulty making rational sense out of its behavior in a particular situation.
In one instance, when I use:
const art = ["hello"]
console.log( [{...art}] )
the return value is
=> [ { '0': 'hello' } ]
However, when I iterate over the single array value it produces an entirely different effect:
const art2 = art.map((item) => ({ ...item }))
console.log(art2)
=> [ { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' } ]
Why does using the spread syntax in the first example only combine it with a single index, but in the second example with .map being used break it down into different index elements? Since there is only a single item in the art array I would have assumed the results would be the same.

In the first code, you're spreading an array which contains one item, a string at the zeroth index:
console.log({ ...["hello"] });
All is as expected. But in the second code, you're calling .map on the array first, and then spreading the first argument provided to the .map function - the items being spreaded aren't arrays, but the items the array contains, which, in this case, is a string. When you spread a string, you'll get properties matching the values at each character index:
console.log(['hello'].map((item) => ({ ...item })))
// same as:
console.log({ ...'hello' });
They're entirely different situations.

Both results are as expected.
In first case you are passing whole array to spread operator
const art = ["hello"]
console.log( [{...art}] )
So spread operator is applied to whole array at once.
In second case, first you are iterating the array using .map() i.e. you are picking each item and passing that item to spread operator.
const art2 = art.map((item) => ({ ...item }))
console.log(art2)
So spread operator is applied to each item.

const art = ["hello"]
In above case, you tried to spread the array. So it will give you following output :
[ { '0': 'hello' } ]
Now you tried to execute following one
const art2 = art.map((item) => ({ ...item }))
So here you have used map. Map is something which takes one element of array and applies mapper to it.
In your case your mapper is to spread the given element. So now it will spread the element you have passed. So you got the output as :
[ { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' } ]

Related

Create an array of nested objects from an array of arrays

I'm trying to convert an array of arrays into an array of nested objects in JavaScript. Let's assume each subarray in the array represents a file path. I want to create an array of objects where each object has 2 properties, the name of the current file and any files/children that come after the current file/parent.
So for example, if I have this array of arrays where each subarray represents a file path:
[['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]
I want to get this as the result:
[
{
name :'A',
children: [
{
name: 'B',
children: [
{
name: 'C',
children: []
},
{
name: 'D',
children: []
}
]
}
]
},
{
name: 'L',
children: [
{
name: 'M',
children: [
{
name: 'N',
children: []
}
]
}
]
}
]
I tried mapping through the array of arrays and creating an object for the current file/parent if it hasn't been created yet. I think I may be on the right track but I can't seem to think of the best way to do so.
Something I could do in 5 minutes, it probably can be improved. This could also be written as a recursive function, I believe.
const result = [];
arr.forEach((subArr) => {
var ref = result;
subArr.forEach((name) => {
const obj = ref.find((obj) => obj.name == name);
if (obj) {
ref = obj.children;
} else {
ref.push({ name, children: [] });
ref = ref[ref.length - 1].children;
}
});
});
Here's mine:
// Function to convert path array ‘a’ from position ‘i’ into tree structure in ‘v’.
const tree = (a, i, v) => {
if (i < a.length) { tree(a, i+1, v[a[i]] ||= { }) }
}
// Function to convert a simple node into the desired record format.
const record = (v) => {
const a = [];
for (const [k, w] of Object.entries(v)) {
a.push({ name: k, children: record(w) });
}
return a;
}
const m = { }
for (const a of [['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]) {
tree(a, 0, m);
}
const result = record(m);
While it might be overkill for this requirement, I have handy a variant of one of my utility functions, setPath, that is used for adding such a path to an existing array. This is a bit different from the other answers in that it does this in an immutable way, returning a new object that shares as much structure as possible with the original one. I always prefer to work with immutable data.
Using that, we can write a hydrate function to do this job as the one-liner, const hydrate = (paths) => paths .reduce (setPath, []).
This is quite likely overkill here, as there is probably no reason to build your output one immutable level after another. But it is a demonstration of the value of keeping utility functions handy.
The code looks like this:
const call = (fn, ...args) => fn (...args)
const setPath = (xs, [name, ...names]) => call (
(i = ((xs .findIndex (x => x .name == name) + 1) || xs .length + 1) - 1) =>
name == undefined
? [...xs]
: [
...xs .slice (0, i),
{name, children: setPath ((i == xs .length) ? [] : xs [i] .children, names)},
...xs .slice (i + 1)
]
)
const hydrate = (paths) => paths .reduce (setPath, [])
console .log (
hydrate ([['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']])
)
.as-console-wrapper {max-height: 100% !important; top: 0}
We have a trivial call helper function that I use here to avoid statements. As much as possible, I prefer to work with expressions, as these avoid temporal notions in code brought on by statements, and they compose much better into larger pieces. We could alternatively do this with default parameters, but they have other problems. A trivial call function handles this nicely.
In our main function, we destructure apart the first name in the input from the remaining ones. Then we pass to call a function that calculates the index of the element with our existing name in the input array. If it doesn't exist, the index will be the length of that array. This is perhaps over-tricky. findIndex returns -1 if no element matches. We add 1 to the result, and then, if it's 0, we choose one more than the length of the array. Finally we subtract 1 from the result, and now the index will be where we found our target or the length of the array if it wasn't found.
Now, if the path is empty, we return a copy of our array. (I prefer the copy just for consistency, but it would be legitimate to just return it directly.) If it's not empty, we use the index to tear apart our input array, keeping everything before it, building a new item for that index by recursively calling setPath with the remaining node names, and then keeping everything after that index.
And now, as noted, our hydrate function is a trivial fold of setPath starting with an empty array.

Vue changes array in data after making a copy of it

So I have this code in vue:
export default {
name: 'Test',
data() {
return {
test1: ['1', '2', '3'],
test2: [{
name: 'Hello'
}, {
name: 'Number two'
}, {
name: 'What ever'
}],
};
},
created() {
const first = [...this.test1];
first.forEach((elm, index) => first[index] = 'New');
console.log('first: ', first);
console.log('test1 in data', this.test1);
const second = [...this.test2];
second.forEach(elm => elm.name = 'New');
console.log('second: ', second);
console.log('test2 in data', this.test2);
},
}
After setting the value of each item of the array 'first' (that should be a copy without reference to the data 'test1' array) each item is equal to 'new'. The value of this.test1 doesn't change.
I did the same with test2. Copied and changed the value of each item to 'New'. But now the value of the data array 'test2' also has 'New' in every item.
I have no clue why this is like that. Any ideas?
Spread syntax creates a shallow copy. If your array has primitive types like numbers or strings, it won't update the original array. That's the case with test1. In the second case, only a new array is created. If you push or pop from the array, original array won't be updated. But, the objects are still pointing to their same place in memory. Updating them will update original array's objects as well.
You can use the spread syntax on the individual object to create a copy of the objects:
const second = this.test2.map(o => ({...o}))
You can also use JSON.parse and JSON.stringify. But, if the objects have any function properties, they'll be removed.
const second = JSON.parse(JSON.stringify(this.test2))
The reason it is like that is because you are having an array of Vue data values. So even though you are cloning the Array, you are also copying over each values 'getters' and 'setters' which have a reference to the original array. In order to remove the getters and setters you should do what d-h-e has suggested.
You could also do this.
const second = this.test2.map(() => { name: 'New' } );
console.log('second: ', second);
console.log('test2 in data', this.test2);
Try it with:
const second = JSON.parse(JSON.stringify(this.test2));
The copy method with spreadoperator or Array.from works only with simple arrays.
For deep copy use the method with JSON.parse and JSON.stringify.

Set keyname with spread operator

Is it possible to dynamically set key name to spread operator?
For example I have:
'first, second, third'.split(',');
// Array(3) : [ 'first', 'second', 'third' ]
I want to have an object like this
{ 'first': 'first', 'second': 'second', 'third': 'third' }
By doing this right now I get:
{ ...'first, second, third'.split(',') };
// { 1: 'first', 2: 'second', 3: 'third' }
Can I dynamically set it or I have to iterate through and do it manually at this point?
I've ended up combine the two answers to use this:
const toObject = str => Object.assign(...str.split(/\s*,\s*/).map(key => ({ [key]: key })));
You could spread a list of key/value pairs into object assign:
Object.assign(...'first, second, third'.split(',').map(key => ({[key]: key})))
Jonas' solution is clever. I like it. Here's an alternative:
function toObject(str) {
const parts = str.split(/\s*,\s*/);
return parts.reduce((obj, part) => {
obj[part] = part;
return obj;
}, {});
}
console.log(toObject('first, second, third'));
Note that I use split(/\s*,\s*/) instead of split(',') to eliminate whitespace between parts.
This can be reduced to the following one-liner, if you're into that sort of thing:
const toObject = str =>
str.split(/\s*,\s*/).reduce((o, p) => (o[p] = p, o), {});
console.log(toObject('first, second, third'));

Adding one element in array in React-Redux

I have a project, where I use react-redux, and I have a reducer, which by idea should add one element in array and return new array. How I can do this?
/*---- Reducer ----*/
case CHANGE_EVENT_USERS:
return { ...state, users: payload };
/*---- Here's my hopeless tryings ----*/
userClickHandler() {
const { id, homeFloor, avatarUrl, login } = this.props;
const user = { id, homeFloor, avatarUrl, login };
this.props.changeEventUsers([...user]); // []
this.props.changeEventUsers(this.props.event.users.push()); // number
}
I'm not sure if I understand you correctly but from my understanding the solution to your problem would look something like this:
case CHANGE_EVENT_USERS:
return { ...state, users: [ ...state.users, action.payload ] };
I like better the syntax of concat.
In your reducer do:
case CHANGE_EVENT_USERS:
return users.concat(action.payload);
Do the add directly in the reducer.
From your component
this.props.changeEventUsers(newUser); // add the new user
In the reducer
return { ...state, users: [...state.users, payload] };
I made the assumption that "payload" contains the info coming from the action and the users array is by default initialised with an empty array value []
Use concat()
const initialArray = [1, 2, 3];
const elemenToAdd = 4;
const newArray= initialArray.concat([elementToAdd]);
[1, 2, 3, 4]
Notice I'm using const here to emphasize that the initial array was not mutated.
The great thing about the method above, it's that it can be used to chain operations together.
result = initialArray.concat(..).filter(..).concat(..);
(where .. represents skipped code details)
You can also use concat by passing in arrays as parameters:
newArray = concat(initialArray, [elementToadd])
Or use es7 spread operator syntax for array concatenating:
newArray = [...initialArray, elementToAdd, ...[5], [6] ];
[1, 2, 3, 4, 5, [6] ]
Use ... to send individual elements of the supplied array to be elements in the new array; without the dots, the supplied array is itself the element.
So in the your case, the line in question could be written as:
I found my solution:
this.props.changeEventUsers([...this.props.event.users, user]);

Observable transformation

I'm very new to observables, this might be a silly question for others but I can't find a way to do it. How do I convert
Observable<Array<T>> where T is a TypeScript class with property Id(string) into an Array<string>? I want to convert the Id properties into a string array.
You are trying to do 2 things here:
Flatten all arrays from the source array into a single observable of T
Map each instance of T to Id property
You can use mergeAll to flatten the observable as you would use reduce on a normal JS array.
const source = Rx.Observable.of(
[{ Id: '1' }, { Id: '2' }],
[{ Id: '3' }, { Id: '4' }]
);
const flattened = source.mergeAll();
flattened.subscribe(s => console.log(s));
// => {Id:'1'}, {Id:'2'}, {Id:'3'}, {Id:'4'}
You can then use map (as with a normal JS array) to extract the Id property
const mapped = flattened.map(s => s.Id);
mapped.subscribe(s => console.log(s))
// => '1', '2', '3', '4'
Pull it all into 1 statement and...
source
.mergeAll()
.map(s => s.Id)
.subscribe(s => console.log(s))
// => '1', '2', '3', '4'
You can use reduce operator that like on javascript array. RxJS reduce operator will operate on the whole stream when it is completed.
See the example:
let data$ = new Rx.Subject();
data$
.reduce((acc, el) => {acc.push(...el); return acc;}, [])
.subscribe(x=>console.log(x));
data$.next([{id:'1'}, {id:'2'}]);
data$.next([{id:'3'}, {id:'4'}]);
data$.complete();

Categories