JavaScript – jak dobrać się do obiektu…

Dzisiaj zajmiemy się omówieniem różnych możliwości dostępu do właściwości obiektów JavaScript oraz ich wartości. Pod pojęciem właściwości obiektu mam na myśli również tzw. metody. W JavaScript nie ma tak wyraźnego podziału na „właściwości” i „metody” jak w PHP czy innych językach obiektowych, ale nie to będzie tematem dzisiejszego wpisu.

W języku JavaScript obiekt to jeden z podstawowych elementów z jakim będziemy się spotykać. Niekiedy można spotkać nawet stwierdzenia, że w JS wszystko jest obiektem, ale nie jest to do końca prawdą, gdyż ECMAScript posiada również typy proste (np. string). Można na nich co prawda wywoływać metody np.  String.prototype.replace, ale w rzeczywistości nastąpi wtedy dynamiczne wywołanie konstruktora, a po zakończeniu operacji obiekt zostanie usunięty i ponownie będziemy mieli wartość typu prostego.

Właściwości i deskryptory w obiektach JavaScript

W JavaScript obiekty możemy tworzyć na kilka sposobów – z użyciem składni literalnej, konstruktora Object lub metody Object.create. Aby stworzyć prosty obiekt zawierający kilka właściwości można użyć zapisu:

let person = {
    name: "Tomek",
    age: "31"
}

Od czasu pojawienia się ECMAScript 5 dysponujemy większą kontrolą nad możliwością modyfikowania właściwości obiektów. Mowa tutaj o tzw. deskryptorach właściwości, które określają dla każdej z właściwości cztery parametry:

  • value, czyli wartość danej właściwości,
  • enumerable, czyli „wyliczalność” w pętlach for-in,
  • writable, czyli możliwość zmiany wartości,
  • configurable, czyli możliwość usuwania i zmiany artybutów właściwości.

Nie będę dzisiaj rozwijał szerzej tematu deskryptorów i ochrony obiektów gdyż jest to temat na oddzielny artykuł. Teraz interesuje nas jedynie enumerable. Przypisanie jej wartości false sprawia, że dana właściwość obiektu nie będzie widoczna w czasie iterowania po nim pętlą for-in.

Ustawianie deskryptorów właściwości

Jeśli masz doświadczenie z ustawianiem deskryptorów to ten akapit możesz śmiało ominąć.

Na wstępie należy zaznaczyć, że w JS każda właściwość w obiekcie posiada deskryptory, nawet jeśli ich jawnie nie ustawimy. W takim wypadku wszystkie mają wartość true. Oznacza to, że właściwość jest w pełni edytowalna, konfigurowalna i pojawi się w pętli for-in.

Do wstawienia właściwości z jawnie przypisanymi deskryptorami posłużymy się metodą Object.defineProperty(obj, property):

Object.defineProperty(person, "ses", {
    value: "male"
});

Object.getOwnPropertyDescriptor(person, "sex").enumerable; //false

W powyższym przykładzie tworzymy nową właściwość „skill” obiektu „person”, której przypisujemy wartość „JavaSript”. Nie określiliśmy pozostałych deskryptorów i w tym wypadku jest im przypisywana wartość false, czyli nie będą one widoczne w for-in oraz nie można zmienić ich wartości ani usunąć (czy zmienić deskryptorów). W pewnym sensie można powiedzieć, że jest to właściwość prywatna, ale na razie unikajmy takich stwierdzeń – za chwilę okaże się dlaczego de facto nie jest to tak do końca właściwość prywatna i niewidoczna dla świata zewnętrznego.

No dobrze, to teraz stwórzmy właściwość, która będzie widoczna w pętli for-in, ale nie będzie można zmienić jej wartości:

Object.defineProperty(person, "skill", {
    value: "JavaScript",
    enumerable: true,
    writable: false,
    configurable: false
})

Iterowanie po obiekcie pętlą for-in

Ok, podsumujmy więc co udało nam się dodać do bazowego obiektu person:

person.propertyIsEnumerable("name");  //true
person.propertyIsEnumerale("age");    //true
person.propertyIsEnumerable("sex");   //false
person.propertyIsEnumerable("skill"); //true

Mamy więc cztery właściwości, z których tylko trzy są widoczne w pętli for-in:

for (let prop in person) {
    console.log(prop);
}
//name, age, skill -> brak "sex"

