Что делает [] .forEach.call () в JavaScript?


135

Я просматривал некоторые фрагменты кода и обнаружил, что несколько элементов вызывают функцию по списку узлов с применением forEach к пустому массиву.

Например, у меня есть что-то вроде:

[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

но я не могу понять, как это работает. Может ли кто-нибудь объяснить мне поведение пустого массива перед forEach и как он callработает?

Ответы:


203

[]это массив.
Этот массив вообще не используется.

Он размещается на странице, потому что использование массива дает вам доступ к прототипам массива, например .forEach.

Это быстрее, чем печатать Array.prototype.forEach.call(...);

Далее forEachидет функция, которая принимает на вход функцию ...

[1,2,3].forEach(function (num) { console.log(num); });

... и для каждого элемента в this(где thisподобен массиву, поскольку у него есть, lengthи вы можете получить доступ к его частям, например this[1]), он передаст три вещи:

  1. элемент в массиве
  2. индекс элемента (третий элемент пройдет 2)
  3. ссылка на массив

Наконец, .callэто прототип, который есть у функций (это функция, которая вызывается для других функций).
.callпримет свой первый аргумент и заменит thisвнутри обычной функции все, что вы передали callв качестве первого аргумента ( undefinedили nullбудет использовать windowв повседневном JS, или будет тем, что вы передали, если в "строгом режиме"). Остальные аргументы будут переданы исходной функции.

[1, 2, 3].forEach.call(["a", "b", "c"], function (item, i, arr) {
    console.log(i + ": " + item);
});
// 0: "a"
// 1: "b"
// 2: "c"

Таким образом, вы создаете быстрый способ вызова forEachфункции и переходите thisот пустого массива к списку всех <a>тегов, и для каждого <a>по порядку вы вызываете предоставленную функцию.

РЕДАКТИРОВАТЬ

Логический вывод / Очистка

Ниже приводится ссылка на статью, в которой предлагается отказаться от попыток функционального программирования и каждый раз придерживаться ручного встроенного цикла, потому что это решение взломано и некрасиво.

Я бы сказал , что в то время как .forEachменее полезны , чем его коллеги, .map(transformer), .filter(predicate), .reduce(combiner, initialValue), он по- прежнему служит целям , когда все , что вам действительно нужно сделать , это изменить внешний мир ( а не массив), N-раз, в то время как имеющие доступ к любой arr[i]или i.

Итак, как нам справиться с неравенством, ведь Девиз явно талантливый и знающий парень, и я хотел бы представить, что я знаю, что делаю / куда иду (время от времени ... ... другое раз это обучение с головы до ног)?

Ответ на самом деле довольно прост, и что-то, что дядя Боб и сэр Крокфорд могли бы потерять лицо из-за недосмотра:

убери это .

function toArray (arrLike) { // or asArray(), or array(), or *whatever*
  return [].slice.call(arrLike);
}

var checked = toArray(checkboxes).filter(isChecked);
checked.forEach(listValues);

Теперь, если вы сомневаетесь, нужно ли вам это делать самому, ответ вполне может быть отрицательным ...
В наши дни именно это делается ... ... каждой (?) Библиотекой с функциями высшего порядка.
Если вы используете lodash, подчеркивание или даже jQuery, у всех них будет способ взять набор элементов и выполнить действие n раз.
Если вы не используете такую ​​вещь, то непременно напишите свою.

lib.array = (arrLike, start, end) => [].slice.call(arrLike, start, end);
lib.extend = function (subject) {
  var others = lib.array(arguments, 1);
  return others.reduce(appendKeys, subject);
};

Обновление для ES6 (ES2015) и последующих версий

Вспомогательный метод slice( )/ array( )/ etc не только упростит жизнь людям, которые хотят использовать списки точно так же, как они используют массивы (как они должны), но и тем людям, которые могут позволить себе роскошь работать в браузерах ES6 + относительно близких future, или «транспиляции» в Babel сегодня, у вас есть встроенные языковые функции, которые делают подобные вещи ненужными.

function countArgs (...allArgs) {
  return allArgs.length;
}

function logArgs (...allArgs) {
  return allArgs.forEach(arg => console.log(arg));
}

function extend (subject, ...others) { /* return ... */ }


var nodeArray = [ ...nodeList1, ...nodeList2 ];

Супер-чисто и очень полезно.
Найдите операторы Rest и Spread ; опробуйте их на сайте BabelJS; если ваш стек технологий в порядке, используйте их в производстве с помощью Babel и этапа сборки.


Нет веской причины не использовать преобразование из не массива в массив ... ... просто не путайте свой код, ничего не делая, кроме вставки той же уродливой строки повсюду.


Теперь я немного сбит с толку, потому что если бы я это сделал: console.log (this); Я всегда буду получать объект окна И я подумал, что .call меняет значение этого
Мухаммад Салех

.call это изменить значение this, поэтому вы используете callс Foreach. Если forEach выглядит так, forEach (fn) { var i = 0; var len = this.length; for (; i < length; i += 1) { fn( this[i], i, this ); }то изменение с thisпомощью слова forEach.call( newArrLike )означает, что он будет использовать этот новый объект как this.
Norguard

2
@MuhammadSaleh .callне изменяет значение thisвнутри того, что вы передаете forEach, .callизменяет значение this внутри forEach . Если вы thisне понимаете, почему не передается forEachфункция, которую вы хотели вызвать , вам следует проверить поведение thisв JavaScript. В качестве дополнения, применимого только здесь (и map / filter / reduce), есть второй аргумент forEach, который устанавливается thisвнутри функции arr.forEach(fn, thisInFn)или [].forEach.call(arrLike, fn, thisInFn);или просто use .bind; arr.forEach(fn.bind(thisInFn));
Norguard

1
@thetrystero, в том-то и дело. []существует просто как массив, так что вы можете .callиспользовать .forEachметод и изменить thisего на набор, над которым хотите работать. .callвыглядит следующим образом : за function (thisArg, a, b, c) { var method = this; method(a, b, c); }исключением того, что есть внутренняя черная магия , чтобы установить thisвнутри methodравные , что вы прошли , как thisArg.
Norguard

1
короче ... Array.from ()?
Закиус

53

querySelectorAllметод возвращает NodeList, который похож на массив, но это не совсем массив. Следовательно, у него нет forEachметода (через который наследуются объекты массива Array.prototype).

Поскольку a NodeListпохож на массив, методы массива фактически будут работать с ним, поэтому, используя, [].forEach.callвы вызываетеArray.prototype.forEach метод в контексте NodeList, как если бы вы могли просто сделать yourNodeList.forEach(/*...*/).

Обратите внимание, что литерал пустого массива - это просто ярлык для расширенной версии, которую вы, вероятно, тоже будете видеть довольно часто:

Array.prototype.forEach.call(/*...*/);

7
Итак, чтобы уточнить, нет никакого преимущества в использовании [].forEach.call(['a','b'], cb)over ['a','b'].forEach(cb)в повседневных приложениях со стандартными массивами, просто при попытке перебора структур, подобных массиву, которые не имеют forEachсобственного прототипа? Это правильно?
Мэтт Флетчер

2
@MattFletcher: Да, это правильно. Оба будут работать, но зачем все усложнять? Просто вызовите метод непосредственно в самом массиве.
Джеймс Аллардис

Хорошо, спасибо. И я не знаю, почему, может быть, просто для уличного доверия, произвести впечатление на старушек из ALDI и тому подобное. Я буду придерживаться [].forEach():)
Мэтт Флетчер

var section = document.querySelectorAll(".section"); section.forEach((item)=>{ console.log(item.offsetTop) }) работает хорошо
daslicht

@daslicht отсутствует в старых версиях IE! (что делать , однако, поддержка forEachмассивов, но не Nodelist объекты)
Djave

21

Другие ответы очень хорошо объяснили этот код, поэтому я просто добавлю предложение.

Это хороший пример кода, который следует отремонтировать для простоты и ясности. Вместо использования [].forEach.call()or Array.prototype.forEach.call()каждый раз, когда вы это делаете, сделайте из него простую функцию:

function forEach( list, callback ) {
    Array.prototype.forEach.call( list, callback );
}

Теперь вы можете вызывать эту функцию вместо более сложного и непонятного кода:

forEach( document.querySelectorAll('a'), function( el ) {
   // whatever with the current node
});

5

Лучше написать, используя

Array.prototype.forEach.call( document.querySelectorAll('a'), function(el) {

});

Это document.querySelectorAll('a')возвращает объект, похожий на массив, но не наследующий от Arrayтипа. Поэтому мы вызываем forEachметод из Array.prototypeобъекта с контекстом в качестве значения, возвращаемогоdocument.querySelectorAll('a')


4
[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

Это в основном то же самое, что:

var arr = document.querySelectorAll('a');
arr.forEach(function(el) {
   // whatever with the current node
});

2

Хотите обновить этот старый вопрос:

Причина использования [].foreach.call()циклического перебора элементов в современных браузерах в основном исчезла. Мы можем использовать document.querySelectorAll("a").foreach()напрямую.

Объекты NodeList являются коллекциями узлов, обычно возвращаемых свойствами, такими как Node.childNodes, и методами, такими как document.querySelectorAll ().

Хотя NodeList не является массивом, его можно перебирать с помощью forEach () . Его также можно преобразовать в настоящий массив с помощью Array.from ().

Однако в некоторых старых браузерах не реализованы ни NodeList.forEach (), ни Array.from (). Это можно обойти, используя Array.prototype.forEach () - см. Пример этого документа.


1

Пустой массив имеет свойство forEachв своем прототипе, которое является объектом Function. (Пустой массив - это просто простой способ получить ссылку на forEachфункцию, которая есть у всех Arrayобъектов.) Функциональные объекты, в свою очередь, имеют callсвойство, которое также является функцией. Когда вы вызываете функцию callфункции, она запускает функцию с заданными аргументами. Первый аргумент делается thisв вызываемой функции.

Вы можете найти документацию по этой callфункции здесь . Документация forEachнаходится здесь .


1

Просто добавьте одну строчку:

NodeList.prototype.forEach = HTMLCollection.prototype.forEach = Array.prototype.forEach;

И вуаля!

document.querySelectorAll('a').forEach(function(el) {
  // whatever with the current node
});

Наслаждаться :-)

Предупреждение: NodeList - это глобальный класс. Не используйте эту рекомендацию, если вы пишете для публичной библиотеки. Однако это очень удобный способ повысить самоэффективность при работе на веб-сайте или в приложении node.js.


1

Просто быстрое и грязное решение, которое я всегда использую. Я бы не стал трогать прототипы, это было хорошей практикой. Конечно, есть много способов сделать это лучше, но идею вы поняли.

const forEach = (array, callback) => {
  if (!array || !array.length || !callback) return
  for (var i = 0; i < array.length; i++) {
    callback(array[i], i);
  }
}

forEach(document.querySelectorAll('.a-class'), (item, index) => {
  console.log(`Item: ${item}, index: ${index}`);
});

0

[]всегда возвращает новый массив, он эквивалентен, new Array()но гарантированно возвращает массив, потому что он Arrayможет быть перезаписан пользователем, тогда как []не может. Таким образом, это безопасный способ получить прототип, который Array, как описано, callиспользуется для выполнения функции в массиве узлов (this).

Вызывает функцию с данным значением this и индивидуальными аргументами. MDN


Хотя []на самом деле всегда будет возвращать массив, в то время как window.Arrayего можно перезаписать чем угодно (во всех старых браузерах), поэтому также можно window.Array.prototype.forEachперезаписать чем угодно. В любом случае нет гарантии безопасности в браузерах, которые не поддерживают геттеры / сеттеры или запечатывание / замораживание объектов (замораживание вызывает собственные проблемы с расширяемостью).
Norguard

Вы можете изменить ответ , чтобы добавить дополнительную информацию о возможности перезаписи prototypeили это методы: forEach.
Xotic750

0

Норгуард объяснил, ЧТО [].forEach.call() делает, и Джеймс Аллардис, ПОЧЕМУ мы это делаем: потому что querySelectorAll возвращает объект NodeList, у которого нет метода forEach ...

Если у вас нет современного браузера, такого как Chrome 51+, Firefox 50+, Opera 38, Safari 10.

Если нет, вы можете добавить полифилл :

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = function (callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}

0

На этой странице много полезной информации (см. Ответ + ответ + комментарий ), но недавно у меня был тот же вопрос, что и у OP, и потребовалось немного покопаться, чтобы получить полную картину. Итак, вот краткая версия:

Цель состоит в том, чтобы использовать Arrayметоды в массиве, NodeListкоторый сам не имеет этих методов.

В более старом шаблоне методы Array Function.call()использовались с помощью литерала массива ( []), а не Array.prototypeпотому, что он был короче для ввода:

[].forEach.call(document.querySelectorAll('a'), a => {})

Более новый шаблон (после ECMAScript 2015) должен использовать Array.from():

Array.from(document.querySelectorAll('a')).forEach(a => {})
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.