Почему не удаляет первые два элемента метод forEach
const natural = [1, 2, 3, 4];
natural.forEach(element => natural.pop())
console.log(natural)
Почему не удаляет первые два элемента метод forEach? Выводит [1, 2]
Ответы (2 шт):
Причина в том, что при итерации методом for, for in, for of, forEach не желательно удалять элементы. Они "считают" что длина массива не будет меняться в процессе работы.
Кстати тут можно увидеть что for in и for of наследуются от forEach
Вот пример кода с push который показывает, что длина массива не отслеживается forEach цикл отработает только 6 раз, не бесконечно.
const natural = [1, 2, 3, 4, 5, 6];
natural.forEach(element => {
natural.push(natural.length + 1)
})
console.log(natural.join())
Почему остается половина я объяснял в ответе
Правда в случае с forEach мое объяснение не очень очевидно ведь мы не задаем переменной. Происходит нечто иное. Я представил это в виде таблицы:
| шаг | массив | индекс | значение | действие |
|----------|--------------|---------|----------------|------------------|
| 1 | [1, 2, 3, 4] | 0 | 1 | массив.pop() |
| 2 | [1, 2, 3] | 1 | 2 | массив.pop() |
| 3 | [1, 2] | 2 | ошибка доступа | пропуск |
| 4 | [1] | 3 | ошибка доступа | пропуск |
Пояснения к таблице: каждая строка обозначает итерацию в цикле, и состояние массива и element на третьей итерации доступ получить не получится. Цикл продолжится, но его тело не вызовется, чтобы продемонстрировать это приведу еще один снипет который добавит только 3 элемента в массив testArr т.е. с 4-ой итерации не будет выполнятся тело цикла:
const natural = [1, 2, 3, 4, 5, 6];
const testArr = []
natural.forEach(element => {
natural.pop() // начиная с 4 итерации не выполнится
testArr.push(element + 100) // начиная с 4 итерации не выполнится
})
console.log(testArr)
Информация из исходников v8:
Реализация forEach имеет достаточно много кода, однако v8 использует Torque — язык, который позволяет разработчикам, участвующим в проекте V8, выражать изменения в виртуальной машине, сосредоточив внимание на цели своих изменений в виртуальной машине, а не занимаясь посторонними деталями реализации.
Тут приведен код в упрощенном виде:
function ArrayForEachLoopContinuation(context, callbackfn, thisArg, o, len)
for (let k = 0; k < len; k = k + 1) {
// проверяем есть ли свойство k в объекте o
const kPresent = HasProperty_Inline(o, k);
// если есть то
if (kPresent == True) {
// получаем значение
const kValue = GetProperty(o, k);
// выполняем callback тела цикла ( kValue = элемент , k = индекс, o = массив )
Call(context, callbackfn, thisArg, kValue, k, o);
}
}
return Undefined;
}
Тут можно посмотреть оригинал полностью, снипет ниже содержит приведенную выше функцию:
transitioning builtin ArrayForEachLoopContinuation(implicit context: Context)(
_receiver: JSReceiver, callbackfn: Callable, thisArg: JSAny, _array: JSAny,
o: JSReceiver, initialK: Number, len: Number, _to: JSAny): JSAny {
// variables {array} and {to} are ignored.
// 5. Let k be 0.
// 6. Repeat, while k < len
for (let k: Number = initialK; k < len; k = k + 1) {
// 6a. Let Pk be ! ToString(k).
// k is guaranteed to be a positive integer, hence ToString is
// side-effect free and HasProperty/GetProperty do the conversion inline.
// 6b. Let kPresent be ? HasProperty(O, Pk).
const kPresent: Boolean = HasProperty_Inline(o, k);
// 6c. If kPresent is true, then
if (kPresent == True) {
// 6c. i. Let kValue be ? Get(O, Pk).
const kValue: JSAny = GetProperty(o, k);
// 6c. ii. Perform ? Call(callbackfn, T, <kValue, k, O>).
Call(context, callbackfn, thisArg, kValue, k, o);
}
// 6d. Increase k by 1. (done by the loop).
}
return Undefined;
}
Подробнее о коде в вопросе:
Так как вы удаляете элементы с конца остается половина массива с начала.
const natural = [1, 2, 3, 4, 5, 6];
natural.forEach(element => natural.pop()) // [1, 2, 3]
Если бы делали это же но с начала, осталась бы половина массива с конца
const natural = [1, 2, 3, 4, 5, 6];
natural.forEach(element => natural.shift()) // [4, 5, 6]
Для этих целей можно использовать while ,но это избыточно если нет других действий
const natural = [1, 2, 3, 4, 5, 6];
while (natural.length) natural.pop() // []
Это однако не значит что for совсем нельзя использовать для этих целей, просто нужно делать это немного по-другому (учитывая то как работает пример с while выше) задав условие выхода иначе. Этот вариант тоже избыточен если нет других действий.
const natural = [1, 2, 3, 4, 5, 6];
for (var i = 0; natural.length; i++) {
natural.pop()
}
console.log(natural)
Cамым "правильным" на мой взгляд, будет использование методов массива для работы с массивом, а именно метод splice, который модифицирует наличие элементов массива по месту, не создавая нового массива:
const natural = [1, 2, 3, 4, 5, 6];
natural.splice(0, natural.length); // []
Потому что foeEach не вызывается на дырках:
[10, 20, , , 30].forEach((x, i) => console.log(x, i))
Берём код из вопроса:
const natural = [1, 2, 3, 4];
natural.forEach(element => natural.pop())
console.log(natural)
и переписываем:
const natural = [1, 2, 3, 4];
for (let i=0, n=natural.length; i<n; ++i) {
if (i in natural) {
natural.pop()
}
}
console.log(natural)
Понятно, что как только i превысит половину размера массива, все проверки in станут ложными, а значит начало не удалится.