Pętla for-in nie pokazuje więc właściwości, dla której przypisany został deskryptor enumerable:false. Jest to jeden z często stosowanych sposobów dostępu do właściwości obiektu, lecz nie jedyny. Zaleca się stosowanie w tym wypadku dodatkowego zabezpieczenia z użyciem metody person.hasOwnProperty(prop) aby iterować tylko po właściwościach samego obiektu person, ale tym problemem nie będziemy się teraz zajmować (dla uproszczenia listingów nie będę wprowadzał w nich metody hasOwnProperty).

Object.keys vs Object.getOwnPropertyNames

Jeśli interesują nas wyłącznie nazwy właściwości to nie musimy tworzyć mało eleganckiego rozwiązania z pętlą for-in lecz możemy zastosować jedną z dwóch (choć za chwilę poznamy więcej) metod do wyciągnięcia kluczy, czyli nazw właściwości. Określenie klucze w nazwie (Object.keys) prawdopodobnie wzięło się z analogii do kluczy w tablicach asocjacyjnych, występujących w innych językach programowania.

Trzeba tutaj jednak wyraźnie zaznaczyć, że w JavaScript nie istnieją „tablice asocjacyjne” i nie można mówić, że ich rolę w JS pełnią obiekty. Jest to niestety dość często spotykane twierdzenie, ale zupełnie nieprawdziwe. Tak na prawdę to w JavaScript tablice są obiektami, ale nigdy obiekty tablicami! (dlatego jak mantrę często na forach i w niektórych wpisach powtarzam, aby w żadnym wypadku nie próbować bezpośrednio porównywać JS do takich języków jak PHP, Java, C++ itp.).

Pamiętasz jak chwilę temu pisałem, że nadanie właściwości deskryptora enumerable:false  nie tworzy do końca właściwości prywatnej, niewidocznej z poza obiektu? No to spójrz na poniższy kod:

Object.keys(person);
//["name", "age", "skill"]

Object.getOwnPropertyNames(person);
//["name", "age", "sex", "skill"]

Widać teraz wyraźnie, że mimo przypisania enumerable:false właściwość „sex” jest widoczna przy użyciu metody getOwnPropertyNames. Trzeba pamiętać o tej różnicy między obiema metodami, nawet jeśli obecnie nie stosujemy jawnego określania deskryptorów. Przyjdzie kiedyś potrzeba ich użycia i nasz program może się nieco posypać 🙂

Zwróć również uwagę, że obie metody zwracają tablicę zawierającą nazwy właściwości, niezależnie od tego ile nazw zostaje zwróconych. Dla pustego obiektu otrzymamy pustą tablicę. Pod hasłem „pusty obiekt” mam tu na myśli obiekt bez „własnych” właściwości, czyli takich, dla których metoda hasOwnProperty zwróci true (nie uwzględniamy więc prototypu obiektu).

A co jeśli interesują nas wartości, a nie nazwy właściwości obiektu?

Załóżmy, że interesują nas wartości wszystkich właściwości dodanych do obiektu person. Zobaczmy więc dwa rozwiązania tego problemu, z użyciem pętli for-in oraz metody Object.values:

//METODA 1
let val1 = [];
for (let prop in person) {
    val1.push(person[prop]);
}
val1; //["Tomek", 31, "JavaScript"]


//METODA 2
let val2 = Object.values(person);
val2; //["Tomek", 31, "JavaScript"]

W obu przypadkach otrzymujemy wartości wyłącznie dla właściwości posiadających deskryptor enumerable ustawiony na true. Osobiście uważam, że czytelniejsza jest forma z użyciem Object.values, ale jeśli piszemy dla starszych środowisk to trzeba zabezpieczyć odpowiedni polyfill.

A co, jeśli chcielibyśmy uzyskać w zwrotnej tablicy również wartość „male”, czyli wartość właściwości „sex”, która posiada deskryptor enumerable ustawiony na false? Co ciekawe, jest to możliwe:

let val = Object.getOwnPropertyNames(person).map(property => person[property]);
val; //["Tomek". 31, "male", "JavaScript"]

Myślę, że teraz rozumiesz dlaczego pisałem wcześniej, aby nie stosować zbyt pochopnie określeń „właściwość prywatna”, z którymi spotykam się czasami nawet w literaturze i w internecie.

Powyższy przykład najlepiej traktuj jako ciekawostkę. Jeśli w praktyce musiałbyś uzyskać dostęp do właściwości z enumerable:false  podczas iterowania po obiekcie to polecam cofnąć się o krok i ponownie rozważyć, czy na pewno ustawianie niewyliczalności ma w tym wypadku sens. To, że coś da się zrobić różnymi sztuczkami nie oznacza, że warto z tego korzystać.

