Częste problemy związane z Array length

Tablice w JavaScript, podobnie jak w innych językach programowania, posiadają właściwość length, która często utożsamiana jest z ilością elementów tablicy. W artykule omówimy częste problemy związane z Array length specyficzne dla języka JavaScript.

W niektórych językach programowania istnieją oddzielne funkcje zwracające rzeczywistą ilość elementów tablicy, która przekazana jest jako argument (np. funkcja count($array) z języka PHP).

W JavaScript tablice nie są oddzielnym typem zmiennych lecz są obiektami i posiadają własne metody i właściwości. Tablice w JavaScript zaleca się deklarować przy użyciu prostej składni literalnej z użyciem nawiasów kwadratowych:

var arr = [1,2,3];
typeof arr; //"object"
Object.prototype.toString.apply(arr); //"[object Array]"

Podczas operacji na tablicach często musimy znać ilość jej elementów. Jest to wykorzystywane np. w czasie iterowania po tablicach przy użyciu pętli for. Z pomocą przychodzi nam wtedy właściwość length, którą posiadają wszystkie tablice.

var arr = ['a', 'b', 'c'];
arr.length; //3

W powyższym przykładzie zadeklarowaliśmy prostą tablicę zawierającą trzy elementy typu string. Należy pamiętać, że indeksy w tablicach rozpoczynają się od zera, zatem dostęp do drugiego elementu odbywa się poprzez zapis:

var arr = ['a', 'b', 'c'];
arr[1]; //'b'

Problem leży jednak w tym, że tak na prawdę właściwość length nie oznacza ilości elementów w tablicy. W rzeczywistości jest to wartość, określająca największy obecny w tablicy indeks powiększony o jeden. W naszym przypadku ostatnim indeksem jest 2 (arr[2] ===”c”) więc właściwości length została przypisana wartość 3.

Początkujący programista JavaScript mógłby w tym momencie powiedzieć, że przecież jaka to różnica, skoro i tak właściwość length zwraca prawidłową ilość elementów znajdujących się w tablicy.

Dodawanie elementów do tablicy

Przeanalizujmy jednak poniższy fragment kodu:

var arr = ['a', 'b', 'c'];
arr.length; //3 - zgadza się

arr[3] = 'd';
arr.length; //4 - nadal wszystko jest ok

arr[6] = 'e';
arr.length; //7 - ups... czyżby w tablicy było aż 7 elementów?

arr; //['a', 'b', 'c', 'd', undefined, undefined, 'e']
arr[4] //undefined

Prześledźmy powyższy kod. Na początku zadeklarowaliśmy tablicę z trzema elementami. W kolejnym kroku dodaliśmy do tablicy kolejny element ustawiając go na indeksie 3.

W pewnym momencie dodając nowy element ‚e’ celowo lub przypadkowo użyliśmy do tego indeksu o numerze 6, pomijając indeks 4 i 5. Sprawdzając właściwość arr.length otrzymujemy siedem, co świadczy, że w tablicy znajduje się łącznie siedem elementów.

Tak rzeczywiście jest, gdyż JavaScript dynamicznie dodał brakujące elementy o indeksach 4 i 5. Skoro nie przekazaliśmy jawnie jakie mają mieć one wartości to zostały im przypisane wartości domyślne, czyli undefined (oznaczające brak wartości).

Z tego powodu nie zaleca się tworzenia nowych elementów tablic z wykorzystaniem jawnego wskazywania indeksów, chyba, że z jakiś względów robimy to celowo i pamiętamy o problemach związanych z niezachowaniem ciągłości numeracji indeksów.

Dodawanie elementów z użyciem metod Array.prototype

Jeśli chcemy dodawać elementy do istniejącej tablicy lepszym rozwiązaniem jest zastosowanie metod obiektu Array o nazwach push() lub unshift(); Metoda push dodaje element na końcu tablicy i jest równoznaczna z zapisem arr[arr.length] = „value”.

var arr = [1,2,3,4,5];
arr.push(6);
arr; //[1,2,3,4,5,6]
arr[arr.length] = 7;
arr; //[1,2,3,4,5,6,7]

Metoda unshift dodaje element na początku tablicy. Nie można jednak utożsamiać tego z zapisem arr[0] = „value”. Przeanalizujmy poniższy fragment kodu:

var arr = [1,2,3,4];
arr.unshift(0);
arr; //[0,1,2,3,4];
arr[0] = 'ups';
arr; //['ups',1,2,3,4] - zmieniono pierwszy element!

W rzeczywistości metoda unshift najpierw wykonuje wewnętrzną kopię tablicy, następnie dodaje nowy element na zerowym indeksie, po czym ponownie dodaje do tablicy wszystkie elementy z indeksami zmienionymi o 1. Jest to operacja dość czasochłonna, szczególnie w przypadku bardzo dużych tablic. Jeśli z jakiś powodów musimy często dodawać elementy na początkowych indeksach to lepszym rozwiązaniem jest odwrócenie kolejności elementów (metoda reverse), zastosowanie metody push i ponowne wywołanie metody reverse. Warto przy tym zaznaczyć, że do metody push możemy przekazać wiele parametrów, i każdy z nich zostanie wstawiony do tablicy jako oddzielny element.

