You are on page 1of 22

Typy proste

Typy proste reprezentują dane zapisane bezpośrednio w takiej formie, w jakiej występują,
jak choćby wartości true czy 25 . W JavaScripcie istnieje pięć typów prostych:

boolean wartość logiczna true albo false


number liczbowa wartość całkowita lub zmiennoprzecinkowa
string znak lub łańcuch znaków ujęty w cudzysłowy lub apostrofy (w JavaScripcie nie ma
odrębnego typu dla pojedynczego znaku)
null typ prosty posiadający tylko jedną wartość: null
undefined typ prosty posiadający tylko jedną wartość: undefined (jest to wartość
przypisywana do zmiennej, która nie została jeszcze zainicjalizowana)

Pierwsze trzy typy ( boolean , number i string ) zachowują się podobnie, natomiast dwa
ostatnie ( null i undefined ) są traktowane inaczej. Wartości wszystkich typów prostych mają
reprezentację w postaci literałów. Literały to te wartości, które nie znajdują się w zmiennych,
czyli na przykład zapisane w kodzie imię lub cena.

Zmienne typów prostych zajmują odrębne miejsca w pamięci, więc zmiany wartości jednej
zmiennej nie wpływają na inne zmienne.

??Operator typeof dla wartości null zwraca "object" . Dlaczego obiekt, skoro to typ null ?
Sprawa jest dyskusyjna (TC39, czyli komitet odpowiedzialny za zaprojektowanie i
utrzymywanie JavaScriptu, przyznał, że to błąd), ale jeśli przyjmiemy, że null jest pustym
wskaźnikiem do obiektu, rezultat "object" można uznać za uzasadniony.

Typy referencyjne
Typy referencyjne reprezentują w JavaScripcie obiekty. Referencje to instancje typów
referencyjnych, więc można je uznać za synonim obiektów.
Obiekt jest nieuporządkowaną listą właściwości złożonych z nazwy (będącej zawsze
łańcuchami) i wartości. W przypadku gdy wartością właściwości jest funkcja, nazywa się ją
metodą.

Tworzenie obiektów
Sposób tworzenia ogólnych obiektów za pomocą operatora new i konstruktora: new
Object() .

var object1 = new Object();


var object2 = object1;

W przypadku typów referencyjnych obiekty nie są zapisywane bezpośrednio w zmiennej, do


której są przypisywane, więc zmienna object z powyższego przykładu nie zawiera
utworzonego obiektu. Przechowywany jest za to wskaźnik (czyli referencja) do lokalizacji w
pamięci, gdzie znajduje się obiekt. Jest to podstawowa różnica między obiektami i
wartościami typów prostych, które są przechowywane bezpośrednio w zmiennej.
Typ Object jest tylko jednym z kilku przydatnych wbudowanych typów referencyjnych
dostępnych w języku JavaScript. Pozostałe są bardziej wyspecjalizowane.
Array tablica indeksowana numerycznie
Date data i czas
Error błąd w czasie wykonania (istnieją również bardziej konkretne podtypy błędów)
Function funkcja
Object ogólny obiekt
RegExp wyrażenie regularne

Instancje wbudowanych typów można tworzyć za pomocą operatora new , co widać w


poniższym przykładzie:
var items = new Array();
var now = new Date();
var error = new Error("Stało się coś złego.");
var func = new Function("console.log('Cześć');");
var object = new Object();
var re = new RegExp("\\d+");

Literał to składnia umożliwiająca zdefiniowanie typu referencyjnego bez potrzeby


jawnego tworzenia obiektu za pomocą operatora new i konstruktora.

Literały obiektów i tablic


Aby utworzyć obiekt za pomocą literału obiektu, wystarczy zdefiniować właściwości
wewnątrz nawiasów klamrowych.

var book = {
name: "JavaScript. Zasady programowania obiektowego",
year: 2014
};

Ten sam efekt można uzyskać w jeszcze inny sposób:

var book = new Object();


book.name = "JavaScript. Zasady programowania obiektowego";
book.year = 2014;

var colors = [ "czerwony", "niebieski", "zielony" ];


console.log(colors[0]); // "czerwony"

Taki zapis daje ten sam efekt co poniższy kod:

var colors = new Array("czerwony", "niebieski", "zielony");


console.log(colors[0]); // "czerwony"

Literały funkcji

function reflect(value) {
return value;
}

// jest równoważne temu: ALE rzadko stosowane

var reflect = new Function("value", "return value;");

Literały wyrażeń regularnych

var numbers = /\d+/g;

// jest równoważne temu:

var numbers = new RegExp("\\d+", "g");

Identyfikowanie typów referencyjnych


Najprostszym do zidentyfikowania typem referencyjnym jest funkcja, ponieważ operator
typeof zwraca łańcuch "function".
Pozostałe typy referencyjne trudniej określić, ponieważ operator typeof zwraca dla nich
wszystkich łańcuch "object" . Może się to okazać problemem, jeśli korzysta się z wielu
różnych typów. W takich sytuacjach z pomocą przychodzi inny operator języka JavaScript —
instanceof .
Operator ten ma dwa parametry: obiekt i konstruktor. Jeśli obiekt jest instancją typu
określonego przez konstruktor, operator zwraca true , a w przeciwnych przypadku — false.
Oto przykład:
var items = [];
var object = {};
function reflect(value) {
return value;
}