I na koniec metoda Object.entries

Ciekawą metodą jest również Object.entries, która zwraca tablicę indeksowaną numerycznie (od zera). Każda z takich tablic zawiera kolejną tablicę, w której przechowywane są nazwa właściwości i jej wartość (jako oddzielne elementy tablicy). No dobra… opis zapewne nieco zagmatwany, dlatego najlepiej zobaczmy to na przykładzie:

let arr = Object.entries(person);
arr; //[
         ["name", "Tomek"],
         ["age", 31],
         ["skill", "JavaScript"]
       ]

Zobaczmy teraz, jak można tak uzyskaną tablicę wykorzystać:

Object.entries(person).forEach(([property, value]) => {
    console.log("%s: %s", property, value);
});

//name: Tomek
//age: 31
//skill: JavaScript

Widzimy tutaj, że metoda Object.entries nie uwzględnia właściwości z deskryptorem enumerable:false. Ze względów opisanych wcześniej nie będziemy tutaj zajmować się próbą „ominięcia” tego zabezpieczenia.

Można by zadać pytanie czy taka forma „rozłożenia” obiektu na tablice ma sens i do czegoś się przyda? Otóż tak, jednym z przykładów może być chociażby modyfikowanie styli CSS czy atrybutów elementów DOM.

Zmiana pojedynczego stylu najczęściej realizowana jest poprzez zapis:

document.querySelector('.xxx').style.color = "red";

Ale co w sytuacji, gdy dla danego elementu chcemy ustawić kilka styli? Jedną z metod (wg mnie najlepszą) byłoby stworzenie w CSS nowej klasy i przypisanie jej z poziomu JS tak, aby nadpisała wcześniejsze style, które chcemy zmienić (np. z użyciem metod element.classList). Zaletą tego rozwiązania jest uniknięcie określania reguł CSS w JS, czyli de facto w dwóch miejscach (style CSS oraz potem w JS).

Jeśli jednak mimo wszystko zechcesz to zrobić w JS to można zestaw styli CSS przekazać jako obiekt i z użyciem metody Array.prototype.forEach jak w przykładzie powyżej wywoływać ustawianie kolejnych styli.

Innym przykładem, wg mnie bardziej sensownym  byłoby np. dynamiczne ustawianie atrybutów dla nowo tworzonych elementów DOM, czego już nie wykonamy z poziomu CSS czy HTML. Rozwiązanie tego problemu pozostawiam czytelnikowi (w razie problemów proszę pisać w komentarzach to pomogę).

Podsumowanie

Głównym celem dzisiejszego artykułu było pokazanie, że dostęp do obiektów i iterowanie po nich możliwe jest nie tylko poprzez pętlę for-in. Osobiście uważam, że lepszym rozwiązaniem jest korzystanie z metod jasno wskazujących zamysł programisty jak Object.keys, Object.values, Array.prototype.map, Array.prorotype.filter itp. zamiast notorycznego używania pętli for-in, for-of i „tradycyjnego” for.

Można by dyskutować nad wydajnością różnych rozwiązań, ale w praktyce w większości aplikacji wydajność rzędu pojedynczych milisekund nie ma większego znaczenia. Warto więc zawsze poszukiwać rozsądnego kompromisu między wydajnością, a czytelnością kodu.

W artykule pokazałem również, jak można „oszukać” deskryptor enumerable:false. Zalecam jednak dużą ostrożność w stosowaniu tego typu rozwiązań gdyż wcześniej czy później mogą one okazać się strzałem w kolano, szczególnie w aplikacjach stale rozwijanych i rozbudowywanych.

  • Pingback: Co nowego w ECMASCript 2016 i 2017? - Poradnik JavaScript()

  • Świąteczny Karol

    bardzo fajny wpis, końcówka z metodą Object.entries bardzo mi pomogła. Oraz to, że można zwrócić się o pomoc :). Dziś już niestety nie mam czasu na wyjaśnienia. Ale jutro opiszę mój problem związany z tworzeniem przeglądarki zdjęć. Jeśli dobrze myślę, powyższa metoda pomoże mi dynamicznie zmieniać wymiary zdjęć, ale też nie tylko…

    • cieszę się, jeśli komuś wpis się przydał 🙂
      Co do pomocy to jak najbardziej potwierdzam taką możliwość 🙂

      Ewentualnie możesz dać posta z prośbą o pomoc na forum: forum.pasja-informatyki.pl gdzie więcej osób się zaangażuje w temat, a ja również staram się tam w miarę często bywać 🙂