A co z usuwaniem elementów z tablicy?

No dobrze, wiemy już zatem jak bezpiecznie dodawać nowe elementy aby zachować oczekiwaną przez nas wartość właściwości length. Co jednak gdy musimy usunąć z tablicy wybrane elementy?

W niektórych poradnikach i kursach można znaleźć sposób bazujący na instrukcji delete. Jest to jednak bardzo zła praktyka, gdyż w rzeczywistości nie prowadzi do usunięcia elementu lecz usunięcia jego wartości.

var arr = [1,2,3,4];
arr.length; //4
delete arr[3]; //próbujemy usunąć ostatni element
arr.length; //4 ups... coś poszło nie tak?
arr; //[1,2,3,undefined]

Widzimy więc, że instrukcja delete nie nadaje się do modyfikacji elementów tablicy. Osobiście uważam, że nawet jeśli celowo chciałbyś przypisać któremuś z indeksów udenfined (czyli brak wartości) to lepszym rozwiązaniem jest jawna instrukcja przypisania:

var arr = ['a', 'b', 'c'];
delete arr[1]; //Zła praktyka!
arr; //['a', undefined, 'c']
arr[2] = undefined; //Lepsze rozwiązanie
arr; //['a', undefined, undefined]

Zawsze gdy staniemy przed koniecznością przypisywania poszczególnym elementom tablicy wartości typu undefined warto zastanowić się dlaczego jest nam to potrzebne i czy na pewno jest to dobre posunięcie?

W języku JavaScript tablice mogą zawierać wartości różnych typów, poprawna jest więc tablica [1, 2, 3, „d”, „e”] choć uważam, że stosowanie tego typu tablic wcześniej czy później może prowadzić do problemów. Mogą one wystąpić podczas często stosowanych iteracji po tablicach np. przy użyciu pętli lub metod Array.prototype (map, filter, every, some, forEach itp.) w momencie sprawdzania warunków bazujących na porównaniach z wartościami elementów tablicy.

No dobrze, więc jak zatem bezpiecznie usuwać elementy z tablicy? Mamy tutaj dwa rozwiązania, w zależności od tego jaki element chcemy usunąć. Jeśli usuwamy pierwszy lub ostatni to warto skorzystać z metod pop (usuwa arr[0]) lub unshift (usuwa arr[length-1]). Metoda unshift jest znacznie wolniejsza od metody pop, gdyż po usunięciu pierwszego elementu dokonuje również przenumerowania indeksów dalszych elementów tak, aby cały czas pierwszy element tablicy posiadał indeks „0”.

A gdy trzeba usunąć elementy ze środka tablicy?

W tej sytuacji można skorzystać z metody splice, która pobiera kilka parametrów, lecz nas w tym wypadku interesują tylko pierwsze dwa: indeks, od którego rozpoczniemy usuwanie elementów oraz ilość usuwanych elementów. Dalsze argumenty, jeśli zostaną podane będą stanowić elementy dodawane w miejscu usuwanych.

Spróbujmy zatem zadeklarować tablicę, a następnie usunąć jej pierwszy i ostatni element oraz wybrany element środkowy:

var arr = [1,2,3,4,5,6,7];
arr.pop();
arr; //[1,2,3,4,5,6];
arr.length; //6
arr.unshift();
arr; //[2,3,4,5,6]
arr.length; //5;
arr[0] //2
//A teraz usuwamy trzeci element, czyli arr[2]===4
arr.splice(2,1);
arr; //[2,3,5,6]
arr.length; //4

Widzimy zatem, że stosowanie wyżej opisanych metod pozwala na całkowicie bezpieczne i trwałe usunięcie elementów tablicy z zachowaniem oczekiwanej wartości właściwości length, która faktycznie będzie wskazywać ilość elementów znajdujących się w tablicy.

Podsumowanie – czyli jak bezpiecznie korzystać z array length?

Przede wszystkim należy przyzwyczaić się do faktu, że w JavaScript właściwość length nie jest w żaden sposób związana z rzeczywistą ilością elementów w tablicy. Określa ona największy obecnie użyty indeks powiększony o jeden…

Chyba, że samodzielnie zmodyfikujemy wartość length, czego w żadnym razie nie powinniśmy robić! Zobaczmy dlaczego:

var arr = [1,2,3];
arr.length; //3
arr.length = 5; //ups... tego nie rób!
arr; //[1,2,3,undefined,undefined]

Ze względu na opisane w artykule problemy zalecam korzystanie z funkcji Array.prototype w celu modyfikacji tablic (push, pop, shift, splice itd.). Uchroni to przed przypadkową, nieoczekiwaną zmianą zawartości tablicy, a tym samym jej właściwości length.