console.log(items instanceof Array); // true


console.log(object instanceof Object); // true
console.log(reflect instanceof Function); // true

Podsumowanie
Mimo że w języku JavaScript nie ma klas, są typy. Wszystkie zmienne i fragmenty danych
są powiązane z określonymi wartościami typu prostego lub referencjami. Pięć typów
prostych (łańcuchy, liczby, wartości logiczne, null i undefined ) reprezentuje wartości
zapisywane bezpośrednio w obiekcie zmiennych dla danego kontekstu. Do identyfikowania
tych typów można użyć operatora typeof . Wyjątkiem jest null , który można wykryć tylko
przez bezpośrednie porównanie z wartością null.

Typy referencyjne w JavaScripcie najbliżej odpowiadają klasom znanym z innych języków.


Obiekty są instancjami klas referencyjnych. Nowe obiekty można tworzyć za pomocą
operatora new lub literału referencji. Dostęp do właściwości i metod jest realizowany przede
wszystkim za pomocą notacji z kropką, ale można też korzystać z notacji z nawiasami
kwadratowymi. Funkcje są w JavaScripcie obiektami i można je identyfikować za pomocą
operatora typeof . Aby sprawdzić, czy obiekt jest instancją określonego typu referencyjnego,
należy użyć operatora instanceof .

Dzięki trzem typom opakowującym — String , Number i Boolean — zmienne typów prostych
można traktować jak referencje. Silnik JavaScriptu automatycznie tworzy obiekty
opakowujące, więc z poziomu kodu prostymi zmiennymi można się posługiwać jak
obiektami, ale trzeba pamiętać, że obiekty te są tymczasowe, więc są niszczone zaraz po
ich użyciu. Mimo że instancje typów opakowujących można tworzyć samodzielnie, nie jest to
zalecane ze względu na wprowadzanie zamieszania i większe prawdopodobieństwo
popełnienia pomyłki.

2. FUNKCJE
W JAVASCRIPCIE FUNKCJE SĄ OBIEKTAMI . CECHĄ ODRÓŻNIAJĄCĄ JE OD INNYCH
OBIEKTÓW JEST WEWnętrzna właściwość [[Call]].

Ponieważ funkcje są obiektami, zachowują się trochę inaczej niż w innych językach.

Dla funkcji istnieją dwie postacie literałów. Pierwsza to deklaracja funkcji, która rozpoczyna
się od słowa kluczowego function i następującej po nim nazwy funkcji. Zawartość funkcji jest
umieszczana w nawiasach klamrowych, co widać w poniższym przykładzie:

function add(num1, num2) {


return num1 + num2;
}

Drugi z literałów to wyrażenie funkcyjne, które nie wymaga podawania nazwy funkcji po
słowie kluczowym function . Tego typu funkcje określa się jako anonimowe, ponieważ
obiekt funkcji nie ma nazwy. Do wyrażeń funkcyjnych tworzy się zazwyczaj referencje i
zapisuje je w zmiennych lub właściwościach, jak w poniższym przykładzie:

var add = function(num1, num2) {


return num1 + num2;
};

!!!! Mimo że te dwie definicje wyglądają bardzo podobnie, różnią się w dość znaczący
sposób. Przed uruchomieniem kodu deklaracja funkcji jest niejawnie przenoszona na
początek kontekstu (czyli zawierającej ją funkcji lub zasięgu globalnego). Mechanizm ten
jest określany terminem hoisting. Wynika stąd jeden istotny wniosek — funkcję można
zdefiniować w dowolnym miejscu kodu (nawet po jej wywołaniu), a i tak będzie widoczna
wszędzie.
var result = add(5, 5);
function add(num1, num2) {
return num1 + num2;
}

Mechanizm przenoszenia działa tylko w przypadku deklaracji funkcji, ponieważ ich nazwy są
znane z wyprzedzeniem. Wyrażenia funkcyjne nie mogą zostać przeniesione, ponieważ
referencje do nich są przechowywane w zmiennych. W związku z tym poniższy kod
spowoduje zgłoszenie błędu:

// błąd!
var result = add(5, 5);
var add = function(num1, num2) {
return num1 + num2;
};

Parametry
Wersja wykorzystująca obiekt arguments może być myląca — do pełnego zrozumienia
wymagane jest przeanalizowanie całego kodu funkcji. Dlatego właśnie większość
programistów stara się unikać korzystania z obiektu arguments. W niektórych przypadkach
użycie obiektu arguments jest jak najbardziej uzasadnione. Jeśli na przykład chcielibyśmy
zdefiniować funkcję przyjmującą dowolną liczbę parametrów i zwracającą ich sumę, nie
moglibyśmy posłużyć się nazwanymi parametrami, ponieważ w chwili defi-
niowania nie wiemy, ile ich będzie potrzebnych.

function sum() {
var result = 0,
i = 0,
len = arguments.length;
while (i < len) {
result += arguments[i];
I++;
}
return result;
}

* Z parametrem:
function reflect(value) {
return value;
}

W JavaScripcie przy próbie zdefiniowania wielu funkcji o tej samej nazwie ostatnia definicja
„wygrywa”, czyli zastępuje poprzednie.

function sayMessage(message) {
console.log(message);
}

function sayMessage() {
console.log("Domyślny komunikat");
}

sayMessage("Cześć!"); // wyświetla "Domyślny komunikat"

