Related
Background knowledge
We can mutate an array using splice and filter. filter is not a mutable method, but splice is a mutable method. So if we use splice to an array, then the original array will be mutated.
The problem
Now, we are going to do forEach to an array and if a condition matches, we will remove an element using splice and filter. Let's see:
1. splice
let arr = [1, 2, 3, 4];
arr.forEach(el => {
console.log(el);
if (el === 2) {
arr.splice(el, 1); // remove element 2
}
});
Yeah that's what we expect. That prints 1, 2, 4. Because in the middle of the loop we mutate the arr, the result is broken.
2. filter
let arr = [1,2,3,4];
arr.forEach(el => {
console.log(el);
if (el === 2) {
arr = arr.filter(el => el !== 2); // remove element 2
}
});
However, as you can see, that prints 1, 2, 3, 4 even if we mutate the arr in the middle of the loop!
The question
We've mutated two arrays in the middle of the loop in a similar way, but the results are different! What happened?
Why does splice affect original array but filter not?
That is simply how Array.prototype.splice, Array.prototype.filter and Array.prototype.forEach are defined. splice is meant to modify the array (and return removed values) while filter is meant to return a copy of the array with only the matching values
Array.prototype.forEach iterates over the array in ascending index order. Even though you modify the array during e.g. index 3, next iteration it'll just go to the next index, e.g. 4, unless the end of the array is already reached. Mind that once .forEach is called on an array, it'll keep working on that array. You setting arr to another value doesn't affect the array nor the state of the .forEach call. Similar to a regular function call:
let someVariable = 5;
function test(param) {
someVariable = 6;
console.log(param); // still prints 5
}
test(someVariable);
After all, JavaScript works by reference (or primitive values), you're not passing a pointer to the someVariable variable like you can do in some other languages.
One point of clarification: in the OP's first example he's actually deleting the third value in the array rather than the second one despite the comment indicating he meant to delete the second element (at least, that is what I think he was going for based on the subsequent example).
One would fix that problem by using the second parameter passed to the forEach callback as follows:
let arr = [1, 2, 3, 4];
arr.forEach((el, index) => {
console.log(el);
if (el === 2) {
// actually remove the second element instead of the element at index 2
arr.splice(index, 1);
}
});
What I find interesting is that even if the semantic error is fixed, the console will still show what the OP mentioned:
1
2
4
This, despite the resulting value of arr being set to [1, 3, 4] by the splice() call.
What happened? The MDN has a similar example regarding modifying an array inside a forEach loop. Basically, the callback one passes to forEach is invoked for every index of the list until it reaches the length of the list. In the second invocation, the underlying logic is pointing to index 1 and the callback deletes that index, moving everything currently following that index forward one index in the array: the value 3 is moved to index 1 and the value 4 is moved to index 2. Because we've already iterated over index 1, the third invocation will be invoked on index 2 which now contains the value 4.
The following table is another way to see this:
Iteration
value of el
arr value before callback
arr value after callback
1
1
[1, 2, 3, 4]
[1, 2, 3, 4]
2
2
[1, 2, 3, 4]
[1, 3, 4]
3
4
[1, 3, 4]
[1, 3, 4]
Basically, you can think of Array.prototype.forEach being defined similarly to the following:
Array.prototype.forEach = function(callbackFn, thisArg) {
for (let index = 0; index < this.length; ++index) {
callbackFn.call(thisArg, this.at(index), index, this)
}
}
The difference between the two examples is that in the first one, the OP is using splice to modify the object referenced by the variable arr "in place" (as noted in the MDN doc). In the second example, the OP is changing the variable arr to point to a new object; however, because forEach is a function, the original object referenced by arr will be kept in scope by the forEach closure until the function completes. This becomes a little easier to see when you add a little more logging to the example using the third parameter passed to the callback (the array against which the callback was executed).
let arr = [1,2,3,4];
arr.forEach((el, index, list) => {
console.log("el:", el, "list:", list, "arr:", arr);
if (el === 2) {
arr = arr.filter(el => el !== 2); // remove element 2
}
});
This modified example will produce the following output:
el: 1 list: [1, 2, 3, 4] arr: [1, 2, 3, 4]
el: 2 list: [1, 2, 3, 4] arr: [1, 2, 3, 4]
el: 3 list: [1, 2, 3, 4] arr: [1, 3, 4]
el: 4 list: [1, 2, 3, 4] arr: [1, 3, 4]
One can see that the value of list, which comes from the forEach closure, never changes despite arr getting overwritten during the second iteration.
Is there a way to get the array being generated by the map function, inside the map function? For e.g.
let arr = [1, 2, 3, 4];
arr.map(item => {
console.log(currentArray);
return item * item;
}
So, in the above example, if at line 3 (currentArray), I want below output, how can I get that.
[1]
[1, 4]
[1, 4, 9]
The "array" parameter in map callback, returns the original array on which map function is called.
Or do I need to go conventional for loop route to achieve this?
You could map the squares by using a closure over ta temporary result set.
let array = [1, 2, 3, 4],
result = array.map((t => v => t = [...t, v * v])([]));
console.log(result);
You can use reduce function as follow,
let arr = [1, 2, 3, 4];
arr.reduce((acc, item)=>{
acc.push(item*item);
console.log(acc);
return acc;
}, []);
// SINCE YOU ARE ASKING HOW TO DO IT USING MAP
console.log('*******************************')
let arr1 = [1, 2, 3, 4];
const currentArray = [];
arr1.map(item => {
currentArray.push(item * item);
console.log(currentArray);
return item* item;
})
You can use the reduce instead of the map, but you do not have to by the way. Anyways, then you will have your accumulator as the first argument to the reducer callback. The variable accumulator will hold the intermediate states of the value to be returned.
let arr = [1, 2, 3, 4];
let result = arr.reduce((accumulator, item) => {
console.log("currentArray accumulator: ", accumulator);
return [...accumulator, item * item];
}, []);
There is a neat another answer around which uses map and do what you want. So, yes it is possible to achieve same with map and reduce is a convenience function that actually is a special case map function itself.
Does anyone knows how to directly call a array key of a Map Object.
As shown in below code, I can map.get(arr), but not map.get([0, 1, 2, 3])
const map = new Map()
const arr = [0,1,2,3]
map.set(arr, "I am some number")
map.get(arr) // "I am some number"
map.get([0,1,2,3]) // undefined
You can't. Map compares objects by object identity. [0, 1, 2, 3] !== [0, 1, 2, 3] as they are different objects, even if they hold the same data.
The nearest thing you can do is to try to convert the array to something you can compare meaningfully:
const map = new Map()
const arr = [0,1,2,3]
map.set(JSON.stringify([0, 1, 2, 3]), "I am some number")
console.log(map.get(JSON.stringify([0, 1, 2, 3])))
That's correct, you have to use the same array (as in map.get(arr)), not just an equivalent array. Key comparison is like === (except that NaN matches itself). So just like this shows false:
console.log([0, 1, 2, 3] === [0, 1, 2, 3]); // false
...using map.get([0, 1, 2, 3]) is guaranteed not to find anything, because there isn't any entry in the map keyed by that array.
Separate arrays aren't === to each other - your arr does not refer to the same array container as the [0,1,2,3] that you pass to map.get. To do something like this, you'd have to iterate over the map's keys and find the one whose values all match:
const map = new Map()
const arr = [0,1,2,3];
map.set(arr, "I am some number")
// Get a reference to the same `arr` whose key you set previously:
const arrKey = [...map.keys()].find(
key => Array.isArray(key) && JSON.stringify(key) === JSON.stringify([0, 1, 2, 3])
);
console.log(map.get(arrKey));
(but this is a pretty ugly thing to have to do - if you find yourself having to do this, usually it'd be better to use a different data structure)
You need the same object reference for getting the value from a Map.
If you like to use a starting part of the array as key, you need to get all keys from the map and check against with the new array.
var map = new Map,
key0 = [0, 1, 2, 3],
key1 = [0, 1, 2, 3];
map.set(key0, "I am some number");
console.log(map.get(key0)); // "I am some number"
for (let key of map.keys())
if (key.join('|') === key1.join('|'))
console.log(map.get(key));
I am trying to use the splice method to add the values from the first array into the second array in the index location provided by the third parameter. I was confident that my solution would work, even verified that my understanding of slice was correct. However, when I log this in console it returns as an empty array.
I feel like I am really close, but something is missing.
function frankenSplice(arr1, arr2, n) {
//copies made as to not disrupt referece.
let array1 = arr1.slice(0, arr1.length);
let array2 = arr2.slice(0, arr2.length);
let mutatedArray = array2.splice(n,0,...array1);
return mutatedArray;}
frankenSplice([1, 2, 3], [4, 5], 1) should return [4, 1, 2, 3, 5].
Also, is this a good usage of creating copies of the arrays in the function? I'm learning about referencing non-primitives and if I'm not mistaken creating the copies in the scope of the function protects the original reference from being modified. It's not pointless in this context is it?
The problem is that the return value for splice is not the array that got mutated, but the removed elements, in an array:
Return value: An array containing the deleted elements. If only one element is removed, an array of one element is returned. If no elements are removed, an empty array is returned.
You aren't removing any elements with splice, so the result is an empty array. Don't assign the result of splice to a variable, and return array2 instead:
function frankenSplice(arr1, arr2, n) {
//copies made as to not disrupt referece.
let array1 = arr1.slice(0, arr1.length);
let array2 = arr2.slice(0, arr2.length);
array2.splice(n, 0, ...array1);
return array2;
}
console.log(frankenSplice([1, 2, 3], [4, 5], 1))
Also note that you can use slice() without any arguments to create a shallow copy of an array, no need for (0, arr.length):
function frankenSplice(arr1, arr2, n) {
//copies made as to not disrupt referece.
let array1 = arr1.slice();
let array2 = arr2.slice();
array2.splice(n, 0, ...array1);
return array2;
}
console.log(frankenSplice([1, 2, 3], [4, 5], 1))
Another option is to immediately return an array into which you spread sliced sections of the original arrays, which might be clearer than using splice:
function frankenSplice(arr1, arr2, n) {
return [
...arr2.slice(0, n),
...arr1,
...arr2.slice(n)
];
}
console.log(frankenSplice([1, 2, 3], [4, 5], 1))
As a continuation of my min/max across an array of objects I was wondering about the performance comparisons of filter vs map.
So I put together a test on the values in my code as was going to look at the results in FireBug.
This is the code:
var _vec = this.vec;
min_x = Math.min.apply(Math, _vec.filter(function(el){ return el["x"]; }));
min_y = Math.min.apply(Math, _vec.map(function(el){ return el["x"]; }));
The mapped version returns the correct result. However the filtered version returns NaN. Breaking it out, stepping through and finally inspecting the results, it would appear that the inner function returns the x property of _vec but the actual array returned from filter is the unfiltered _vec.
I believe my usage of filter is correct - can anyone else see my problem?
Here's a simple test:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>S:GTC Map Test</title>
</head>
<body>
<script type="text/javascript">
function vector(x,y,z) { this.x = x; this.y =y; this.z=z; }
var vec = [];
vec.push(new vector(1,1,1));
vec.push(new vector(2,2,2));
vec.push(new vector(2,3,3));
var _vec = vec;
min_x = Math.min.apply(Math, _vec.filter(function(el){ return el["x"]; }));
min_y = Math.min.apply(Math, _vec.map(function(el){ return el["x"]; }));
document.write("<br>filter = " + min_x);
document.write("<br>map = " + min_y);
</script>
</body>
</html>
No, the filter method doesn't return the unfiletered array. It returns an array containing the items where the inner function returns true.
As you are not returning a boolean value from the inner function, the value is converted to boolean, so the object reference is converted to true. Thus, it returns a new array that contains all the items from the original array.
The filter method doesn't do the same as the map method. The map method is used to convert each item of an array, while the filter method is used to select certain items of an array. Comparing the performance between the methods is moot, as only one of them does what you want to do.
Quoted from:
JavaScript: The Definitive Guide
by David Flanagan
map()
The map() method passes each element of the array on which it is
invoked to the function you specify, and returns an array containing
the values returned by that function.
For example:
a = [1, 2, 3];
b = a.map(function(x) { return x*x; }); // b is [1, 4, 9]
The function you pass to map() is invoked in the same way as a
function passed to forEach(). For the map() method, however, the
function you pass should return a value.Note that map() returns a new
array: it does not modify the array it is invoked on. If that array is
sparse, the returned array will be sparse in the same way: it will
have the same length and the same missing elements.
filter()
The method returns an array containing a subset of the elements of the
array on which it is invoked. The function you pass to it should be
predicate: a function that returns true or false. The predicate is
invoked just as for forEach() and map(). If the return value is true ,
or a value that converts to true, then the element passed to the
predicate is a member of the subset and is added to the array that
will become the return value.
Examples:
a = [5, 4, 3, 2, 1];
smallvalues = a.filter(function(x) { return x < 3 }); // [2, 1]
everyother = a.filter(function(x,i) { return i%2==0 }); // [5, 3, 1]
// MAP creates a new array
// MPA return new Array
var arr = [1, 2, 3, 4, 5, 6, 7];
var newArr = arr.map((el) => {
return el * 2;
});
console.log(newArr); //2,4,3,8,10,12,14
// filter() return new Array
var newFilter = arr.filter((el) => {
return el * 2;
});
console.log(newFilter); // 1,2,3,4,5,6,7
now you can see I have return el*2 both for map and filter they are giving a different output
Filter()
The filter() method creates a new array filled with all array elements that pass a test implemented by the function.
If this conditional returns true, the element gets pushed to the output array. If the condition returns false, the element does not get pushed to the output array.
var arr = [1, 2, 3, 4, 5, 6, 7];
var newFilter = arr.filter((el) => {
return el > 3;
});
console.log(newFilter); //[1, 2, 3, 4]
Map()
The map() method is used for creating a new array from an existing one, applying a function to each one of the elements of the first array
var arr = [1, 2, 3, 4, 5, 6, 7];
var newArr = arr.map((el) => {
return el * 2;
});
console.log(newArr); //2,4,3,8,10,12,14