Co robi [] .forEach.call () w JavaScript?

137

Patrzyłem na kilka fragmentów kodu i znalazłem wiele elementów wywołujących funkcję na liście węzłów z forEach zastosowanym do pustej tablicy.

Na przykład mam coś takiego:

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

ale nie rozumiem, jak to działa. Czy ktoś może mi wyjaśnić zachowanie pustej tablicy przed forEach i jak calldziała?

Mimo
źródło

Odpowiedzi:

208

[]jest tablicą.
Ta tablica nie jest w ogóle używana.

Jest umieszczany na stronie, ponieważ użycie tablicy daje dostęp do prototypów tablic, takich jak .forEach.

To jest po prostu szybsze niż pisanie Array.prototype.forEach.call(...);

Następnie forEachjest to funkcja, która przyjmuje funkcję jako dane wejściowe ...

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

... i dla każdego elementu w this(gdzie thisjest podobny do tablicy, w tym, że ma lengthi możesz uzyskać dostęp do jego części jak this[1]), przekaże trzy rzeczy:

  1. element w tablicy
  2. indeks elementu (przeszedłby trzeci element 2)
  3. odniesienie do tablicy

Na koniec .calljest prototypem, który mają funkcje (jest to funkcja wywoływana przez inne funkcje).
.callweźmie swój pierwszy argument i zastąpi thiswewnątrz zwykłej funkcji tym, co przekazałeś call, jako pierwszy argument ( undefinedlub nullbędzie użyty windoww codziennym JS, lub będzie tym, co przekazałeś, jeśli w "trybie ścisłym"). Reszta argumentów zostanie przekazana do oryginalnej funkcji.

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

Dlatego tworzysz szybki sposób wywołania forEachfunkcji i zmieniasz thispustą tablicę na listę wszystkich <a>tagów, a dla każdego <a>w kolejności wywołujesz podaną funkcję.

EDYTOWAĆ

Logiczne wnioski / czyszczenie

Poniżej znajduje się link do artykułu sugerującego, że za każdym razem rezygnujemy z prób programowania funkcjonalnego i trzymamy się ręcznego, wbudowanego zapętlania, ponieważ to rozwiązanie jest hakerskie i brzydkie.

Powiedziałbym, że choć .forEachjest mniej przydatne niż jego odpowiedniki, .map(transformer), .filter(predicate), .reduce(combiner, initialValue), to nadal służy celom kiedy wszystko, co naprawdę chcesz robić to zmieniać świat na zewnątrz (nie array), n-razy, mając jednocześnie dostęp do jednej arr[i]lub i.

Jak więc sobie radzić z rozbieżnością, skoro Motto jest wyraźnie utalentowanym i kompetentnym facetem, i chciałbym sobie wyobrazić, że wiem, co robię / dokąd zmierzam (od czasu do czasu ... ... inne razy jest to uczenie się w pierwszej kolejności)?

Odpowiedź jest w rzeczywistości dość prosta i coś, co wujek Bob i Sir Crockford obaj spojrzeliby na siebie z powodu przeoczenia:

posprzątaj to .

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

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

Jeśli zastanawiasz się, czy sam musisz to zrobić, odpowiedź może brzmieć nie ...
Dokładnie to robi ... ... każda (?) Biblioteka z funkcjami wyższego rzędu.
Jeśli używasz lodash lub podkreślenia, a nawet jQuery, wszyscy będą mieli sposób na pobranie zestawu elementów i wykonanie akcji n-krotnie.
Jeśli nie używasz czegoś takiego, to jak najbardziej napisz własną.

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);
};

Aktualizacja dla ES6 (ES2015) i nie tylko

Metoda pomocnicza slice( )/ array( )/ etc ułatwi życie nie tylko ludziom, którzy chcą używać list tak samo, jak używają tablic (tak jak powinni), ale także osobom, które mają luksus pracy w przeglądarkach ES6 + o stosunkowo bliskiej przyszłości lub „transpilacji” w Babel dzisiaj, masz wbudowane funkcje językowe, które sprawiają, że tego typu rzeczy są niepotrzebne.

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 ];

Super czysty i bardzo przydatny.
Wyszukaj operatory Rest i Spread ; wypróbuj je w witrynie BabelJS; jeśli twój stos technologiczny jest w porządku, użyj go w produkcji z Babel i krokiem budowy.


Nie ma dobrego powodu, aby nie być w stanie korzystać z transformacji z braku tablicy do tablicy ... ... tylko nie rób bałaganu kodzie nic nie robi , ale wklejając że sam brzydki linii, wszędzie.