Metody obiektów

Do obiektów w dowolnej chwili można dodawać nowe właściwości oraz usuwać istniejące.
Jeśli wartość właściwości jest funkcją, właściwość tę nazywa się metodą.
Metody można dodawać do obiektów dokładnie na tej samej zasadzie co zwykłe
właściwości.

var person = {
name: "Nicholas",
sayName: function() {
console.log(person.name);
}
};
person.sayName(); // wyświetla "Nicholas"

Obiekt this

Każdy zasięg w JavaScripcie posiada obiekt this reprezentujący obiekt, na którym została
wywołana funkcja.

W globalnym zasięgu this reprezentuje globalny obiekt (jeśli kod jest wykonywany w
przeglądarce, jest to obiekt window ).

Jeśli funkcja jest wywoływana na obiekcie, wartość this jest domyślnie ustawiana na ten
obiekt.

var person = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
person.sayName(); // wyświetla "Nicholas"

Kod ten działa dokładnie tak samo jak poprzednia jego wersja, ale tym razem w metodzie
sayName() odwołujemy się do pola name poprzez this , a nie person . Dzięki temu można
bez problemu zmienić nazwę zmiennej przechowującej obiekt, a nawet użyć tej funkcji w
innych obiektach.

Modyfikowanie this

Funkcje mogą być używane w wielu różnych kontekstach.


Istnieją trzy metody obiektu funkcji, które pozwalają na zmianę this (funkcje są obiektami,
więc mają również metody).

1.Metoda call()
Uruchamia funkcję z określoną wartością this.
Wartość tę przekazujemy przez pierwszy parametr metody. Kolejne parametry zawierają
wartości, które mają zostać przekazane do wywoływanej funkcji jako jej parametry.

function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Nicholas"
};
var person2 = {
name: "Greg"
};
var name = "Michael";
sayNameForAll.call(this, "globalny");// wyświetla "globalny:Michael"
sayNameForAll.call(person1, "person1");// wyświetla "person1:Nicholas"
sayNameForAll.call(person2, "person2");// wyświetla "person2:Greg"

2.Metoda apply()
Działa ona bardzo podobnie jak call() , ale ma tylko dwa parametry: wartość dla this oraz
tablicę lub obiekt pełniący rolę tablicy z wartościami parametrów, które mają zostać
przekazane do wywoływanej funkcji (oznacza to, że drugim parametrem może być obiekt
arguments ).

function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Nicholas"
};
var person2 = {
name: "Greg"
};
var name = "Michael";
sayNameForAll.apply(this, ["globalny"]); // wyświetla "globalny:Michael"
sayNameForAll.apply(person1, ["person1"]); // wyświetla "person1:Nicholas"
sayNameForAll.apply(person2, ["person2"]); // wyświetla "person2:Greg"

3.Metoda bind()
Pierwszy argument to wartość this dla nowej funkcji. Wszystkie pozostałe argumenty to
nazwane parametry, które powinny być na stałe ustawione w nowej funkcji.

function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Nicholas"
};
var person2 = {
name: "Greg"
};
// tworzy funkcję powiązaną z obiektem person1
var sayNameForPerson1 = sayNameForAll.bind(person1);
sayNameForPerson1("person1");// wyświetla "person1:Nicholas"

// tworzy funkcję powiązaną z obiektem person2


var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");
sayNameForPerson2(); // wyświetla "person2:Greg"

// dołączanie metody do obiektu nie zmienia wartości this


person2.sayName = sayNameForPerson1;
person2.sayName("person2"); // wyświetla "person2:Nicholas"

Podsumowanie
Funkcje w języku JavaScript są wyjątkowe, ponieważ są obiektami, a więc można je kopiować,
nadpisywać i traktować jak wszystkie inne „normalne” obiekty. Funkcje odróżnia jednak od innych
obiektów specjalna wewnętrzna właściwość [[Call]] , która zawiera instrukcje wykonywania funkcji.
Operator typeof szuka tej właściwości i jeśli ją znajdzie, zwraca wartość "function".
Istnieją dwie formy literału funkcji: deklaracje i wyrażenia. Deklaracje funkcji składają się z
nazwy funkcji poprzedzonej słowem kluczowym function . Co ważne, deklaracje są przenoszone na
początek kontekstu (ang. hoisting), w którym zostały umieszczone. Wyrażenia funkcyjne są
stosowane tam, gdzie można użyć również innych wartości, czyli w przypisaniach, parametrach
funkcji, a także jako wartości zwracane przez inne funkcje.
Ponieważ funkcje są obiektami, posiadają konstruktor Function . Można go oczywiście użyć
do tworzenia nowych funkcji, ale nie jest to polecane, ponieważ tak skonstruowany kod trudniej się
analizuje i debuguje. Taka możliwość przydaje się jednak od czasu do czasu, gdy ciało funkcji nie jest
znane przed uruchomieniem kodu.
Funkcje trzeba dobrze zrozumieć, ponieważ to na nich opiera się obiektowość w
JavaScripcie. Ze względu na brak klas funkcje i inne obiekty są wszystkim, czym dysponujemy, by
tworzyć obiektowy kod wykorzystujący mechanizmy kompozycji i dziedziczenia.

3. OBIEKTY
Obiekty w JavaScripcie są dynamiczne, czyli można je modyfikować w czasie wykonywania
kodu, co nie jest możliwe w wielu innych językach obiektowych, w których definicja klasy
blokuje strukturę obiektu.

