While on the nodejs REPL I was trying to clean up an array defined as const array = [...] and just found out that using array.forEach(() => /pop|shift/()) would not work. After such expression the array will still hold values in it.
I'm well aware of better methods to clean the array, like array.splice(0), but I'm really curious about this behavior as seems counter-intuitive, at least for me.
Here's the test:
const a = [1, 2, 3]
a.forEach(() => {
a.shift()
})
console.log(a) // [ 3 ]
const b = [1, 2, 3]
b.forEach(() => {
b.pop()
})
console.log(b) // prints [ 1 ]
Notes
At first I was using arr.forEach(() => arr.pop()), so I though that one of the values was short-circuiting the forEach but wrapping the lambda in a body-block { .. } will also produce the same results.
The results are consistent across different node versions and browsers .. so it seems like it's well-defined behavior.
The quantity of leftover values, those still in the result array, change depending on the length of the input array, and seems to be Math.floor(array.length / 2)
The leftover values are always ordered accordingly to the /pop|shift/ method used, so some of the calls are actually changing the input array.
It also returns the same results by calling Array.prototype.forEach(array, fn)
Check out this quote from here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
If the values of existing elements of the array are changed, the
value passed to callback will be the value at the time forEach()
visits them; elements that are deleted before being visited are not
visited.
You're iterating from the beginning and removing the last element each iteration. This means you're moving forward 1, and reducing the length by 1, each iteration. Hence why you're ending up with floor(initialLength / 2) iterations. You're modifying the same array that you're forEaching, which as stated above means that you will not have the callback invoked for those pop'd elements.
Modifying an array while iterating over it is generally a bad idea. In fact, in Java, trying to do so would cause an exception to be thrown. But let's convert the forEach into an old-school for loop, and maybe you'll see the issue.
for (let i = 0; i < a.length; ++i) {
a.pop();
}
Is it clearer now what's going on? Each iteration you're shortening the length of the array by 1 when you pop the last element off. So the loop will end after iterating over half the elements -- because by then, it will have REMOVED half the elements, too, causing the value of i to be more than the current length of the array.
The same thing is happening when you use forEach: you're shortening the array with each iteration when you pop, causing the loop to terminate after only half the elements have been iterated. In other words, the iterator variable will move forward past the end of the array as the array shrinks.
.pop
Let's do this instead:
let arr = 'abcde'.split('');
arr.forEach((x,i,a) => {
console.log('before', x,i,a);
arr.pop();
console.log('after', x,i,a);
});
console.log(arr);
Your index is incrementing but your length is decrementing, so you're deleting the last elements when your index is in the first elements, thus the result where you delete the right half of the array.
.shift
Same: the iterating index goes one way, the length in another, so the whole things stop mid-work:
let arr = 'abcde'.split('');
arr.forEach((x,i,a) => {
console.log('before', x,i,a);
arr.shift();
console.log('after', x,i,a);
});
console.log(arr);
Related
In the following example, splice().splice() didn't work. What am I missing? How can I chain two splice methods in a single line? Thank you for any advice!
function test() {
var data = [0,1,2,3,4,5];
data.splice(0,2).splice(-1); // Removed only the first two elements, but not the last element.
console.log(data) // Returned [ 2, 3, 4, 5 ]
var data = [0,1,2,3,4,5];
data.splice(0,2); // Removed the first two elements.
data.splice(-1); // Removed the last element.
console.log(data) // Returned [ 2, 3, 4 ]
}
splice returns the removed elements, so chaining them in a single statement won't really work - you don't want to operate on the removed elements, you want to operate on the original array both times.
Usually, in this sort of situation, the right approach is to use slice instead, which is both easier to work with (just specify a start (inclusive) and end (exclusive) index) and is more functional (you get a new array instead of mutating an existing one - it's nice to avoid mutation when possible, makes code more understandable).
const data = [0,1,2,3,4,5];
console.log(data.slice(2,5));
You can do this, although I don't recommend this because it makes the code confusing
const data = [0,1,2,3,4,5];
console.log(data.splice(2,4).splice(0,3)); // Returned [2,3,4]
This question already has answers here:
Filter and delete filtered elements in an array
(10 answers)
Closed 3 years ago.
I am iterating over an array of strings using forEach(), and testing each element's length to determine wether it is even or odd. If the length of the string is even, it will be removed using splice().
My input and output are shown below, and as you can see even though (i think) my conditions are correct, in my return array, I still get an even, two character word - which should have been spliced out.
Code:
function filterOddLengthWords(words) {
words.forEach(function(element, index) {
if (element.length%2===0) {
words.splice(index, 1);
}
})
return words;
}
var output = filterOddLengthWords(['there', 'it', 'is', 'now']);
console.log(output); // --> [ 'there', 'is', 'now' ]
I understand where the error is, but I just don't know how to compensate for it. I could possibly rewrite this by creating an empty array at the beginning of the function, and then testing each element against the inverse condition, using the push() method to add each positive to the empty array. However, that is more inefficient and I'm curious to see if my way is possible. Thanks for the answers in advance.
By splicing an array, you change the index, but forEach takes the elements in advance and the old indices.
Usually by using splice, the iteration is started from the end and if some item is remoce, the index remains for the items before.
You could filter the array.
function filterOddLengthWords(words) {
return words.filter(function(element) {
return element.length % 2;
});
}
var output = filterOddLengthWords(['there', 'it', 'is', 'now']);
console.log(output);
The problem with splicing the array in the forEach() is that when you splice() on the array at index 1 which has the element it the element at index 2 is is moved to index 1.
So in the next iteration when the index is 2 in the callback the element at the index 2 of the array is the value now instead of the element is. As now is odd it is kept but the element is is completely skipped.
Using Array.prototype.filter will work here as it does not modify the original array but instead collects the valid results into a new array.
I am getting some strange behaviour out of JavaScript array.length. In particular, I have an array that returns length as 3, when there is only one element in the array, at index 0. I've read this question/answer dealing with incorrect values returned by array.length, but it doesn't answer my question because my array doesn't seem to be associative.
Here's a screenshot of my console, demonstrating the odd behaviour.
The code is throwing an error because I'm looping over the first array using array.length and the code is trying to acccess the second and third elements it thinks should be in the array, and not finding them. Other entries in the database seem to not have this problem (see the second array in the screenshot).
So here's the question: Why is array.length in the first array 3 instead of 1?
The length Array property in javascript is writable from anyone and is not a reliable source to find the number of elements in the array.
Usually, it is safe to assume that the array has length elements in it, but sometime you can have different behaviours. This one is one of them.
var x = [1,2,3];
x.length = 5;
using a for construct will lead to some undefined values
for (var i = 0; i < x.length; i++) {
alert(x[i]);
}
using the forEach Array method would result (on Firefox at least) in the desired behaviour.
x.forEach(function(item) {
alert(item);
});
Note that, if you change the array length to a lesser value than the real value, the array would lose the extra elements and if you restore the original value the extra elements will be lost forever.
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length
I know that map returns a new array, and that forEach does not return anything (the docs say it returns undefined).
For example, if I had some code like this:
let test;
values.forEach((value, idx) => {
if (someNumber >= value) {
test = value;
}
});
Here I am just checking if someNumber is greater than some value, and if it is then set test = value. Is there another array method I should use here?
Or is it fine to use .forEach
Your example doesn't make sense because it finds the last value that is less than or equal to someNumber, repeatedly assigning to the test variable if more than one is found. Thus, your code is not truly expressing your intent well since other developers can be confused about what you're trying to achieve. In fact, other answers here have had differing opinions on your goal due to this ambiguity. You even said:
if the number is great than or equal to the value from the array at whatever index, stop, and set test equal to that value
But your code doesn't stop at the first value! It keeps going through the entire array and the result in test will be the last value, not the first one.
In general, making your loop refer to outside variables is not the best way to express your intent. It makes it harder for the reader to understand what you're doing. It's better if the function you use returns a value so that it's clear the variable is being assigned.
Here's a guide for you:
forEach
Use this when you want to iterate over all the values in order to do something with each of them. Don't use this if you are creating a new output value--but do use it if you need to modify existing items or run a method on each one, where the forEach has no logical output value. array.forEach at MDN says:
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool, use a plain loop instead. If you are testing the array elements for a predicate and need a Boolean return value, you can use every() or some() instead. If available, the new methods find() or findIndex() can be used for early termination upon true predicates as well.
find
Use this when you want to find the first instance of something, and stop. What you said makes it sound like you want this:
let testResult = values.find(value => value <= someNumber);
This is far superior to setting the test value from inside the lambda or a loop. I also think that reversing the inequality and the variables is better because of the way we tend to think about lambdas.
some
These only give you a Boolean as a result, so you have to misuse them slightly to get an output value. It will traverse the array until the condition is true or the traversal is complete, but you have to do something a bit hacky to get any array value out. Instead, use find as above, which is intended to output the found value instead of simply a true/false whether the condition is met by any element in the array.
every
This is similar to some in that it returns a Boolean, but is what you would expect, it is only true if all the items in the array meet the condition. It will traverse the array until the condition is false or the traversal is complete. Again, don't misuse it by throwing away the Boolean result and setting a variable to a value. If you want to do something to every item in an array and return a single value, at that point you would want to use reduce. Also, notice that !arr.every(lambdacondition) is the same as arr.some(!lambdacondition).
reduce
The way your code is actually written—finding the last value that matches the condition—naturally lends itself to reduce:
let testResult = values.reduce(
(recent, value) => {
if (value <= someNumber) {
recent = value;
}
return recent;
},
undefined
);
This does the same job of finding the last value as your example code does.
map
map is for when you want to transform each element of an array into a new array of the same length. If you have any experience with C# it is much like the Linq-to-objects .Select method. For example:
let inputs = [ 1, 2, 3, 4];
let doubleInputs = inputs.map(value => value * 2);
// result: [ 2, 4, 6, 8]
New requirements
Given your new description of finding the adjacent values in a sorted array between which some value can be found, consider this code:
let sortedBoundaries = [ 10, 20, 30, 40, 50 ];
let inputValue = 37;
let interval = sortedBoundaries
.map((value, index) => ({ prev: value, next: sortedBoundaries[index + 1] }))
.find(pair => pair.prev < inputValue && inputValue <= pair.next);
// result: { prev: 20, next: 30 }
You can improve this to work on the ends so that a number > 50 or <= 10 will be found as well (for example, { prev: undefined, next: 10 }).
Final notes
By using this coding style of returning a value instead of modifying an outside variable, you not only communicate your intent better to other developers, you then get the chance to use const instead of let if the variable will not be reassigned afterward.
I encourage you to browse the documentation of the various Array prototype functions at MDN—doing this will help you sort them out. Note that each method I listed is a link to the MDN documentation.
I would suggest you to use Array#some, instead of Array#forEach.
Array#forEach keeps iterating the array even if given condition was fulfilled.
Array#some stops iteration when given condition was fulfilled.
One of the advantages would be connected with performance, another - depends on your purposes - Array#forEach keeps overwriting the result with every passed condition, Array#some assigns the first found value and stops the iteration.
let test,
values = [4,5,6,7],
someNumber = 5;
values.some((value, idx) => {
if (someNumber >= value) {
test = value;
return test;
}
});
console.log(test);
Another option would be to use the Array.some() method.
let test;
const someNumber = 10;
[1, 5, 10, 15].some(function (value) {
if (value > someNumber) {
return test = value
}
})
One advantage to the .some() method over your original solution is optimization, as it will return once the condition has been met.
How about Object?
you can search with for-of
In another question of mine someone posted a really cool solution on how to flatten an nth nested array into one array. Since I did not want to start a long chat, and I still don't really fully understand what this code does, I thought I'd ask.
So my impression is that first in this case our array has length 2, and then it becomes 1 in the while loop. We then check is array[1], is an array. It is so we proceed. Now here is where I'm a bit confused. I believe we call the flatten function again so we can get into the nested arrays, but I'm still kind of hazy on the reasoning. We then take array[1] and slice it, here doesn't slicing just mean getting the whole array[l] anyways? since we go from 0th position to the end since slice() has no parameters.
function flatten(array) {
var l = array.length, temp;
while (l--) {
if (Array.isArray(array[l])) {
flatten(array[l]);
temp = array[l].slice();
temp.unshift(1);
temp.unshift(l);
[].splice.apply(array, temp);
}
}
}
var array = [['1', '2', '3'], ['4', '5', ['6'], ['7', '8']]];
flatten(array);
console.log(array);
https://jsfiddle.net/curw7mdp/
So I'm going to assume you understand the basics of recursion. I'll walk you through line by line.
var l = array.length, temp;
Declares l equal to the length of the array, and declares temp.
while (l--)
This decrements l after the iteration of the loop (as opposed to --l doing it before);
if (Array.isArray(array[l]))
This is checking if the 'l'th element in the array is another array. This is important because it means this element isn't flat.
flatten(array[l]);
This is where it gets fun, the function recursively calls itself so that it can now traverse the sub-array. And if the sub-array contains another array, it can keep going down deeper. I believe this is head recursion.
temp = array[l].slice();
Looks a little weird, but this allows us to extract the array into a new variable called temp.
temp.unshift(1);
temp.unshift(l);
[].splice.apply(array, temp);
This is also a very janky way of writing things, but basically it puts 1 and l as the first to elements in the temp array, and then it calls splice on array, with temp as the parameters. Only the first two elements of temp are passed as parameters (the two we put in just a second ago), so it uses basically removes the sub array, and replaces it with the flattened version.
References: splice(), apply(), unshift()
See comments for detailed explanation.
function flatten(array) {
// put the length of the array in l
// and create a temp variable with nothing in it
var l = array.length, temp;
// while(l--) will iterate through
// each item in the array backwards
// this is just a little faster than doing
// for(var i=0; i<array.length; i++)
while (l--) {
// The current item in the while loop is array[l]
// (that's an L, by the way, not a one)
// so this is saying "if current item is an array...."
if (Array.isArray(array[l])) {
// we call the function again (recursion)
// if this is an array. eventually, one of
// the inner items will be something other
// than an array and the recursion will stop
flatten(array[l]);
// when you call slice() with no parameters
// all it does is create a copy of the entire array
// and then we store it in temp
temp = array[l].slice();
// adds the number 1 to the begining of the array
// after we unshift one more time below,
// this will become the second parameter for the
// splice() call below, which is the number of
// items to delete
temp.unshift(1);
// adds the current index to the begining of the array
// this will be the first argument passed to splice()
// below, the first argument for splice is the
// index to start splicing..
temp.unshift(l);
// apply()'s first argument is context, in this case
// passing "array" as the context means the action will
// be performed on the array variable,
// which is the original arrray..
// apply()'s second argumetn is an array, each item
// of the array is passed in order to the function
// as arguments
// So basically this is saying.. "remove the current
// index (which is an array) and replace it with each
// of the items that were in that array
[].splice.apply(array, temp);
}
}
}