Professional Documents
Culture Documents
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:
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 book = {
name: "JavaScript. Zasady programowania obiektowego",
year: 2014
};
Literały funkcji
function reflect(value) {
return value;
}
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.
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:
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:
!!!! 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");
}
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
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"
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.
var person1 = {
name: "Nicholas"
};
person1.age = "ukryty";
person2.age = "ukryty";
person1.name = "Greg";
person2.name = "Michael";
Wykrywanie właściwości
// 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)
var person1 = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
var person1 = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
Usuwanie właściwości
Wyliczenia
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 i, len;
Rodzaje 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;
}
};
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
});
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;
}
};
Object.defineProperties(person1, {
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 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);
};
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);
};
person1.name = "Greg";
console.log(person1.name); // "Greg"
delete person1.name;
console.log("name" in person1); // true
console.log(person1.name); // "Greg"
!!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);
};
person1.name = "Greg";
console.log(person1.name); // "Nicholas"
delete person1.name;
console.log("name" in person1); // true
console.log(person1.name); // "Nicholas"
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
}
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.
!!!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);
};
}
console.log(person1.name);// "Nicholas"
console.log(person2.name); // "Greg"
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).
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"
};
Właściwość [[Prototype]]
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.
Udaje się uniknąć wielokrotnego powtarzania kodu funkcji, ponieważ instancje danego typu
zawierają referencje do tego samego prototypu.
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);
};
console.log(person1.name); // "Nicholas"
console.log(person2.name); // "Greg"
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.
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
},
toString: function() {
return "[Person " + this.name + "]";
}
};
Modyfikowanie prototypów
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");
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.
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
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.substring(1);
};
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.