Dwa podstawowe sposoby tworzenia obiektów: za pomocą konstruktora Object i literału


obiektu.

var person1 = {
name: "Nicholas"
};

var person2 = new Object();


person2.name = "Nicholas";

person1.age = "ukryty";
person2.age = "ukryty";
person1.name = "Greg";
person2.name = "Michael";

Tworzone obiekty są gotowe na modyfikacje, chyba że zastosuje się rozwiązanie


blokujące taką możliwość.

Zaczynamy od utworzenia obiektu person1 za pomocą literału. Wykonywana jest wtedy


operacja [[Put]] dodająca właściwość name . Przypisanie wartości do person1.age
powoduje wywołanie metody [[Put]] dla właściwości age . Z kolei przypisanie nowej wartości
( "Greg" ) do person1.name powoduje wykonanie operacji [[Set]] na właściwości name ,
która nadpisuje dotychczasową wartość.

Wykrywanie właściwości

Początkujący programiści JavaScriptu często stosują w tej sytuacji nieprawidłową


konstrukcję:

// antywzorzec!
if (person1.age) {
// operacja na właściwości age
}

!! Warunek podany w instrukcji if zostanie uznany za true , jeśli jego wartość jest
„prawdziwa” (czyli jest obiektem, niepustym łańcuchem znaków, niezerową wartością
numeryczną lub wartością true ), a za false, jeżeli jest „fałszywa” ( null, undefined, 0, false,
NaN lub pusty łańcuch znaków)

Lepszym sposobem sprawdzania istnienia właściwości jest użycie operatora in.


Operator in szuka w określonym obiekcie właściwości o podanej nazwie i jeśli ją znajdzie,
zwraca true.

console.log("name" in person1); // true


console.log("age" in person1); // true
console.log("title" in person1); // false

Metody również są właściwościami (przechowującymi referencje do funkcji), więc w ten sam


sposób można sprawdzać istnienie metod.

var person1 = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};

console.log("sayName" in person1); // true


Sprawdzanie obecności właściwości instancji (ang. own property — własna właściwość).
Operator in wykrywa zarówno właściwości dodane, jak i te, które istnieją w prototypie.

Metoda hasOwnProperty() obecna we wszystkich obiektach zwraca true , jeśli dana


właściwość istnieje i jest to właściwość instancji.

var person1 = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};

console.log("name" in person1); // true


console.log(person1.hasOwnProperty("name")); // true

console.log("toString" in person1); // true


console.log(person1.hasOwnProperty("toString")); // false

“name” to właściwość instancji, więc zarówno operator in , jak i metoda hasOwnProperty()


zwracają true.
Metoda toString() to właściwość prototypu, która jest obecna we wszystkich obiektach.
Operator in zwraca dla niej wartość true, natomiast metoda hasOwnProperty() — false.

Usuwanie właściwości

Operator delete działa na pojedynczej właściwości obiektu i wywołuje wewnętrzną metodę


[[Delete]] .
Operację tę można potraktować jak usunięcie pary klucz-wartość z tablicy asocjacyjnej.
Jeśli operacja zakończy się sukcesem, operator delete zwraca true.
var person1 = {
name: "Nicholas"
};

console.log("name" in person1); // true

delete person1.name; // true - not output


console.log("name" in person1); // false
console.log(person1.name); // undefined

Wyliczenia

Wszystkie właściwości dodawane do obiektu są domyślnie wyliczalne (ang. enumerable),


czyli można po nich iterować za pomocą pętli for-in .
Pętla for-in przechodzi przez wszystkie właściwości wyliczalne w obiekcie i w każdej iteracji
przypisuje nazwę właściwości do zmiennej.

var property;
for (property in object) {
console.log("Nazwa: " + property);
console.log("Wartość: " + object[property]);
}

W każdym obiegu pętli for-in zmienna property jest wypełniana kolejną wyliczalną
właściwością obiektu. Proces ten jest przeprowadzany dla wszystkich właściwości, po czym
pętla kończy działanie.

Jeśli w programie jest potrzebna lista wszystkich właściwości jakiegoś obiektu, można
skorzystać z metody Object.keys() , która zwraca tablicę nazw wyliczalnych właściwości.

var properties = Object.keys(object);


// rozwiązanie alternatywne dla pętli for-in

var i, len;

for (i=0, len=properties.length; i < len; i++){


console.log("Nazwa: " + properties[i]);
console.log("Wartość: " + object[properties[i]]);
}

!! \Metoda zwraca listę właściwości instancji, natomiast pętla iteruje również po


właściwościach prototypu.

Rodzaje właściwości

Istnieją dwa odmienne rodzaje właściwości: danych i funkcji dostępowych.


Właściwości danych zawierają wartości, tak jak w przypadku właściwości name z
poprzednich przykładów.

Właściwości funkcji dostępowych nie zawierają wartości, lecz definicję funkcji


wywoływanej w celu odczytania (tzw. getter) lub zapisania (tzw. setter) wartości właściwości.

var person1 = {
_name: "Nicholas",
get name() {
console.log("Odczytuję właściwość name");
return this._name;
},
set name(value) {
console.log("Właściwość name ustawiam na %s", value);
this._name = value;
}
};

console.log(person1.name); // "Odczytuję właściwość name", a następnie