Norguard
źródło
Teraz jestem trochę zdezorientowany, jeśli tak: console.log (this); Zawsze dostanę obiekt okna I pomyślałem, że .call zmienia wartość tego
Muhammad Saleh
2
@MuhammadSaleh .callnie zmienia wartości thiswewnątrz tego, do czego przekazujesz forEach, .callzmienia wartość this wewnątrz forEach . Jeśli jesteś zdezorientowany, dlaczego thisnie jest przekazywany z forEachdo funkcji, którą chciałeś wywołać, powinieneś sprawdzić zachowanie thisw JavaScript. Jako dodatek, dotyczy tylko tutaj (i map / filtr / zmniejszenie) jest drugi argument forEach, który ustawia thiswewnątrz funkcji arr.forEach(fn, thisInFn)lub [].forEach.call(arrLike, fn, thisInFn);lub po prostu użyć .bind; arr.forEach(fn.bind(thisInFn));
Norguard
1
@thetrystero o to właśnie chodzi. []to właśnie tam w postaci tablicy, dzięki czemu można .callsię .forEachmetody i zmienić thisdo zestawu, który chcesz zrobić pracować. .callwygląda następująco: function (thisArg, a, b, c) { var method = this; method(a, b, c); }poza tym, że istnieje wewnętrzna czarna magia, którą można ustawić thiswewnątrz, methodaby równać się temu, co zdałeś jako thisArg.
Norguard
1
więc w skrócie ... Array.from ()?
zakius
1
@LukeHutchison to dowcip, [].forEach.call(arr, fn)będzie działał przez długość arr, przeszedł call; nie tablica, która jest używana na początku forEach(jak podano w pierwszych dwóch zdaniach i omówiono później). Długość początkowej tablicy jest całkowicie nieistotna. Kiedy używasz drugiego argumentu forEachfunkcji, używasz indeksu; tak samo, jakbyś przekazał funkcję bezpośrednio do forEachzamiast call. Gdybyś zarejestrował pierwszy argument, dostałbyś "a", "b", "c"nie 1, 2, 3, ze względu na to, jak działa callzamiany thisi jak forEachdziała.
Norguard
55

querySelectorAllMetoda zwraca NodeList, który jest podobny do tablicy, ale nie jest to całkiem tablicą. Dlatego nie ma forEachmetody (która dziedziczy przez obiekty tablicy Array.prototype).

Ponieważ a NodeListjest podobny do tablicy, metody tablicowe będą na nim faktycznie działać, więc używając [].forEach.callwywołania Array.prototype.forEachmetody w kontekście the NodeList, tak jakbyś był w stanie to po prostu zrobić yourNodeList.forEach(/*...*/).

Zwróć uwagę, że pusty literał tablicy to tylko skrót do rozszerzonej wersji, którą prawdopodobnie będziesz często widywać:

Array.prototype.forEach.call(/*...*/);
James Allardice
źródło
7
Tak więc, w celu wyjaśnienia, nie ma przewagę w użyciu [].forEach.call(['a','b'], cb)ponad ['a','b'].forEach(cb)zastosowań codziennych ze standardowymi tablicami, tylko podczas próby iteracyjne nad tablicy struktury przypominające, które nie mają forEachwłasnej prototypu? Czy to jest poprawne?
Matt Fletcher
2
@MattFletcher: Tak, zgadza się. Oba będą działać, ale po co komplikować sprawy? Po prostu wywołaj metodę bezpośrednio w samej tablicy.
James Allardice
Fajne dzięki. I nie wiem dlaczego, może tylko dla ulicznego uznania, imponowania staruszkom z ALDI i tym podobnych. Będę się trzymać [].forEach():)
Matt Fletcher
var section = document.querySelectorAll(".section"); section.forEach((item)=>{ console.log(item.offsetTop) }) działa dobrze
daslicht
@daslicht nie ma w starszych wersjach IE! ( to jednak obsługuje forEachtablice, ale nie obiekty NodeList)
Djave
21

Inne odpowiedzi bardzo dobrze wyjaśniły ten kod, więc dodam tylko sugestię.

To jest dobry przykład kodu, który należy przeformułować dla uproszczenia i przejrzystości. Zamiast używać [].forEach.call()lub za Array.prototype.forEach.call()każdym razem, gdy to robisz, wykonaj z tego prostą funkcję:

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

Teraz możesz wywołać tę funkcję zamiast bardziej skomplikowanego i niejasnego kodu:

forEach( document.querySelectorAll('a'), function( el ) {
   // whatever with the current node
});
Michael Geary
źródło
5

Można go lepiej napisać za pomocą

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

});

Co to robi jest document.querySelectorAll('a')Zwraca obiekt podobny do tablicy, ale nie dziedziczą od Arraytypu. Dlatego wywołujemy forEachmetodę z Array.prototypeobiektu z kontekstem jako wartością zwróconą przezdocument.querySelectorAll('a')

