Podstawy#
Kolekcje#
Praca z drzewem węzłów najczęściej polega po prostu na manipulowaniu kolekcjami w pętlach. Warto poświęcić więcej miejsca na dokładniejszy opis tego mechanizmu, niż czyni to sama specyfikacja DOM4.
Interfejsy i polecenia#
Kolekcje są obiektami implementującymi określone interfejsy. Specyfikacja DOM4 dzieli kolekcje na dwie podstawowe grupy:
- Kolekcje nowoczesne: Elements (tylko dla elementów).
- Kolekcje w starym stylu: NodeList (dla wszystkich węzłów) oraz HTMLCollection (tylko dla elementów).
Wprowadzenie kolekcji Elements jest konsekwencją ciągłych zmian ostatniej specyfikacji DOM i pokrewnych jej dokumentów. Na dzień dzisiejszy żadna przeglądarka internetowa nie obsługuje tego wariantu i cała koncepcja została odłożona w czasie (DOM - Fix).
Najwięcej kontrowersji budzi interfejs HTMLCollection, który pozostawiono jedynie ze względów historycznych. Osoby odpowiedzialne za rozwój DOM najchętniej usunęłyby go całkowicie ze specyfikacji, ale w najbliższej przyszłości wydaje się to mało prawdopodobne (kwestia kompatybilności wstecznej jest ważniejsza). Interfejs ten będzie dziedziczony przez dwa kolejne interfejsy definiowane w specyfikacji HTML5:
- HTMLFormControlsCollection
- HTMLOptionsCollection
- HTMLAllCollection - ten od jakiegoś czasu stanowi osobny rodzaj i nie dziedziczy już z HTMLCollection, ale jest do niego bardzo podobny (HTML - Bug 210, HTML - Bug 775).
- RadioNodeList - ten jest specyficzny i dziedziczy z NodeList.
To jakiego rodzaju kolekcja zostanie zwrócona będzie zależało od wywoływanej metody lub właściwości, ponieważ każda z nich ma z góry przypisany interfejs. Różnica między interfejsami najczęściej będzie sprowadzała się do sposobu wydobycia danego węzła/elementu z kolekcji - w przypadku HTMLCollection (i pochodnych) może być więcej możliwości i reguł.
Poniższa tabela zawiera wykaz poleceń, które zwracają kolekcje określonego typu (znakiem *
oznaczono kolekcje statyczne): #
Powyższy podział należy traktować z lekkim przymrużeniem oka. W rzeczywistości przeglądarki internetowe działają odmiennie i zwracają różne typy kolekcji dla poszczególnych poleceń. Prawidłowa implementacja interfejsów zajmie zapewne sporo czasu. W żaden sposób nie wpływa to na codzienną pracę przy skryptach, a przynajmniej nie utrudnia jej znacząco.
Prosty przykład:
<script>
function testCollection(){
var obj = {
"docHTML": document,
"docHTML2": document.implementation.createHTMLDocument(''),
"docXML": document.implementation.createDocument(null, null, null),
"dtd": document.implementation.createDocumentType('xml', '', ''),
"frag": document.createDocumentFragment(),
"txt": document.createTextNode('data'),
"comment": document.createComment('data'),
"pi": document.implementation.createDocument(null, null, null)
.createProcessingInstruction('target', 'data'),
"el_html": document.documentElement,
"el_button": document.createElement("button"),
"el_input": document.createElement("input"),
"el_keygen": document.createElement("keygen"),
"el_meter": document.createElement("meter"),
"el_output": document.createElement("output"),
"el_progress": document.createElement("progress"),
"el_select": document.createElement("select"),
"el_textarea": document.createElement("textarea"),
"el_map": document.createElement("map"),
"el_datalist": document.createElement("datalist"),
"el_table": document.createElement("table"),
"el_tbody": document.createElement("tbody"),
"el_tr": document.createElement("tr"),
"el_form": document.createElement("form"),
"el_fieldset": document.createElement("fieldset")
};
var tests = {
"NodeList": {
"childNodes": ["docHTML", "docHTML2", "docXML", "dtd", "frag", "txt", "comment", "pi"],
"querySelectorAll('p')": ["docHTML", "docHTML2", "docXML", "frag", "el_html"],
"labels": [
"el_button", "el_input", "el_keygen", "el_meter",
"el_output", "el_progress", "el_select", "el_textarea"
],
"getElementsByName('p')": ["docHTML", "docHTML2", "docXML"]
},
"HTMLCollection": {
"children": ["docHTML", "docHTML2", "docXML", "frag", "el_html"],
"getElementsByTagName('p')": ["docHTML", "docHTML2", "docXML", "el_html"],
"getElementsByTagNameNS(null, 'p')": ["docHTML", "docHTML2", "docXML", "el_html"],
"getElementsByClassName('p')": ["docHTML", "docHTML2", "docXML", "el_html"],
"anchors": ["docHTML", "docHTML2", "docXML"],
"applets": ["docHTML", "docHTML2", "docXML"],
"embeds": ["docHTML", "docHTML2", "docXML"],
"forms": ["docHTML", "docHTML2", "docXML"],
"images": ["docHTML", "docHTML2", "docXML"],
"links": ["docHTML", "docHTML2", "docXML"],
"plugins": ["docHTML", "docHTML2", "docXML"],
"scripts": ["docHTML", "docHTML2", "docXML"],
"areas": ["el_map"],
"options": ["el_datalist"],
"selectedOptions": ["el_select"],
"rows": ["el_table", "el_tbody"],
"tBodies": ["el_table"],
"cells": ["el_tr"],
"elements": ["el_fieldset"]
},
"HTMLAllCollection": {
"all": ["docHTML", "docHTML2", "docXML"]
},
"HTMLFormControlsCollection": {
"elements": ["el_form"]
},
"HTMLOptionsCollection": {
"options": ["el_select"]
}
};
for (var coll in tests){
var test = tests[coll];
for (var prop in test){
var nodes = test[prop];
var nodesLen = nodes.length;
for (var i = 0; i <nodesLen; i++){
var node = nodes[i];
var res = null;
var status = "<span style='color: red'>ŹLE</span>";
document.write(node + "." + prop + ": ");
try{
if (prop.indexOf("(") != -1){
var propPart = prop.split("(");
if (propPart[1].indexOf(",") != -1){
res = obj[node][propPart[0]](null, "p");
}
else{
res = obj[node][propPart[0]]("p");
}
}
else{
res = obj[node][prop];
}
document.write(res + "<br>");
}
catch(e){
document.write(e + "<br>");
}
if (res + "" == "[object " + coll + "]"){
status = "<span style='color: green'>DOBRZE</span>";
}
document.write(status + "<br><br>");
}
}
}
}
testCollection();
</script>
Kolekcje aktualne#
Wszystkie kolekcje, prócz tych zwracanych za pomocą metod ParentNode.querySelectorAll()
, ParentNode.queryAll()
i Elements.queryAll()
, są z reguły aktualne. Niektórzy programiści nazywają je po prostu żywymi kolekcjami.
W praktyce oznacza to, że za każdym razem, kiedy odwołamy się do zmiennej będącej referencją (wskaźnikiem) do konkretnej kolekcji, zawsze otrzymamy aktualną kolekcję, tak jakbyśmy od nowa wywołali polecenie tworzące tę kolekcję. Czyli każda modyfikacja węzła, który należy do kolekcji, a która przeprowadzona została za pomocą poleceń DOM powoduje, że ponowne odwołanie się do kolekcji będzie zawierało przeprowadzone zmiany. Co istotne, jeśli przykładowo mamy kolekcję pobierającą wszystkie elementy <p>
w danym dokumencie, następnie dodamy kolejne akapity do dokumentu, to przy odczycie kolekcji nowe akapity będą się w niej znajdowały, nawet jeśli kolekcja utworzona została przed dodaniem elementów.
Oto cała specyfika aktualnego charakteru kolekcji. Czasami takie zachowanie może być problematyczne, bo przez nieumiejętne manewrowanie drzewem węzłów można łatwo doprowadzić do nieskończonych pętli po kolekcjach:
<!DOCTYPE html>
<html>
<p>Akapit</p>
<script>
var allP = document.getElementsByTagName("p"); // Kolekcja (żywe zapytanie)
for (var i = 0; i < allP.length; i++){
var newP = document.createElement("p");
newP.textContent = "Akapit " + i;
document.body.appendChild(newP); // Nowy element automatycznie zwiększa krańcowy warunek
if (i == 15){ // Zabezpieczenie przed nieskończoną pętlą
break;
}
}
</script>
</html>
Nasz pierwszy przykład można scharakteryzować następująco:
- Pobieramy kolekcję ze wszystkie elementami
P
i umieszczamy ją w zmiennejallP
. - Iterujemy po kolekcji za pomocą pętli
for
. - W każdym kroku pętli tworzymy nowy akapit i wstawiamy go do dokumentu, przez co automatycznie zwiększa się warunek końcowy pętli
allP.length
. Właśnie to polecenie odpowiada za ciągłe odwoływanie się do aktualnej kolekcji. - Jest to przykład pętli nieskończonej, ale żeby można było przetestować wszystko samodzielnie wstawiłem instrukcję przerywającą, która wyjdzie z pętli kiedy licznik będzie równy
15
.
Trzeba uważać przed tego typu sytuacjami. Jeśli faktycznie żywe zapytanie nie jest potrzebne, można po prostu przechwycić długość kolekcji do osobnej zmiennej:
<!DOCTYPE html>
<html>
<p>Akapit</p>
<script>
var allP = document.getElementsByTagName("p"); // Kolekcja (żywe zapytanie)
var allPLen = allP.length; // Konkretna liczba elementów w kolekcji (licznik pętli)
for (var i = 0; i < allPLen; i++){
var newP = document.createElement("p");
newP.textContent = "Akapit " + i;
document.body.appendChild(newP); // Nowy element automatycznie zwiększa długość kolekcji, ale nie licznik pętli
}
</script>
</html>
Tym razem pętla wykona się tylko raz i żadne zabezpieczenie nie jest potrzebne. Samo przechowywanie długości kolekcji (czy tablicy) w zmiennej przyspiesza wykonywanie pętli, ponieważ liczba elementów odczytywana jest tylko raz. Jest to bardzo często prezentowany przykład przy omawianiu technik optymalizacyjnych ECMAScript.
Zwracanie tego samego obiektu kolekcji#
Kolekcje aktualne są w zasadzie żywym zapytaniem do jakiegoś drzewa węzłów, dlatego w ich przypadku wszystkie polecenia, które zwracają obiekty kolekcji, mogą zwracać zawsze ten sam obiekt. Z perspektywy Web IDL jest to sygnalizowane specjalnym atrybutem rozszerzającym [SameObject]
. Dotyczy to każdej właściwości oznaczonej takim atrybutem oraz tych metod, którym ponownie przekazujemy ten sam argument (ale informacja o takiej możliwości pojawia się już tylko w prozie).
Pewnym osobliwym przypadkiem będą takie metody, dla których proces filtracji elementów w kolekcjach jest uzależniony od rodzaju dokumentu (np. HTML , czy też różnych odmian XML-a). Trzeba pamiętać o tym, że węzeł stanowiący korzeń w kolekcji może zmienić swojego właściciela, i jeśli typ we właścicielu dla korzenia ulegnie zmianie (tj. z wartości "html"
na "xml"
lub odwrotnie), to w takim przypadku obiekt kolekcji należałoby utworzyć na nowo. W praktyce będzie to istotne jedynie dla metody Element.getElementsByTagName()
, która korzysta z algorytmu pobierającego listę elementów z nazwą kwalifikowaną qualifiedName
.
Powyższe wytyczne dla metod są jedynie zaleceniami i nie muszą być spełniane przez każdą implementację. Przykładowo przeglądarki IE11 i Opera (Presto) zawsze zwracają nowy obiekt kolekcji, kiedy jej korzeń jest przenoszony w inne miejsce drzewa węzłów (nawet w obrębie tego samego właściciela).
Prosty przykład:
<script>
var collection1 = document.getElementsByTagName("a");
var collection2 = document.getElementsByTagName("a");
var collection3 = document.getElementsByTagName("A");
document.write(collection1 == collection2); // true
document.write("<br>");
document.write(collection1 == collection3); // false>
var collection4 = document.childNodes;
var collection5 = document.childNodes;
document.write("<br><br>");
document.write(collection4 == collection5); // true
document.write("<br><br>");
var newDoc = document.implementation.createDocument(null, "html", null); // nowy dokument XML
var newEl = document.createElement("div");
var collection6 = newEl.getElementsByTagName("a");
var collection7 = newEl.childNodes;
newDoc.adoptNode(newEl); // zmieniamy właściciela z typu "html" na "xml"
var collection8 = newEl.getElementsByTagName("a");
var collection9 = newEl.childNodes;
document.write(collection6 == collection8); // false
document.write("<br>");
document.write(collection7 == collection9); // true
document.write("<br><br>");
newDoc.documentElement.appendChild(newEl);
var collection10 = newEl.getElementsByTagName("a");
var collection11 = newEl.childNodes;
document.write(collection8 == collection10); // true (Firefox, Chrome), false (IE, Opera na Presto)
document.write("<br>");
document.write(collection9 == collection11); // true
</script>
Warto podkreślić, że w przypadku metody Element.getElementsByClassName()
, która korzysta z algorytmu pobierającego listę elementów z nazwami klas classNames
, i dla tego samego argumentu zawsze zwracany będzie ten sam obiekt, mimo że sposób filtracji jest zależny od trybu we właścicielu posiadanym przez element, na którym wywołano metodę. Przenoszenie elementu między dokumentami z różnym trybem nie utworzy nowego obiektu kolekcji dla tego samego argumentu przekazanego przy wywołaniu metody.
Prosty przykład:
<script>
var newDoc = document.implementation.createDocument("http://www.w3.org/1999/xhtml", "html", null);
var newEl = document.createElement("div");
document.write(newEl.ownerDocument); // [object HTMLDocument]
document.write("<br>");
document.write(document.compatMode); // BackCompat
document.write("<br>");
document.write(newDoc.compatMode); // CSS1Compat
document.write("<br><br>");
var collection1 = newEl.getElementsByClassName("a");
var collection2 = newEl.getElementsByClassName("a");
var collection3 = newEl.getElementsByClassName("A");
document.write(collection1 == collection2); // true
document.write("<br>");
document.write(collection1 == collection3); // false
document.write("<br><br>");
newDoc.documentElement.appendChild(newEl);
var collection4 = newEl.getElementsByClassName("a");
document.write(newEl.ownerDocument); // [object XMLDocument]
document.write("<br>");
document.write(collection1 == collection4); // true
document.write("<br>");
document.write(collection2 == collection4); // true
document.write("<br><br>");
document.documentElement.appendChild(newEl);
var collection5 = newEl.getElementsByClassName("a");
document.write(newEl.ownerDocument); // [object HTMLDocument]
document.write("<br>");
document.write(collection1 == collection5); // true
document.write("<br>");
document.write(collection2 == collection5); // true
</script>
Kolekcje statyczne#
Kolekcje mogą być statyczne, ale stanowią niewielki procent ze wszystkich poleceni zwracających kolekcje. Można je uzyskać jedynie za pomocą metod zaliczanych do API selektorów, takich jak ParentNode.querySelectorAll()
, ParentNode.queryAll()
i Elements.queryAll()
.
Statyczność polega na tym, że przy tworzeniu kolekcji kopiowane są wszystkie dane potrzebne do jej obsługi, czyli tworzona jest tzw. migawka # (snapshot) odzwierciedlająca stan w danej chwili. Z praktycznego punktu widzenia chodzi o to, że taka migawka zapamiętuje referencje do wszystkich pasujących węzłów w chwili jej tworzenia, późniejsze manipulowanie drzewem węzłów nie zmienia jej stanu. Zatem ponowne odwołanie się do kolekcji nie ma już takiego efektu, jakbyśmy od nowa wywołali polecenie tworzące tę kolekcję (co ma miejsce w przypadku kolekcji aktualnych). Oto kilka charakterystycznych cech dla tego rodzaju kolekcji:
- Dodanie nowego węzła do drzewa DOM nie wpływa na stan utworzonej wcześniej kolekcji.
- Modyfikacja poprzez drzewo DOM węzła należącego do kolekcji jest odzwierciedlana w kolekcji, która tak naprawdę przechowuje do nich jedynie referencje.
- Przenoszenie czy też usunięcie z drzewa DOM węzła należącego do kolekcji nie modyfikuje jej stanu, usunięty węzeł dalej jest dostępny w kolekcji, nawet w sytuacji, kiedy węzeł zmienia swojego właściciela.
- Wywołanie metody zwracającej kolekcję statyczną (niezależnie od przekazanego argumentu) zawsze zwraca nowy obiekt kolekcji.
Wcześniejszy przykład z nieskończoną pętlą nie będzie występował w przypadku kolekcji statycznej:
<!DOCTYPE html>
<html>
<p>Akapit</p>
<script>
var allP = document.querySelectorAll("p"); // Kolekcja (statyczne zapytanie)
for (var i = 0; i < allP.length; i++){
var newP = document.createElement("p");
newP.textContent = "Akapit " + i;
document.body.appendChild(newP); // Nowy element nie zwiększa krańcowego warunku
if (i == 15){ // Zabezpieczenie przed nieskończoną pętlą nie jest potrzebne
break;
}
}
var allP2 = document.querySelectorAll("p"); // Kolekcja (statyczne zapytanie)
document.write(allP == allP2); // false - dwa różne obiekty kolekcji
var newDoc = document.implementation.createDocument(null, "html", null); // Nowy dokument XML
newDoc.documentElement.appendChild(document.getElementsByTagName("p")[0]);
document.write("<br>");
document.write(allP[0]); // [object HTMLParagraphElement] - akapit dalej dostępny (mimo przeniesienia w nowe miejsce)
document.write("<br>");
document.write(allP2[0]); // [object HTMLParagraphElement] - akapit dalej dostępny (mimo przeniesienia w nowe miejsce)
document.write("<br>");
document.write(allP[0] == allP2[0]); // true
</script>
</html>
Uporządkowanie węzłów w kolekcjach#
Kolekcje zawierają węzły, które pobiera się ze skojarzonego korzenia i przy użyciu skojarzonego filtra. W większości przypadków kolejność węzłów jest zgodna z porządkiem drzewa bez żadnych dodatkowych obostrzeń. Istnieją jednak takie polecenia, np. HTMLCollection.rows
, gdzie kolejność pobieranych wierszy z tabeli zależy także od innych zawartych w niej elementów. Zgodnie z wymogami specyfikacji HTML5 pierwszeństwo mają wiersze z thead
, następnie wiersze umieszczone bezpośrednio w table
lub w tbody
, i na sam koniec wiersze z tfoot
, co odwzorowuje kolejność wierszy przy wyświetlaniu tabeli.
Prosty przykład:
<!DOCTYPE html>
<html>
<body>
<script>
function createTR(txt){
var tr = doc.createElement("tr");
var data = doc.createTextNode(txt);
tr.appendChild(data);
return tr;
}
var doc = document;
var t = doc.createElement("table");
var tHead = doc.createElement("thead");
var tBody = doc.createElement("tbody");
var tFoot = doc.createElement("tfoot");
tHead.appendChild(createTR("Wiersz1 w elemencie THEAD."));
tHead.appendChild(createTR("Wiersz2 w elemencie THEAD."));
tBody.appendChild(createTR("Wiersz1 w elemencie TBODY."));
tBody.appendChild(createTR("Wiersz2 w elemencie TBODY."));
tFoot.appendChild(createTR("Wiersz1 w elemencie TFOOT."));
tFoot.appendChild(createTR("Wiersz2 w elemencie TFOOT."));
t.border = 1;
t.appendChild(createTR("Wiersz1 bezpośrednio w tabeli (na początku)."));
t.appendChild(createTR("Wiersz2 bezpośrednio w tabeli (na początku)."));
t.appendChild(tHead);
t.appendChild(tFoot);
t.appendChild(tBody);
t.appendChild(createTR("Wiersz3 bezpośrednio w tabeli (na końcu)."));
t.appendChild(createTR("Wiersz4 bezpośrednio w tabeli (na końcu)."));
var allTR = t.rows; // Kolekcja (żywe zapytanie)
var allTRLen = allTR.length;
document.write("Dane z kolekcji:<br><br>");
for (var i = 0; i < allTRLen; i++){
document.write(allTR[i].textContent + "<br>");
}
document.write("<br>Wyrenderowana tabela (widoczna kolejność wierszy może być inna niż w DOM");
document.write(", ale wciąż powinna być zgodna z kolejnością wierszy w pobranej kolekcji):<br><br>");
doc.body.appendChild(t);
</script>
</body>
</html>
Tablice a kolekcje#
Warto wspomnieć o kilku kluczowych różnicach między tablicami a kolekcjami przy korzystaniu z JavaScriptu. Mimo że obydwa obiekty wyglądają podobnie, to w rzeczywistości stanowią osobne zagadnienia.
Poniższe uwagi dotyczą wszystkich pozostałych liniowych wykazów w DOM, takich jak zestawy czy mapy nazwanych atrybutów, które również przypominają klasyczne tablice JS.
Tablica to obiekt (jak większość danych w JS), co może wydawać się nieco dziwne dla dużej grupy programistów wywodzących się z innych języków. Tak, to prawda, w JS nie ma "prawdziwych" tablic, a to co sporo osób uważa za tablice, tak naprawdę jest obiektem, a dokładniej obiektem tablicowym. Podział obiektów JS na różne rodzaje wynika z tego, że na każdym z nich udostępniono nieco inne metody i właściwości (ogólny obiekt, obiekt tablicowy, obiekt funkcyjny, obiekt boolowski itd.). Polecenia są udostępniane z bazowych prototypów przy wykorzystaniu mechanizm dziedziczenia.
Prosty przykład:
<script>
// Tworzenie tablicy z literału
var tablica = ["pies", "kot", "ptak"];
// Tworzenie tablicy z konstruktora
var tablica = new Array();
tablica[0] = "pies";
tablica[1] = "kot";
tablica[2] = "ptak";
document.write(tablica.length); // 3
document.write("<br>");
document.write(tablica[0]); // pies
document.write("<br>");
document.write(tablica[1]); // kot
document.write("<br>");
document.write(tablica[2]); // ptak
document.write("<br><br>");
document.write(typeof tablica); // object
document.write("<br>");
document.write(tablica.constructor); // function Array() { [native code] }
</script>
Można powiedzieć, że tablica to płaski (liniowy) zbiór danych udostępnianych pod kolejnymi indeksami liczbowymi. Elementami mogą być typy proste jak i inne obiekty, co umożliwia tworzenie macierzy:
var tablica = [[0, 1], "banan", {liczba: 1}];
JS oferuje wiele użytecznych metod przydatnych przy przetwarzaniu tablic, których liczba została dodatkowo poszerzona przez kolejne wydania EcmaScript. Nie są to klasyczne tablice ponieważ nie mają wskaźników, są one tak naprawdę automatycznie konwertowane na obiekty przez interpreter JS. W związku z tym spora część programistów naśmiewa się z JavaScriptu, że wcale nie ma tablic, oraz że jego rozwiązanie jest wolniejsze w porównaniu z innymi językami. Mi tak naprawdę nigdy to nie przeszkadzało w codziennej pracy, wróćmy zatem do tematu.
Kolekcja, jak wspomniałem na początku, też jest obiektem, ale wywodzi się z DOM, nie należy do rdzennego ECMAScript, chociaż bardzo często z nim współpracuje. Kolekcje dziedziczą po innych prototypach, dlatego mają swoje własne polecenia. Chociaż kolekcja wygląda jak tablica - ponieważ ma identyczną właściwość length
oraz można wydobywać jej elementy za pomocą nawiasów []
- tak naprawdę tablicą nie jest. Na obiekcie kolekcji nie można jawnie wykonywać żadnej metody tablicowej, np. Array.push()
, Array.concat()
czy Array.splice()
. W razie czego metody takie można "wypożyczyć" stosując polecenia call()
lub apply()
na konkretnej funkcji z tablicy, co wykorzystywane jest bardzo często przez programistów (patrz kolejny przykład).
Odstępstwem od reguły będzie nowy interfejs Elements, którego wprowadzenie od razu zakładało bezpośrednie rozszerzenie tablic JS (definicja Web IDL: class Elements extends Array
). Rozwiązanie jest na etapie wczesnego planowania i minie sporo czasu, nim ujrzymy jego rzeczywiste implementacje.
Wydajne przetwarzanie kolekcji#
Umiejętne stosowanie JS i DOM pod kątem wydajności to bardzo rozległe zagadnienie, ale warto napomknąć parę słów przy omawianiu kolekcji. Gorąco polecam zakup czegoś wartościowego z prezentowanej literatury.
Od dawien dawna, przy przetwarzaniu wielu danych w pętli, zawsze najwydajniejsze były literały i zmienne, a potem obiekty tablicowe. Nic dziwnego, polecenia te są wbudowane bezpośrednio w rdzeń JS. W przypadku niektórych przeglądarek (np. IE6) tablice były nawet kilka/kilkanaście razy szybsze od klasycznych obiektów. Obecnie, kiedy upowszechniły się kompilatory JIT, różnica wydajnościowa między obróbką obiektu tablicopodobnego a klasycznego obiektu się zatarła. Oczywiście bierzemy pod uwagę sytuacje kiedy długość tablicy odczytywana jest tylko raz.
Manipulowanie kolekcjami, jak i całym DOM, zawsze będzie bardziej czasochłonne ze względu na naturę DOM. Model dokumentu implementowany jest w przeglądarkach osobno (niezależnie od rdzenia JS), a to dlatego, że w razie potrzeby inne języki skryptowe mogą z niego korzystać. Efektem ubocznym takiego podejścia będzie dodatkowy narzut dla samego ECMAScript. Są pewne techniki, które pozwalają minimalizować niepożądane straty czasowe, ale nigdy nie będą to takie efekty, jakbyśmy operowali czystym JavaScriptem.
Kiedy iteruje się po wielu elementach kolekcji, gdzie priorytetem jest czas, to najlepiej skopiować je do tablicy. Oto prosty przykład z wypożyczeniem tablicowej metody Array.slice()
: #
<!DOCTYPE html>
<html>
<p>Akapit1</p>
<p>Akapit2</p>
<p>Akapit3</p>
<script>
var allP = document.getElementsByTagName("p"); // Kolekcja (żywe zapytanie)
var arrayP = Array.prototype.slice.call(allP); // Kopia kolekcji do tablicy (statyczna)
document.write(allP.length); // 3
document.write("<br>");
document.write(arrayP.length); // 3
document.write("<br>");
document.write(allP[0] == arrayP[0]); // true
</script>
</html>
Takie rozwiązanie generuje dodatkowe koszta wynikający z samego procesu kopiowania, więc trzeba dobrze przeanalizować sytuację i zdecydować, czy użycie kopiowania do tablicy będzie korzystne w danym, konkretnym przypadku. Kolejnym plusem kopiowania jest fakt, że tablica z węzłami będzie zachowywała się podobnie jak kolekcja statyczna, co przez wielu programistów jest czymś naturalnym i pożądanym. Sporo bibliotek dla własnych metod DOM domyślnie zwraca tablice zamiast kolekcji.
Kolejna sprawa to częstość dostępu do DOM. Im będzie go mniej, tym aplikacja będzie działać szybciej. Na ogół najlepiej używać zmiennej lokalnej, jeśli dostęp do tej samej właściwości lub metody DOM ma miejsce więcej niż raz.
Przykład:
<!DOCTYPE html>
<html>
<p>Akapit1</p>
<p>Akapit2</p>
<p>Akapit3</p>
<script>
var allP = document.getElementsByTagName("p"); // Kolekcja (żywe zapytanie)
var allPLen = allP.length; // Statyczne zapytanie do długości kolekcji
// Wolne rozwiązanie
for (var i = 0; i < allPLen; i++){
document.write(document.getElementsByTagName("p")[i].nodeName + "<br>");
document.write(document.getElementsByTagName("p")[i].nodeType + "<br>");
document.write(document.getElementsByTagName("p")[i].tagName + "<br>");
document.write(document.getElementsByTagName("p")[i].textContent + "<br><br>");
}
document.write("<br><br><br><br>");
// Zoptymalizowane rozwiązanie
for (var i = 0; i < allPLen; i++){
var el = allP[i]; // Zmienna lokalna poprawiająca wydajność
document.write(el.nodeName + "<br>");
document.write(el.nodeType + "<br>");
document.write(el.tagName + "<br>");
document.write(el.textContent + "<br><br>");
}
</script>
</html>
Ciekawie wygląda sprawa pozyskiwania kolekcji #. Wywoływanie konkretnych metod oraz właściwości (które wypunktowałem wcześniej) może mieć różną wydajność, szczególnie widoczną przy większej liczbie elementów. Najciekawiej prezentuje się różnica wydajnościowa między kolekcją żywą a kolekcją statyczną. Okazuje się, że to ta pierwsza jest szybsza. Przeglądarki internetowe muszą wykonać zdecydowanie więcej pracy przy tworzeniu kolekcji statycznych, dlatego metoda Document.getElementsByTagName()
będzie szybsza od ParentNode.querySelectorAll()
. Wyjaśnione zostało to w artykule Nicholasa C. Zakasa oraz uzupełnione w kolejnym wpisie.
Optymalizacja kodu JS jest bardzo ważna. Nie wszystko wykonane zostanie przez najnowsze interpretery, w wielu miejscach będzie trzeba "dopieszczać" kod ręcznie. Uwaga ta będzie zawsze właściwa przy manewrowaniu drzewem DOM. Na przestrzeni ostatnich lat postęp w tej dziedzinie jest znaczny, dlatego sporo rozwiązań opisywanych w różnych źródłach mogło stracić już na aktualności. Najlepiej samodzielnie przeanalizować wąskie gardła w tworzonym kodzie (poprzez profilowanie) i poszukać aktualnych technik optymalizacyjnych.