// "Nicholas"
person1.name = "Greg";

console.log(person1.name); // "Właściwość name ustawiam na",


// a następnie "Greg"

Atrybuty właściwości

Wspólne atrybuty
Istnieją dwa atrybuty występujące w obu rodzajach właściwości. Pierwszy z nich to
[[Enumerable]] , który określa, czy właściwość zostanie uwzględniona podczas iterowania
po zawierającym ją obiekcie. Drugi atrybut — [[Configurable]] — określa, czy właściwość
jest konfigurowalna, czyli czy jest możliwa jej zmiana.
Aby zmienić atrybut właściwości, należy użyć metody Object.defineProperty(), która
przyjmuje trzy argumenty: obiekt zawierający właściwość, nazwę właściwości i deskryptor
właściwości definiujący stan atrybutów.

var person1 = {
name: "Nicholas"
};
Object.defineProperty(person1, "name", {
enumerable: false
});

console.log("name" in person1); // true


console.log(person1.propertyIsEnumerable("name")); // false

Atrybuty właściwości danych


Właściwości danych posiadają dwa atrybuty, których nie mają właściwości funkcji
dostępowych. Pierwszy z nich to [[Value]] , który przechowuje wartość właściwości. Atrybut
ten jest automatycznie wypełniany w chwili dodawania właściwości do obiektu. Wszystkie
wartości są przechowywane w tym atrybucie, nawet jeśli są funkcjami.
Drugi z atrybutów to [[Writable]], który jest wartością logiczną wskazująca, czy do
właściwości można wpisać wartość. Wszystkie właściwości mają ten atrybut domyślnie
ustawiony na true.

Atrybuty właściwości funkcji dostępowych


Ponieważ w tego typu właściwościach nie są zapisywane wartości, atrybuty [[Value]] i
[[Writable]] nie są potrzebne. Niezbędne są za to atrybuty [[Get]] i [[Set]] , które
przechowują funkcje gettera i settera. Tak jak w przypadku literału obiektu wystarczy
zdefiniować jedną z nich, by właściwość została utworzona.

var person1 = {
_name: "Nicholas",

get name() {
console.log("Odczytuję właściwość name");
return this._name;
},
set name(value) {
console.log("Właściwość name ustawiam na %s", value);
this._name = value;
}
};

Definiowanie wielu właściwości


Możliwe jest również dodanie do obiektu wielu właściwości naraz. Służy do tego metoda
Object.defineProperties(), która przyjmuje dwa argumenty: obiekt, do którego chcemy
dodać właściwości, oraz obiekt zawierający definicje wszystkich właściwości.

var person1 = {};

Object.defineProperties(person1, {

// właściwość do przechowywania danych


_name: {
value: "Nicholas",
enumerable: true,
configurable: true,
writable: true
},

// właściwość funkcji dostępowych


name: {
get: function() {
console.log("Odczytuję właściwość name");
return this._name;
},
set: function(value) {
console.log("Właściwość name ustawiam na %s", value);
this._name = value;
},
enumerable: true,
configurable: true
}
});

Pobieranie atrybutów właściwości


Do odczytywania atrybutów właściwości służy metoda
Object.getOwnPropertyDescriptor(). Działanie metody ogranicza się tylko do właściwości
instancji. Metoda przyjmuje dwa argumenty: obiekt, w którym znajduje się właściwość, oraz
nazwę właściwości. Jeśli dana właściwość istnieje, metoda zwraca obiekt deskryptora z
czterema polami: configurable , enumerable i dwoma pozostałymi, zależnymi od
rodzaju właściwości.

var person1 = {
name: "Nicholas"
};
var descriptor = Object.getOwnPropertyDescriptor(person1, "name");

console.log(descriptor.enumerable); // true
console.log(descriptor.configurable); // true
console.log(descriptor.writable); // true
console.log(descriptor.value); // "Nicholas"

Zapobieganie modyfikowaniu obiektu

Atrybut [[Extensible]] określa, czy obiekt może być modyfikowany.


Wszystkie tworzone obiekty mają ten atrybut domyślnie ustawiony na true, czyli są
rozszerzalne, a więc w dowolnej chwili można do nich dodawać nowe właściwości.

Zapobieganie rozszerzaniu
Jednym ze sposobów tworzenia nierozszerzalnych obiektów jest użycie metody
Object.preventExtensions(), która przyjmuje jeden argument: obiekt, który ma się stać
nierozszerzalny. Po wykonaniu tej operacji do obiektu nie będzie już można dodać żadnej
właściwości.

var person1 = {
name: "Nicholas"
};

console.log(Object.isExtensible(person1)); // true

Object.preventExtensions(person1);
console.log(Object.isExtensible(person1)); // false

person1.sayName = function() {
console.log(this.name);
};

console.log("sayName" in person1); // false

Pieczętowanie obiektów
Tego typu obiekt jest nierozszerzalny, a wszystkie jego właściwości są niekonfigurowalne.
Jeśli obiekt jest zapieczętowany, można jedynie odczytywać i zapisywać jego
właściwości.
Należy użyć metody Object.seal(). Ustawia ona atrybut [[Extensible]] obiektu i atrybuty
[[Configurable]] wszystkich właściwości na false.

var person1 = {
name: "Nicholas"
};

console.log(Object.isExtensible(person1)); // true
console.log(Object.isSealed(person1)); // false