Arun P Johny
źródło
4
[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

Jest to w zasadzie to samo, co:

var arr = document.querySelectorAll('a');
arr.forEach(function(el) {
   // whatever with the current node
});
vava
źródło
2

Chcesz zaktualizować to stare pytanie:

Powód używania [].foreach.call()pętli do elementów w nowoczesnych przeglądarkach w większości dobiegł końca. Możemy użyć document.querySelectorAll("a").foreach()bezpośrednio.

Obiekty NodeList to kolekcje węzłów, zwykle zwracane przez właściwości, takie jak Node.childNodes i metody, takie jak document.querySelectorAll ().

Chociaż NodeList nie jest tablicą , można ją iterować za pomocą forEach () . Można go również przekonwertować na prawdziwy Array za pomocą funkcji Array.from ().

Jednak niektóre starsze przeglądarki nie zaimplementowały NodeList.forEach () ani Array.from (). Można to obejść za pomocą metody Array.prototype.forEach () - zobacz przykład tego dokumentu.

František Heča
źródło
1

Pusta tablica ma forEachw swoim prototypie właściwość, która jest obiektem Function. (Pusta tablica jest po prostu łatwym sposobem uzyskania odniesienia do forEachfunkcji, którą Arraymają wszystkie obiekty). Z kolei obiekty funkcji mają callwłaściwość, która jest również funkcją. Kiedy wywołujesz funkcję Function call, uruchamia ona funkcję z podanymi argumentami. Pierwszy argument pojawia się thisw wywołanej funkcji.

Dokumentację callfunkcji można znaleźć tutaj . Dokumentacja dla forEachjest tutaj .

Ted Hopp
źródło
1

Po prostu dodaj jedną linię:

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

I voila!

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

Cieszyć się :-)

Ostrzeżenie: NodeList to klasa globalna. Nie używaj tej rekomendacji, jeśli piszesz do biblioteki publicznej. Jest to jednak bardzo wygodny sposób na zwiększenie poczucia własnej skuteczności podczas pracy na stronie internetowej lub aplikacji node.js.

imos
źródło
1

Tylko szybkie i brudne rozwiązanie, z którego zawsze korzystam. Nie dotykałbym prototypów, po prostu dobra praktyka. Oczywiście istnieje wiele sposobów, aby to poprawić, ale masz pomysł.

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}`);
});
Alfredo Maria Milano
źródło
0

[]zawsze zwraca nową tablicę, jest równoważny z, new Array()ale gwarantuje, że zwróci tablicę, ponieważ Arraymoże zostać nadpisana przez użytkownika, a []nie może. Jest to więc bezpieczny sposób na uzyskanie prototypu programu Array, który zgodnie z opisem calljest używany do wykonania funkcji na liście węzłów typu Arraylike (this).

Wywołuje funkcję z daną wartością i argumentami podanymi indywidualnie. mdn

Xotic750
źródło
While []w rzeczywistości zawsze zwróci tablicę, podczas gdy window.Arraymożna go nadpisać czymkolwiek (we wszystkich starych przeglądarkach), więc również można window.Array.prototype.forEachgo nadpisać czymkolwiek. W obu przypadkach nie ma gwarancji bezpieczeństwa w przeglądarkach, które nie obsługują metod pobierających / ustawiających lub uszczelniania / zamrażania obiektów (zamrażanie powoduje własne problemy z rozszerzalnością).
Norguard
Czuć się swobodnie edytować odpowiedź na dodanie dodatkowych informacji odnośnie możliwości nadpisania prototypelub jego metod: forEach.
Xotic750
0

Norguard wyjaśnił CO [].forEach.call() robi, a James Allardice DLACZEGO to robimy: ponieważ querySelectorAll zwraca wartość NodeList, która nie ma metody forEach ...

Chyba że masz nowoczesną przeglądarkę, taką jak Chrome 51+, Firefox 50+, Opera 38, Safari 10.

Jeśli nie, możesz dodać Polyfill :

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);
        }
    };
}
Mikel
źródło
0

Wiele dobrych informacji na tej stronie (patrz odpowiedź + odpowiedź + komentarz ), ale ostatnio miałem to samo pytanie co OP i zajęło trochę kopania, aby uzyskać pełny obraz. Oto krótka wersja:

Celem jest użycie Arraymetod na tablicy, NodeListktóra sama nie ma tych metod.

Starszy wzorzec kooptował metody Array za pośrednictwem Function.call()i używał literału tablicowego ( []), a nie Array.prototypedlatego, że był krótszy do wpisania:

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

Nowszy wzorzec (po ECMAScript 2015) jest używany Array.from():

Array.from(document.querySelectorAll('a')).forEach(a => {})
ellemenno
źródło