Object.seal(person1);
console.log(Object.isExtensible(person1)); // false
console.log(Object.isSealed(person1)); // true

person1.sayName = function() {
console.log(this.name);
};

console.log("sayName" in person1); // false

person1.name = "Greg";
console.log(person1.name); // "Greg"

delete person1.name;
console.log("name" in person1); // true

console.log(person1.name); // "Greg"

var descriptor = Object.getOwnPropertyDescriptor(person1, "name");


console.log(descriptor.configurable); // false

!!Tak jak w Java czy C++, mechanizm pieczętowania można porównać z tworzeniem
instancji klasy. Do utworzonego obiektu nie można dodawać nowych właściwości, ale — jeśli
właściwość zawiera jakiś obiekt — można go zmienić. Dzięki mechanizmowi pieczętowania
można więc osiągnąć podobny stopnień kontroli nad obiektami jak za pomocą klas.

Zamrażanie obiektów
Po zamrożeniu nie można dodawać i usuwać właściwości, zmieniać ich typu, a także
zapisywać wartości do właściwości danych. Zamrożony obiekt jest więc
zapieczętowanym obiektem z właściwościami ustawionymi na tryb tylko do odczytu. Tej
operacji nie można wycofać, więc do końca działania programu obiekty pozostają w stanie,
w jakim były w chwili zamrożenia.
Do zamrażania obiektów służy metoda Object.freeze().

var person1 = {
name: "Nicholas"
};

console.log(Object.isExtensible(person1)); // true
console.log(Object.isSealed(person1)); // false
console.log(Object.isFrozen(person1)); // false

Object.freeze(person1);
console.log(Object.isExtensible(person1)); // false
console.log(Object.isSealed(person1)); // true
console.log(Object.isFrozen(person1)); // true

person1.sayName = function() {
console.log(this.name);
};

console.log("sayName" in person1); // false

person1.name = "Greg";
console.log(person1.name); // "Nicholas"

delete person1.name;
console.log("name" in person1); // true
console.log(person1.name); // "Nicholas"

var descriptor = Object.getOwnPropertyDescriptor(person1, "name");


console.log(descriptor.configurable); // false
console.log(descriptor.writable); // false

!!!Podczas korzystania z opisanych tu technik należy zachować ostrożność i trzeba


koniecznie stosować tryb ścisły, tak by były zgłaszane (w postaci komunikatów o
błędach) wszelkie nieprawidłowości.

4. Konstruktory i prototypy
Ponieważ w JavaScripcie nie ma klas, to właśnie na konstruktory i prototypy spada ciężar
zaprowadzenia w obiektach porządku uzyskiwanego w innych językach dzięki klasom.

Konstruktory

Konstruktor jest funkcją, której używa się z operatorem new do utworzenia obiektu.
Zaletą stosowania konstruktorów jest to, że tworzone za ich pomocą obiekty mają te same
właściwości i metody.

Ponieważ konstruktor jest zwykłą funkcją, definiuje się go dokładnie w ten sam sposób.
Jedyna różnica polega na zapisywaniu nazw konstruktorów wielką literą.

function Person() {
// celowo pusta
}

Po zdefiniowaniu konstruktora możemy utworzyć instancje.

var person1 = new Person();


var person2 = new Person();

Typ instancji można również sprawdzić za pomocą właściwości constructor. Jest ona
składnikiem wszystkich obiektów i zawiera referencję do funkcji konstruktora, za pomocą
której obiekt został utworzony.

console.log(person1.constructor === Person); // true


console.log(person2.constructor === Person); // true

!!!Oczywiście pusta funkcja konstruktora nie jest zbyt przydatna. Cały sens definiowania
konstruktora polega na tym, by tworzone za jego pomocą obiekty miały te same właściwości
i metody. Aby to uzyskać, trzeba je dodać wewnątrz funkcji do obiektu this.

function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}

Konstruktor Person przyjmuje jeden parametr — name — i przypisuje jego wartość do


właściwości name obiektu this.
Konstruktor dodaje również do obiektu metodę sayName().
Obiekt this jest automatycznie tworzony przez operator new w chwili wywoływania
konstruktora i jest referencją do tworzonej instancji typu (czyli w tym przypadku this jest
instancją typu Person ).

Teraz można użyć konstruktora Person do utworzenia obiektów z zainicjalizowaną


właściwością name.

var person1 = new Person("Nicholas");


var person2 = new Person("Greg");

console.log(person1.name);// "Nicholas"
console.log(person2.name); // "Greg"

person1.sayName();// wypisuje "Nicholas"


person2.sayName();// wypisuje "Greg"

!!!Należy pamiętać, by zawsze wywoływać konstruktor za pomocą operatora new. W


przeciwnym przypadku istnieje ryzyko zmiany globalnego obiektu, a nie nowo tworzonego.

var person1 = Person("Nicholas"); // uwaga: brakuje operatora new

console.log(person1 instanceof Person); // false


console.log(typeof person1); // "undefined"
console.log(name); // "Nicholas"

W sytuacji gdy konstruktor Person jest wywoływany jak zwykła funkcja (czyli bez operatora
new ), this jest globalnym obiektem (globalny obiekt zależy od środowiska, w którym jest
uruchamiany kod JavaScript; jeżeli jest to przeglądarka, globalnym obiektem jest window).

Przypisanie wartości do this.name tworzy tak naprawdę właściwość name w globalnym


obiekcie, i to tam jest zapisywana wartość przekazana do funkcji Person .

!!!Konstruktory pozwalają na tworzenie obiektów o tych samych właściwościach, ale trzeba


wiedzieć, że nie da się za ich pomocą wyeliminować redundancji kodu, czyli istnienia wielu
egzemplarzy tego samego kodu. Oznacza to, że jeżeli utworzymy sto obiektów, będzie
istniało sto kopii funkcji, która wykonuje dokładnie taką samą operację, ale na innych
danych.

Prototypy
Prototypy można traktować jak przepisy na obiekt. Prawie wszystkie funkcje (z wyjątkiem
niektórych wbudowanych) mają właściwość prototype, używaną podczas tworzenia nowych
instancji.
Prototyp jest współdzielony przez wszystkie obiekty danego typu, które, co istotne, mają
dostęp do właściwości prototypu.
Na przykład metoda hasOwnProperty() jest zdefiniowana w prototypie ogólnego typu Object,
ale można z niej korzystać w dowolnym obiekcie

var book = {
title: "The Principles of Object-Oriented JavaScript"
};

console.log("title" in book); // true


console.log(book.hasOwnProperty("title")); // true
console.log("hasOwnProperty" in book); // true
console.log(book.hasOwnProperty("hasOwnProperty")); // false
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); // true

Mimo że w obiekcie book nie ma definicji metody hasOwnProperty(), można ją wywołać


poprzez book.hasOwnProperty() . Wynika to stąd, że definicja ta znajduje się w prototypie
typu Object (Object.prototype ).

Właściwość [[Prototype]]

Każda instancja przechowuje informację o swoim prototypie. Służy do tego wewnętrzna


właściwość [[Prototype]], która przechowuje wskaźnik na wykorzystywany obiekt
prototypu.

Gdy za pomocą operatora new tworzy się nowy obiekt, właściwość prototype konstruktora
jest przypisywana do właściwości [[Prototype]] nowo tworzonego obiektu.

var object = {};


var prototype = Object.getPrototypeOf(object);
console.log(prototype === Object.prototype); // true

Udaje się uniknąć wielokrotnego powtarzania kodu funkcji, ponieważ instancje danego typu
zawierają referencje do tego samego prototypu.

Używanie prototypów z konstruktorami

Dzięki swoim cechom prototypy doskonale nadają się do definiowania metod wspólnych
dla wszystkich instancji danego typu.
Metody wykonują te same operacje we wszystkich instancjach, nie ma powodu, by każdy z
obiektów przechowywał własną kopię metody.
!!Znacznie lepszym wyjściem jest umieszczenie metod w prototypie i korzystanie z
obiektu this w celu odwołania się do bieżącej instancji.

function Person(name) {
this.name = name;
}

Person.prototype.sayName = function() {
console.log(this.name);
};

var person1 = new Person("Nicholas");


var person2 = new Person("Greg");

console.log(person1.name); // "Nicholas"
console.log(person2.name); // "Greg"

person1.sayName(); // wypisuje "Nicholas"

person2.sayName(); // wypisuje "Greg"

!!W tym przykładzie metoda sayName() została umieszczona w prototypie , a nie w


konstruktorze. Mimo to obiekty działają dokładnie tak samo jak wcześniej, czyli tak, jakby
metoda należała do instancji. Gdy metoda sayName() jest wywoływana na obiektach
person1 i person2, wartości this są przypisywane referencje do nich, więc metoda działa na
polach odpowiednich instancji.

Właściwości można dodawać do prototypu po jednej na raz, ale czasami wygodniejszym


wyjściem jest zastosowanie literału obiektu i zastąpienie prototypu.

function Person(name) {
this.name = name;
}
Person.prototype = {
sayName: function() {
console.log(this.name);
},
toString: function() {
return "[Person " + this.name + "]";
}
};

W prototypie definiujemy dwie metody: sayName() i toString(). Ten wzorzec jest dosyć
popularny, ponieważ unika się dzięki niemu potrzeby wielokrotnego wpisywania kodu
Person.prototype , a uzyskuje się dużą czytelność kodu.

!!Trzeba jednak wiedzieć o jednym efekcie ubocznym takiego rozwiązania.

var person1 = new Person("Nicholas");

console.log(person1 instanceof Person);// true


console.log(person1.constructor === Person);// false
console.log(person1.constructor === Object);// true

Korzystanie z takiej notacji powoduje zmianę właściwości constructor w taki sposób, że


wskazuje Object, a nie Person. Dzieje się tak dlatego, że właściwość constructor znajduje
się w prototypie, a nie w instancji, więc w chwili tworzenia funkcji jej właściwość prototype
jest tworzona z właściwością constructor wskazującą funkcję. W tym wzorcu obiekt
prototypu jest w całości nadpisywany, co prowadzi do tego, że właściwość constructor
pochodzi z nowo utworzonego ogólnego obiektu przypisanego do Person.prototype .

Aby tego uniknąć, trzeba przywrócić właściwości constructor prawidłową wartość:

function Person(name) {
this.name = name;
}

Person.prototype = {
constructor: Person,

sayName: function() {
console.log(this.name);
},

toString: function() {
return "[Person " + this.name + "]";
}
};

var person1 = new Person("Nicholas");


var person2 = new Person("Greg");

console.log(person1 instanceof Person); // true


console.log(person1.constructor === Person); // true
console.log(person1.constructor === Object); // false

console.log(person2 instanceof Person); // true


console.log(person2.constructor === Person); // true
console.log(person2.constructor === Object); // false

Wartość właściwości constructor zostaje ustawiona w prototypie. Dobrym zwyczajem jest


umieszczanie jej jako pierwszej, by nie zapomnieć o jej dołączeniu.

!! Chyba najciekawszym aspektem powiązania między konstruktorami, prototypami i


instancjami jest to, że nie ma bezpośredniego połączenia między instancją i konstruktorem.
Istnieją za to połączenia między instancją i prototypem oraz między prototypem i
konstruktorem.

Modyfikowanie prototypów

Ponieważ wszystkie instancje określonego typu odwołują się do wspólnego prototypu,


można je wszystkie naraz zmodyfikować.
Właściwość [[Prototype]] zawiera referencję do prototypu, więc wszystkie wprowadzone w
nim zmiany są od razu widoczne w każdej instancji korzystającej z tego prototypu.
Można więc dodać do prototypu nową właściwość, a stanie się ona dostępna we wszystkich
instancjach.

function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,

sayName: function() {
console.log(this.name);
},

toString: function() {
return "[Person " + this.name + "]";
}
};
var person1 = new Person("Nicholas");
var person2 = new Person("Greg");

console.log("sayHi" in person1) ;// false


console.log("sayHi" in person2); // false

// dodajemy nową metodę


Person.prototype.sayHi = function() {
console.log("Cześć");
};

person1.sayHi(); // wypisuje "Cześć"


person2.sayHi(); // wypisuje "Cześć"

Silnik JavaScriptu szuka właściwości za każdym razem, gdy je wywołujemy, więc z punktu
widzenia kodu rozbudowa prototypu odnosi natychmiastowy skutek.

Prototypy wbudowanych obiektów

Wszystkie wbudowane obiekty mają konstruktory, a więc mają również prototypy, które
można modyfikować.

Aby dodać na przykład nową metodę, z której można korzystać we wszystkich tablicach,
należy jedynie zmodyfikować prototyp Array.prototype.

Array.prototype.sum = function() {
return this.reduce(function(previous, current) {
return previous + current;
});
};

var numbers = [ 1, 2, 3, 4, 5, 6 ];
var result = numbers.sum();

console.log(result); // 21

W przykładzie do prototypu Array.prototype dodajemy metodę sum() , która sumuje


wszystkie elementy tablicy i zwraca wynik. Ponieważ jest to metoda prototypu, tablica
numbers ma do niej dostęp. Wewnątrz funkcji sum() obiekt this odnosi się w tym przypadku
do tablicy numbers , która jest instancją typu Array , więc można w niej użyć innych metod
tego typu, na przykład reduce().
Typy proste (łańcuchy znaków, liczby i wartości logiczne) mają swoje typy opakowujące,
dzięki którym można je traktować jak obiekty. Jeśli zmodyfikujemy ich prototypy, możemy
dodać do nich nowe funkcjonalności.

String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.substring(1);
};

var message = "witaj, świecie!";


console.log(message.capitalize()); // "Witaj, świecie!"

Do prototypu typu String dodajemy nową metodę capitalize() . Typ String jest typem
opakowującym typ prosty reprezentujący łańcuch znaków. Zmodyfikowanie jego prototypu
skutkuje tym, że wszystkie łańcuchy znaków zyskają tę metodę.

Podsumowanie
Konstruktory są zwykłymi funkcjami wywoływanymi przez operator new . Własne konstruktory definiuje się
wtedy, gdy chce się tworzyć wiele obiektów o tych samych właściwościach. Aby zidentyfikować obiekt utworzony
za pomocą konstruktora, można użyć operatora instanceof lub właściwości constructor.

Każda funkcja ma właściwość prototype , która wskazuje prototyp definiujący właściwości współdzielone przez
wszystkie obiekty tworzone za pomocą określonego konstruktora. Współdzielone metody i właściwości są
definiowane w prototypie, a pozostałe — w konstruktorze. Właściwość constructor tak naprawdę znajduje się w
prototypie, ponieważ jest współdzielona przez wiele instancji.

Prototyp obiektu jest przechowywany w wewnętrznej właściwości [[Prototype]] . Jest to referencja, a nie kopia.
Jeśli zmodyfikuje się prototyp, zmiana zostanie uwzględniona we wszystkich instancjach, co wynika ze sposobu
działania silnika JavaScriptu. Przy próbie dostępu do właściwości w pierwszej kolejności jest przeszukiwany
obiekt. Jeśli właściwość o danej nazwie nie została znaleziona, przeszukiwany jest prototyp. Dzięki takiemu
mechanizmowi prototyp może być modyfikowany, a wprowadzone w nim zmiany są od razu widoczne w
obiektach zawierających referencję do tego prototypu.

Wbudowane obiekty również posiadają prototypy, które można modyfikować. Nie powinno się jednak tego robić
podczas tworzenia konkretnych aplikacji. Możliwość ta jest za to przydatna podczas eksperymentów i testowania
nowych funkcjonalności.

5. Dziedziczenie
W Java-Scripcie dziedziczenie odbywa się bezpośrednio między obiektami, a więc bez
udziału klas. Mechanizm stojący za tego typu dziedziczeniem to prototypy.

You might also like