Autorzy
Ten przewodnik jest dziełem dwóch uroczych użytkowników Stack Overflow, Ivo Wetzel (Treść) oraz Zhang Yi Jiang (Projekt).
Ten przewodnik jest dziełem dwóch uroczych użytkowników Stack Overflow, Ivo Wetzel (Treść) oraz Zhang Yi Jiang (Projekt).
JavaScript Garden znajduje się na serwerach GitHub, ale dzięki wsparciu Cramer Development posiadamy również mirror na serwerze JavaScriptGarden.info.
JavaScript Garden jest publikowany w ramach licencji MIT i kod źródłowy znajduje się na serwerze GitHub. Jeśli znajdziesz jakieś błędy lub literówki, zgłoś proszę problem lub rozwiąż go i zgloś pull request ze swojego repozytorium. Możesz nas także znaleźć w pokoju JavaScript na chacie Stack Overflow.
Wszystko w JavaScripcie zachowuje się jak obiekt, z dwoma wyjątkami
null
oraz undefined
.
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'
function Foo(){}
Foo.bar = 1;
Foo.bar; // 1
Popularnym błędem jest traktowanie literałów liczbowych jak obiektu. Spowodowane jest to specyfiką parsera JavaScript, który interpretuje kropkę po literale liczbowym jako rozdzielenie części całkowitej od części ułamkowej liczby.
2.toString(); // wyrzuca błąd SyntaxError
Istnieje kilka rozwiązań, dzięki którym literał liczbowy będzie zachowywał się jak obiekt.
2..toString(); // druga kropka jest poprawnie rozpoznana
2 .toString(); // zauważ, że pozostawiona jest spacja przed kropką
(2).toString(); // 2 zostanie najpierw zewaluowane
Obiekty w języku JavaScript mogą być używana jako tablice asocjacyjne, ponieważ obiekty składają się głównie z mapowań pomiędzy nazwanymi właściwościami (kluczami) a wartościami dla tych atrybutów.
Używając literału obiektu - notacji {}
- istnieje możliwość stworzenia obiektu prostego.
Ten nowy obiekt będzie dziedziczył z Object.prototype
oraz
nie będzie posiadał żadnych własnych właściwości.
var foo = {}; // nowy, pusty obiekt
// nowy obiekt z właściwością test o wartości 12
var bar = {test: 12};
Właściwości obiektu można uzyskać na dwa sposoby - poprzez notację z kropką lub z nawiasami kwadratowymi.
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // wyrzuca błąd SyntaxError
foo['1234']; // działa, zwraca undefined
Obie notacje są identyczne w swoim działaniu, z tą tylko różnicą, że notacja z nawiasami kwadratowymi pozwala na dynamiczne dodawanie właściwości i nie prowadzi do wyrzucenia błędu podczas odczytu nieistniejącej właściwości.
Jedynym sposobem na faktyczne usunięcie własności z obiektu jest użycie operatora
delete
. Ustawienie własności na undefined
lub null
usunie tylko wartość
związaną z własnością, ale nie usunie to klucza (nazwy własności) z obiektu.
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
Powyższy kod wypisuje dwie linie - bar undefined
i foo null
. Tylko własność baz
została usunięta i dlatego nie została wypisana.
var test = {
'case': 'jestem słowem kluczowym, więc muszę być w cudzysłowie',
delete: 'tak samo jak ja' // wyrzuca błąd SyntaxError
};
Nazwy właściwości obiektu mogą być zarówno zapisane jako tekst (bez cudzysłowów
lub apostrofów) lub jako string (w cudzysłowach lub apostrofach).
Ze względu na kolejne niedociągnięcie w parserze JavaScript,
powyższy kod wyrzuci błąd SyntaxError
dla implementacji JavaScript poniżej ECMAScript 5.
Ten błąd wynika z faktu, że delete
jest słowem kluczowym, dlatego musi zostać
zapisany jako string (z cudzysłowami lub apostrofami), aby zapewnić, że zostanie
to poprawnie zinterpretowane przez starsze silniki języka JavaScript.
JavaScript nie posiada klasycznego modelu dziedziczenia. Zamiast tego dziedziczenie jest realizowane poprzez prototypy.
Choć jest to często uważane za jedną ze słabości języka JavaScript, prototypowy model dziedziczenia, jest w rzeczywistości potężniejszy od klasycznego modelu. Na przykład stworzenia klasycznego modelu na podstawie modelu prototypowego jest dość proste, podczas gdy zrobienie odwrotnego przekształcenie to o wiele trudniejsze zadanie.
Ze względu na fakt, że w JavaScript jest w zasadzie jedynym powszechnie stosowanym językiem, który posiada prototypowy model dziedziczenia, dostosowanie się do różnic pomiędzy tymi dwoma modelami wymaga trochę czasu.
Pierwszą znaczącą różnicą jest to, że dziedziczenie w JavaScript odbywa się za pomocą tak zwanych łańcuchów prototypów.
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Ustawienie prototypu Bar na nową instancję Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// Upewniamy się, że Bar jest ustawiony jako rzeczywisty konstruktor
Bar.prototype.constructor = Bar;
var test = new Bar() // tworzymy nową instancję Bar
// The resulting prototype chain
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World', value: 42 }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* etc. */ }
W powyższym przykładzie obiekt test
będzie dziedziczył z obydwu, tj.
Bar.prototype
i Foo.prototype
, stąd będzie miał dostęp do funkcji method
,
która była zdefiniowana w Foo
. Ponadto obiekt będzie miał dostęp do
właściwości value
, która jest jedyną instancją Foo
i stała się jego prototypem.
Należy pamiętać, że new Bar
nie tworzy nowej instancji Foo
,
tylko wykorzystuje instancję, która jest przypisana do własności prototype
.
Zatem Wszystkie instancje Bar
będą dzieliły tą samą własność value
.
Podczas dostępu do właściwości obiektu JavaScript przejdzie w górę łańcucha prototypów, dopóki nie znajdzie właściwości bez nazwy.
Gdy przeszukiwanie dotrze do końca (szczytu) łańcucha, mianowicie Object.prototype
i nadal nie znajdzie określonej właściwości, to zwróci wartość
undefined.
Podczas gdy właściwość prototype
jest używana przez język do budowania łańcucha
prototypów, istnieje możliwość przypisania do niej dowolnej wartości. Jednakże
prymitywne typy będą po prostu ignorowanie, jeżeli zostaną ustawione jako prototype
.
function Foo() {}
Foo.prototype = 1; // nie ma wpływu
Przypisywanie obiektów, jak pokazano w powyższym przykładzie, zadziała i pozwala na dynamiczne tworzenie łańcuchów prototypów.
Czas wyszukiwania właściwości, które są na końcu łańcucha prototypów może mieć negatywny wpływ na wydajność krytycznych części kodu. Dodatkowo, próba dostępu do nieistniejącej właściwości zawsze spowoduje przeszukanie całego łańcucha prototypów.
Również podczas iteracji po właściwościach obiektu każda właściwość, która znajduje się w łańcuchu prototypów (niezależnie na jakim znajduje się poziomie) zostanie wyliczona.
Rozszerzanie Object.prototype
lub innego prototypu wbudowanych typów jest jednym z
najczęściej nadużywanej częsci języka JavaScript.
Technika ta nazywana jest monkey patching i łamie zasady enkapsulacji. Mimo to jest szeroko rozpowszechniona w frameworkach takich jak Prototype. Nie ma jednak dobrego powodu, aby zaśmiecać wbudowane typy poprzez wzbogacanie ich o niestandardowe funkcjonalności.
Jedynym dobrym powodem do rozszerzania wbudowanych prototypów jest portowanie
funkcjonalności znajdujących się w nowszych silnikach JavaScript, np. Array.forEach
Zanim przystąpi się do pisania skomplikowanego kodu korzystającego z dziedziczenia,
należy całkowicie zrozumieć prototypowy model dziedziczenia. Ponadto trzeba uważać
na długość łańcucha prototypów i w razie potrzeby zmniejszać ilość dziedziczeń,
aby uniknąć problemów z wydajnością. Natywne prototypy nigdy nie powinny być
rozszerzane, chyba że ze względu na wprowadzanie kompatybilności z nowszymi silnikami
JavaScript.
hasOwnProperty
W celu sprawdzenia, czy dana właściwość została zdefiniowana w tym obiekcie, a nie
w łańcuchu prototypów, niezbędne jest skorzystanie z metody
hasOwnProperty
, której wszystkie obiekty dziedziczą z Object.prototype
.
hasOwnProperty
jest jedyną metodą w języku JavaScript, która operuje na właściwościach
i nie przegląda całego łańcucha prototypów.
// Zatrucie Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
Tylko hasOwnProperty
da prawidłowy i oczekiwany rezultat. Jest to istotne podczas
iteracji po właściwościach obiektu. Nie ma innego sposobu na ominięcie
właściwości, która nie została zdefiniowana przez ten konkretny obiekt,
ale gdzieś indziej w łańcuchu prototypów.
hasOwnProperty
jako właściwośćJavaScript nie chroni właściwości o nazwie hasOwnProperty
, zatem istnieje
możliwość, że obiekt będzie posiadać tak nazwaną właściwość. Konieczne jest użycie
zewnętrznego hasOwnProperty
, aby otrzymać poprawne rezultaty.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // zawsze zwraca false
// Została użyta metoda innego obiektu i wywołana z kontekstem
// `this` ustawionym na foo
({}).hasOwnProperty.call(foo, 'bar'); // true
Jedyną metodą służącą do sprawdzenia istnienia jakiejś właściwości w konkretnym
obiekcie jest metoda hasOwnProperty
. Zaleca się korzystać z hasOwnProperty
w
każdej pętli for in
. Pozwoli to uniknąć błędów pochodzących
z rozszerzonych natywnych prototypów.
for in
Podobnie jak operator in
, pętla for in
przeszukuje łańcuch prototypów
podczas iteracji po właściwościach obiektu.
// Zatrucie Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // wyświetla obie właściwości: bar i moo
}
Ponieważ zmiana zachowania pętli for in
nie jest możliwa, niezbędne
jest odfiltrowanie niechcianych właściwości wewnątrz ciała pętli, korzystając
z metody hasOwnProperty
z Object.prototype
.
hasOwnProperty
// foo z przykładu powyżej
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
To jest jedyna poprawna wersja, której należy używać. Ze względu na użycie
hasOwnProperty
zostanie wypisane jedynie moo
. Gdy opuścimy hasOwnProperty
,
kod będzie podatny na błędy, gdy natywne prototypy (np. Object.prototype
)
zostaną rozszerzone.
Prototype jest jednym z popularniejszych frameworków, które dokonują
takiego rozszerzenia. Używanie tego frameworku oraz nie stosowanie w pętli for in
metody hasOwnProperty
gwarantuje błędy w wykonaniu.
Zaleca się, aby zawsze używać metody hasOwnProperty
. Nigdy nie powinno się dokonywać
żadnych założeń na temat środowiska, w którym kod będzie wykonywany ani tego, czy
natywne prototypy zostały rozszerzone, czy nie.
Funkcje w języku JavaScript są typami pierwszoklasowymi, co oznacza, że mogą być przekazywane jak każda inna wartość. Jednym z typowych zastosowań tej cechy jest przekazywanie anonimowej funkcji jako callback do innej, prawdopodobnie asynchronicznej funkcji.
function foo() {}
Powyższa funkcja zostaje wyniesiona zanim program wystartuje. Dzięki temu jest dostępna wszędzie w ramach zasięgu, w którym została zadeklarowana, nawet, jeżeli ta funkcja została wywołana przed faktyczną definicją w kodzie źródłowym.
foo(); // Działa ponieważ definicja funkcji została wyniesiona
// na początek zasięgu przed uruchomieniem kodu
function foo() {}
var foo = function() {};
Ten przykład przypisuje nienazwaną i anonimową funkcję do zmiennej foo
.
foo; // 'undefined'
foo(); // wyrzuca błąd TypeError
var foo = function() {};
Ze względu na fakt, że deklaracja var
wynosi zmienną foo
na początek zasięgu
zanim kod faktycznie zostanie uruchomiony, foo
będzie zdefiniowane kiedy skrypt
będzie wykonywany.
Ale ponieważ przypisania robione są dopiero podczas wykonania, wartość foo
będzie
ustawiona na domyślną wartość undefined zanim powyższy kod
zostanie uruchomiony.
Kolejnym specjalnym przypadkiem jest przypisanie nazwanej funkcji.
var foo = function bar() {
bar(); // Działa
}
bar(); // wyrzuca ReferenceError
W zewnętrznym zakresie bar
nie będzie dostępna, ponieważ funkcja zostaje
przypisana do foo
, jednakże w wewnętrznym zakresie bar
będzie dostępna.
Jest to spowodowane tym, jak działa rozwiązywanie nazw
w języku JavaScript. Nazwa funkcji jest zawsze dostępna w lokalnym
zakresie tej funkcji.
this
JavaScript posiada inną koncepcję odnośnie tego na co wskazuje słowo kluczowe
this
, niż większość innych języków programowania. Istnieje dokładnie
pięć różnych sytuacji, w których wartość this
jest przypisana w języku JavaScript.
this;
Używanie this
w globalnym zasięgu, zwróci po prostu referencję do obiektu global.
foo();
Tutaj this
również będzie wskazywało na obiekt global
test.foo();
W tym przypadku this
będzie wskazywało na test
.
new foo();
Wywołanie funkcji, które jest poprzedzone słowem kluczowym new
, zachowuje się
jak konstruktor. Wewnątrz funkcji this
będzie
wskazywało na nowo utworzony obiekt.
this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // tablica zostanie zamieniona w to co poniżej
foo.call(bar, 1, 2, 3); // rezultat a = 1, b = 2, c = 3
Używając metod call
lub apply
z prototypu Function.prototype
, wartość this
wewnątrz wołanej funkcji zostanie jawnie ustawiona na pierwszy argument przekazany
podczas wywołania tych metod.
Zatem w powyższym przykładzie przypadek Wywoływanie metody nie będzie miał
miejsca i this
wewnątrz foo
będzie wskazywać na bar
.
Mimo iż Większość z tych przypadków ma sens, to pierwszy przypadek powinien być traktowany jako błąd podczas projektowania języka i nigdy nie wykorzystywany w praktyce.
Foo.method = function() {
function test() {
// wewnątrz tej funkcji this wskazuje na obiekt global
}
test();
};
Powszechnym błędem jest myślenie, że this
wewnątrz test
wskazuje na Foo
,
podczas gdy w rzeczywistości tak nie jest.
Aby uzyskać dostęp do Foo
wewnątrz test
, niezbędne jest stworzenie wewnątrz
metody lokalnej zmiennej, która będzie wskazywała na Foo
.
Foo.method = function() {
var that = this;
function test() {
// Należy używać that zamiast this wewnątrz tej funkcji
}
test();
};
that
jest zwykłą zmienną, ale jest to powszechnie stosowana konwencja otrzymywania
wartości zewnętrznego this
. W połączeniu z domknięciami(closures),
jest to sposób na przekazywanie wartości this
wokół.
Kolejną rzeczą, która nie działa w języku JavaScript, jest nadawanie aliasów funkcjom, co oznacza przypisanie metody do zmiennej.
var test = someObject.methodTest;
test();
Podobnie jak w pierwszym przypadku test
zachowuje się jak wywołanie zwykłej
funkcji, a zatem wewnątrz funkcji this
już nie będzie wskazywało someObject
.
Podczas gdy późne wiązanie this
może się na początku wydawać złym pomysłem,
to w rzeczywistości jest to rzecz, która sprawia, że
dziedziczenie prototypowe działa.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
Kiedy metoda method
zostanie wywołana na instancji Bar
, this
będzie
wskazywało właśnie tę instancję.
Jedną z najpotężniejszych funkcjonalności języka JavaScript są domknięcia. Oznacza to że zasięg zawsze posiada dostęp do zewnętrznego zasięgu, w którym został zdefiniowany. Ponieważ zasięg w JavaScript można definiować tylko poprzez funkcję, wszystkie funkcje domyślnie zachowują się jak domknięcia.
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
Tutaj Counter
zwraca dwa domknięcia: funkcję increment
oraz funkcję get
.
Obie te funkcje trzymają referencję do zasięgu Counter
, a co za tym idzie
zawsze posiadają dostęp do zmiennej count
tak, jakby ta zmienna była zdefiniowana
w zasięgu tych funkcji.
Ponieważ nie ma możliwości wskazania lub przypisania zasięgu w JavaScript,
nie istnieje sposób, aby uzyskać dostęp do zmiennej count
z zewnątrz.
Wykorzystanie tych dwóch domknięć jest jedynym sposobem na interakcję z tą zmienną.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
Powyższy kod nie zmieni wartości zmiennej count
wewnątrz zasięgu Counter
,
ponieważ foo.hack
nie została zadeklarowana wewnątrz tego konkretnego zasięgu.
Zamiast tego funkcja utworzy lub nadpisze globalną zmienną count
.
Jednym z częstszych błędów jest wykorzystywanie domknięć wewnątrz pętli, aby wartość zmiennej po której odbywa się iteracja była kopiowana do wewnętrznej funkcji.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Powyższy kod nie wypisze numerów od 0
do 9
, ale wypisze
dziesięć razy liczbę 10
.
Anonimowa funkcja trzyma wskaźnik do zmiennej i
i podczas uruchomienia
console.log
, pętla for
już zakończyła działanie i wartość zmiennej i
została ustawiona na 10
.
Aby otrzymać zamierzony efekt, niezbędne jest skopiowanie wartości
zmiennej i
.
Aby skopiować wartość zmiennej, po której iterujemy w pętli, należy skorzystać z anonimowego wrappera.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Zewnętrzna anonimowa funkcja zostanie wywołana od razu z parametrem i
jako pierwszym argumentem oraz otrzyma kopię wartości zmiennej i
jako
zmienną e
.
Anonimowa funkcja która zostaje przekazana do setTimeout
teraz posiada
referencję do zmiennej e
, która nie zostanie zmieniona przez pętle for
.
Istnieje jeszcze jeden sposób na osiągnięcie tego samego efektu. Należy zwrócic fukcję z anonimowego wrappera, wówczas kod będzie zachowywał się jak ten wcześniejszy.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
arguments
Każdy zasięg funkcyjny w języku JavaScript ma dostęp do specjalnej zmiennej arguments
.
Ta zmienna trzyma listę wszystkich argumentów przekazanych do funkcji.
Obiekt arguments
nie jest typu Array
. Mimo że posiada pewne cechy
semantyki tablic - właściwość length
- to w rzeczywistości nie dziedziczy
on z Array.prototype
, tylko z Object
.
Ze względu na to, na obiekcie arguments
nie można używać standardowych dla tablic metod,
takich jak push
, pop
czy slice
. Mimo że iteracja przy pomocy
pętli for
działa dobrze, to aby skorzystać ze standardowych metod tablicowych
należy skonwertować arguments
do prawdziwego obiekt Array
.
Poniższy kod zwróci nowy obiekt Array
zawierający wszystkie elementy
obiektu arguments
.
Array.prototype.slice.call(arguments);
Jednakże konwersja ta jest wolna i nie jest zalecana w sekcjach, które mają duży wpływ na wydajność.
Zalecany sposób przekazywania argumentów z jednej funkcji do następnej wyglada następująco:
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// do stuff here
}
Kolejną sztuczką jest użycie razem call
i apply
w celu stworzenia
szybkich i nieograniczonych wrapperów.
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Stworzenie nieograniczoną wersję metody "method"
// która przyjmuje parametry: this, arg1, arg2...argN
Foo.method = function() {
// Rezultat: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Obiekt arguments
tworzy funkcje getter i setter nie tylko dla swoich
właściwości, ale również dla parametrów formalnych funkcji.
W rezultacie zmiana wartości parametru formalnego zmieni również wartość
odpowiadającemu mu wpisowi w obiekcie arguments
. Zachodzi to również w drugą stronę.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Obiekt arguments
jest tworzony zawsze, z wyjątkiem dwóch przypadków, gdy
zmienna o takiej nazwie jest zdefiniowana wewnątrz funkcji lub jeden z parametrów
formalnych funkcji ma taką nazwę. Nie ma znaczenia czy obiekt arguments
jest
używany czy nie.
Zarówno gettery jak i settery są zawsze tworzone, zatem używanie ich nie ma
praktycznie żadnego wpływu na wydajność. Zwłaszcza w rzeczywistym kodzie, który
wykorzystuje coś więcej niż tylko prosty dostęp do właściwości obiektu arguments
.
Jednakże, istnieje jeden przypadek w którym wydajność drastycznie spada w
nowoczesnych silnikach JavaScript. Ten przypadek to wykorzystanie
arguments.callee
.
function foo() {
arguments.callee; // operowanie na obiekcie funkcji
arguments.callee.caller; // i obiekcie funkcji wywołującej
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Normalnie zostałaby wykorzystana metoda inline
}
}
W powyższym przykładzie foo
nie może zostać wykorzystana metoda inline
ponieważ potrzebne są nie tylko informacje na własny temat ale również
na temat funkcji wywołującej. Takie użycie nie tylko uniemożliwia
inlining i korzyści z niego wynikające, ale też łamie zasady enkapsulacji,
ponieważ ta funkcja jest zależna od kontekstu w jakim została wywołana.
Mocno zalecane jest aby nigdy nie korzystać z arguments.callee
i żadnej jej własności.
Konstruktory w JavaScript również wyglądają inaczej niż innych językach. Każde
wywołanie funkcji, które jest poprzedone słowem kluczowym new
, zachowuje się
jak konstruktor.
Wewnątrz konstruktora - wywoływanej fukcji - wartość this
wskazuje na
nowo utworzony obiekt Object
. Prototyp prototype
tego
nowego obiektu będzie wskazywał na prototyp prototype
obiektu fukcji,
która została wywołana jako konstruktor.
Jeżeli wywołana funkcja nie posiada jawnej deklaracji return
, wówczas
fukcja domyślnie zwraca wartość this
- nowy obiekt.
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
Powyżej wywołana została funkcja Foo
jako konstruktor oraz ustawia
nowo utworzonemu obiektowi właściwość prototype
na Foo.prototype
.
W tym przypadku jawna deklaracja return
w funkcji zwraca wartość
ustawioną w deklaracji, ale tylko jeżeli zwracaną wartością jest
obiekt Object
.
function Bar() {
return 2;
}
new Bar(); // nowy obiekt
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // zwrócony obiekt
Jeżeli słowo kluczowe new
zostanie pominięte, funkcja nie zwróci nowego
obiektu.
function Foo() {
this.bla = 1; // zostanie ustawiona w obiekcie global
}
Foo(); // undefined
Mimo że powyższy kod może zadziałać w pewnych przypadkach, w związku
z działaniem this
w języku JavaScript, to jako
wartość this
zostanie wykorzystany obiekt global.
Aby móc ominąć słowo kluczowe new
, konstruktor musi jawnie zwracać wartość.
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
Oba wywołania Bar
zwrócą tę samą rzecz, nowo utworzony obiekt, który posiada
właściwość nazwaną method
i dla którego Bar
jest Domknięciem.
Należy również pamiętać, że wywołanie new Bar()
nie ma wpływu na
prototyp zwróconego obiektu (prototypem będzie object.prototype
a nie Bar.prototype
).
Kiedy prototyp zostanie przypisany do nowo utworzonego obiektu, Bar
nidgy
nie zwróci tego nowego obiektu Bar
, tylko literał obiektu, który jest po
słowie kluczowym return
.
W powyższym przykładzie nie ma żadnej różnicy w działaniu pomiędzy użyciem
i nieużyciem słowa kluczowego new
.
Często zaleca się nie korzystać z operatora new
, ponieważ zapominanie
o jego stosowaniu może prowadzić do błędów.
W celu stworzenia nowego obiektu, powinno się używać fabryki i konstruować nowy obiekt wewnątrz tej fabryki.
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.someMethod = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
Mimo że powyższy kod jest odporny na brak słowa kluczowego new
i ułatwia
korzystanie ze zmiennych prywatnych, to posiada
pewne wady.
new
jest sprzeczne z duchem języka.Pominięcie słowa kluczowego new
może prowadzić do błędów, ale na pewno nie
powinno to być powodem odrzucenia używania prototypów w ogóle. Sprowadza się to
do wyboru rozwiązania, które bardziej pasuje do potrzeb aplikacji. Szczególnie
ważne jest, aby wybrać określony styl tworzenia obiektów i trzymać się go.
Mimo że JavaScript radzi sobie dobrze ze składnią opisującą dwa pasujące nawiasy klamrowe jako blok, to jednak nie wspiera zasięgu blokowego. Jedynym zasięgiem jaki istnieje w JavaScript jest zasięg funkcyjny.
function test() { // definiuje zasięg (scope)
for(var i = 0; i < 10; i++) { // nie definiuje zasięgu (scope)
// count
}
console.log(i); // 10
}
W JavaScripcie nie ma również przestrzeni nazw, co oznacza, że wszystko jest definiowane w jednej globalnie współdzielonej przestrzeni nazw.
Z każdym odwołaniem do zmiennej, JavaScript przeszukuje w górę wszystkie zasięgi
dopóki nie znajdzie tej zmiennej. W przypadku, gdy przeszukiwanie dotrze do globalnego
zasięgu i nadal nie znajdzie żądanej nazwy, wyrzuca błąd ReferenceError
.
// script A
foo = '42';
// script B
var foo = '42'
Powyższe dwa skrypty nie dają tego samego efektu. Skrypt A definiuje zmienną
nazwaną foo
w globalnym zasięgu, natomiast skrypt B definiuje foo
w aktualnym zasięgu.
Jeszcze raz, to wcale nie daje tego samego efektu. Brak użycia var
może mieć
poważne konsekwencje.
// globalny zasięg
var foo = 42;
function test() {
// lokalny zasięg
foo = 21;
}
test();
foo; // 21
Pominięcie słowa var
w deklaracji wewnątrz funkcji test
nadpisze wartość
zmiennej globalnej foo
. Mimo, że nie wygląda to na początku na duży problem,
w przypadku kodu, który posiada wielu tysięcy linii, brak var
wprowadzi straszne i trudne do wyśledzenia błędy.
// globalny zasięg
var items = [/* jakaś lista */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// scope of subLoop
for(i = 0; i < 10; i++) { // brakuje słowa var w deklaracji
// do amazing stuff!
}
}
Zewnętrzna pętla zakończy działanie po pierwszym wywołaniu subLoop
, ponieważ
subLoop
nadpisuje wartość globalnej zmiennej i
. Użycie var
w drugiej pętli
for
pozwoliłoby łatwo uniknąć problemu. Słowo kluczowe var
nie powinno być
nigdy pominięte w deklaracji, chyba że pożądanym skutkiem jest modyfikacja
zewnętrznego zasięgu.
Jedynym źródłem zmiennych lokalnych w JavaScripcie są parametry funkcji
oraz zmienne zadeklarowane poprzez deklaracje var
wewnątrz funkcji.
// globalny zasięg
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// lokalny zasięg fukcji test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
Zmienne foo
oraz i
są lokalnymi zmiennymi wewnątrz zasięgu funkcji test
,
natomiast przypisanie wartości do bar
nadpisze zmienną globalną o tej samej nazwie.
JavaScript winduje deklaracje. Oznacza to, że zarówno deklaracja ze słowem
kluczowym var
jak i deklaracje funkcji function
zostaną przeniesione na
początek otaczającego zasięgu.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
Powyższy kod zostanie przekształcony przed rozpoczęciem wykonania. JavaScript
przeniesie deklarację zmiennej var
oraz deklarację funkcji function
na szczyt
najbliższego zasięgu.
// deklaracje var zostaną przeniesione tutaj
var bar, someValue; // ustawione domyślnie na 'undefined'
// deklaracje funkcji zostaną również przeniesione na górę
function test(data) {
var goo, i, e; // brak blokowego zasięgu spowoduje przeniesienie tutaj
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // powoduje błąd TypeError ponieważ bar jest nadal 'undefined'
someValue = 42; // przypisania nie zostają zmienione przez 'hoisting'
bar = function() {};
test();
Brak blokowego zasięgu nie tylko przeniesie deklaracje var
poza ciało pętli,
ale również spowoduje, że niektóre porównania if
staną się nieintuicyjne.
W oryginalnym kodzie instrukcja warunkowa if
zdaje się modyfikować zmienną
globalną goo
, podczas gdy faktycznie modyfikuje ona zmienną lokalną - po tym
jak zostało zastosowane windowanie (hoisting).
Analizując poniższy kod bez wiedzy na temat hoistingu możemy odnieść wrażenie,
że zobaczymy błąd ReferenceError
.
// sprawdź, czy SomeImportantThing zostało zainicjalizowane
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Oczywiście powyższy kod działa ze względu na fakt, że deklaracja var
zostanie
przeniesiona na początek globalnego zasięgu.
var SomeImportantThing;
// inny kod, który może, ale nie musi zainicjalizować SomeImportantThing
// upewnij się, że SomeImportantThing zostało zainicjalizowane
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Wszystkie zasięgi w JavaScripcie, włączając globalny zasięg, posiadają
zdefiniowaną wewnątrz specjalną nazwę this
, która wskazuje
na aktualny obiekt.
Zasięg funkcyjny posiada również zdefiniowaną wewnętrznie nazwę
arguments
, która zawiera listę argumentów przekazaną do
funkcji.
Na przykład, kiedy próbujemy odczytać zmienną foo
wewnątrz zasięgu funkcji,
JavaScript będzie szukać nazwy w określonej kolejności:
var foo
, skorzystaj z niej.foo
, użyj go.foo
, skorzystaj z tego.Powszechnym problemem posiadania tylko jednej globalnej przestrzeni nazw jest prawdopodobieństwo wystąpienia kolizji nazw. W JavaScripcie, można łatwo uniknąć tego problemu korzystając z anonimowych wrapperów (inaczej: Immediately-Invoked Function Expression - IIFE).
(function() {
// autonomiczna "przestrzeń nazw"
window.foo = function() {
// wyeksponowane domkniecie (closure)
};
})(); // natychmiastowe wykonanie funkcji
Anonimowe funkcje są rozpoznane jako wyrażenia, więc aby mogły zostać wywołane muszą zostać zewaluowane.
( // zewaluowanie funkcji znajdującej się wewnątrz nawiasów
function() {}
) // zwrócenie obiektu funkcji
() // wywołanie rezultatu ewaluacji
Istnieją inne sposoby aby zewaluować i wykonać wyrażenie funkcyjne. Mimo że mają inną składnię, zachowują się dokładnie tak samo.
// Trzy inne sposoby
!function(){}();
+function(){}();
(function(){}());
Zaleca się, aby zawsze używać anonimowych wrapperów do hermetyzacji kodu wewnątrz jego własnej przestrzeni nazw. To nie tylko chroni kod przed kolizją nazw, ale również wprowadza lepszą modularyzację programów.
Ponadto, stosowanie zmiennych globalnych jest uznawane za złą praktykę. Wykorzystanie zmiennych globalnych wskazuje na źle napisany kod, który jest podatny na błędy i trudny do utrzymania.
Mimo, że tablice w JavaScript są obiektami, nie ma dobrych powodów, aby używać
pętli for in
do iteracji po nich. W rzeczywstości istnieje
wiele dobrych powodów przeciwko wykorzystaniu for in
na tablicach.
Ponieważ pętla for in
wylicza wszystkie właściwości, które są wewnątrz
łańcucha prototypów, jedynym sposobem, aby wykluczyć te właściwości, jest użycie
hasOwnProperty
. Wówczas pętla staje się jednak
dwadzieścia razy wolniejsza od normalnej pętli for
.
W celu osiągnięcia najlepszej wydajności podczas iteracji po tablicach należy
użyć klasycznej pętli for
.
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
W powyższym przykładzie jest jeszcze jeden dodatkowy haczyk. Jest to zbuforowanie
długości tablicy poprzez l = list.length
.
Mimo, że właściwość length
jest zdefiniowana wewnątrz tablicy, istnieje nadal
dodatkowy koszt wyszukiwania tej właściwości przy każdej iteracji w pętli.
Najnowsze silniki JavaScript mogą zastosować w tym
przypadku optymalizację. Nie ma jednak możliwości ustalenia, czy kod będzie wykonywany w jednym
z tych nowych silników, czy też nie.
W rzeczywistości pominięcie buforowania długości tablicy może spowodować, że pętla będzie tylko w połowie tak szybka jak ta z buforowaniem długości.
length
Mimo, że getter właściwości length
zwraca po prostu liczbę elementów, które są
zawarte w tablicy, to setter może być użyty do skracania tablicy.
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]
foo.length = 6;
foo; // [1, 2, 3]
Przypisanie mniejszej długości spowoduje skrócenie tablicy, ale zwiększenie wartości
length
nie ma żadnego wpływu na tablicę.
Aby uzyskać najlepszą wydajność zaleca się, aby zawsze używać zwykłej pętli for
i zbuforowanie właściwości length
. Korzystanie z pętli for in
na tablicy jest
oznaką źle napisanego kodu, który jest podatny na błędy i ma słabą wydajność.
Array
Zaleca się zawsze korzystać z literału tablicy - notacja []
- podczas tworzenia
nowych tablic, ponieważ konstruktor Array
niejednoznacznie interpretuje
przekazane do niego parametry.
[1, 2, 3]; // Rezultat: [1, 2, 3]
new Array(1, 2, 3); // Rezultat: [1, 2, 3]
[3]; // Rezultat: [3]
new Array(3); // Rezultat: []
new Array('3') // Rezultat: ['3']
W przypadku, gdy tylko jeden argument zostanie przekazany do kostruktora Array
i
ten argument jest typu Number
, konstruktor zwróci nową dziwną tablicę
z właściwością length
ustawioną na wartość przekazaną jako argument. Należy
zauważyć, że tylko właściwość length
zostanie ustawiona w ten sposób.
Rzeczywiste indeksy w tej tablicy nie zostaną zainicjalizowane.
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // zwraca false, indeks nie został ustawiony
Możliwość ustalenia z góry długości tablicy jest użyteczna tylko w kilku
przypadkach, jak np. powtarzanie ciągu znaków, w którym unika się stosowania
pętli for
.
// count - ilosc powtorzen
// stringToRepeat - ciąg znaków do powtórzenia
new Array(count + 1).join(stringToRepeat);
W miarę możliwości należy unikać używania konstruktora Array
. Literały są
zdecydowanie lepszym rozwiązaniem. Są krótsze i mają bardziej precyzyjną składnię.
Zwiększają również czytelność kodu.
JavaScript posiada dwa różne sposoby równościowego porównywania obiektów.
Operator równości składa się z dwóch znaków "równa się": ==
JavaScript jest słabo typowanym językiem. Oznacza to, że operator równości konwertuje typy (dokonuje koercji), aby wykonać porównanie.
"" == "0" // false
0 == "" // true
0 == "0" // true
false == "false" // false
false == "0" // true
false == undefined // false
false == null // false
null == undefined // true
" \t\r\n" == 0 // true
Powyższa tabela przedstawia wyniki koercji typów. Nieprzewidywalne wyniki
porównania są głównym powodem, że stosowanie ==
jest powszechnie uważane za złą
praktykę. Skomplikowane reguły konwersji są powodem trudnych do wyśledzenia błędów.
Ponadto koercja ma również wpływ na wydajność, Na przykład gdy typ String musi zostać przekształcony na typ Number przed porównaniem z drugą liczbą.
Operator ścisłej równości składa się z trzech znaków "równa się": ===
Działa on dokładnie tak jak normalny operator równości, z jednym wyjątkiem - nie dokonuje koercji typów przed porównaniem.
"" === "0" // false
0 === "" // false
0 === "0" // false
false === "false" // false
false === "0" // false
false === undefined // false
false === null // false
null === undefined // false
" \t\r\n" === 0 // false
Powyższe rezultaty są o wiele bardziej przejrzyste. Powoduje to "ustatycznienie" języka do pewnego stopnia oraz pozwala na wprowadzenie optymalizacji porównań obiektów o różnych typach.
Mimo że oba operatory ==
i ===
nazywane są operatorami równościowymi,
to zachowują się różnie, gdy jednym z operandów jest obiekt typu Object
.
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
Oba operatory porównują tożsamość a nie równość, czyli będą porównywać czy
jeden i drugi operand jest tą samą instancją obiektu (podobnie jak operator
is
w Pythonie i porównanie wskaźników w C).
Zaleca się, aby używać tylko operatora ścisłej równości. W sytuacjach gdy potrzebna jest koercja (porównanie obiektów różnych typów), konwersja powinna być dokonana jawnie, a nie pozostawiona trudnym regułom koercji obowiązującym w języku.
typeof
Operator typeof
(razem z operatorem instanceof
) jest
prawdopodobnie najwiekszą wadą konstrukcji języka JavaScript. Posiada on praktycznie
same wady.
Mimo że instanceof
ma swoje wady to nadal ma ograniczone zastosowanie w praktyce,
natomiast typeof
ma tylko jeden praktyczny przypadek użycia, który na dodatek
nie jest związany z sprawdzaniem typu obiektu.
Wartość Klasa Typ
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function w Nitro i V8)
new RegExp("meow") RegExp object (function w Nitro i V8)
{} Object object
new Object() Object object
W powyższej tabeli Typ odnosi się do wartości zwracanej przez operator typeof
.
Wyraźnie widać, że zwracane wartości w ogóle nie są spójne.
Klasa odnosi się do wartości wewnętrznej właściwości [[Class]]
obiektu.
W celu uzyskania wartości właściwości [[Class]]
trzeba skorzystać z metody
toString
z Object.prototype
.
Specyfikacja zawiera dokładnie jeden sposób dostepu do wartości [[Class]]
,
wykorzystując Object.prototype.toString
.
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
is('String', 'test'); // true
is('String', new String('test')); // true
Powyższy przykład wywołuje Object.prototype.toString
z wartością
this ustawioną na obiekt, dla której wartość właściwości
[[Class]]
ma zostać odczytana.
typeof foo !== 'undefined'
Powyższy kod sprawdza czy foo
została faktycznie zadeklarowana czy też nie.
Próba odwołania się do zmiennej spowodowała by wyrzucenie błędu ReferenceError
.
Jest to jedyne praktyczne wykorzystanie operatora typeof
.
W celu sprawdzenia typu obiektu zalecane jest skorzystanie z
Object.prototype.toString
, ponieważ jest to jedyny wiarygodny sposób. Jak
pokazano w powyższej tabeli typów, niektóre wartości zwracane przez typeof
nie
są zdefiniowane w specyfikacji, co za tym idzie mogą się różnić w różnych
implementacjach.
O ile nie operator typeof
nie jest użyty do sprawdzania czy zmienna została
zdefiniowana, powinien być unikany jeśli to tylko możliwe.
instanceof
Operator instanceof
porównuje konstruktory obiektów przekazanych jako operendy.
Jest on użyteczny jedynie do porównywania obiektów utworzonych klas. Stosowanie
go na wbudowanych typach jest praktycznie tak samo bezużyteczne, jak operatora
typeof.
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// poniżej kod który przypisuje do Bar.prototype obiekt funkcji Foo
// a nie faktyczną instancję Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
instanceof
na natywnych typachnew String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
Jedną ważną rzeczą, którą należy zauważyć jest to, że instanceof
nie zadziała
na obiektach, które pochodzą z różnych kontekstów JavaScript (np. z różnych
dokumentów wewnątrz przeglądarki), ponieważ ich konstruktory nie będą tymi
samymi obiektami.
Operator instanceof
powinien być używany wyłącznie podczas korzystania z obiektów
klas utworzonych, które były zdefiniowane w tym samym kontekscie JavaScriptowym.
Podobnie jak operator typeof
, należy unikać korzystania
z tego operatora w innych sytuacjach.
JavaScript jest językiem słabo typowanym. Co za tym idzie, będzie stosować koercję typów gdziekolwiek jest to możliwe.
// te zwracają true
new Number(10) == 10; // Number.toString() zostanie przekształcone
// z powrotem do liczby
10 == '10'; // stringi zostaną przekształcone do typu Number
10 == '+10 '; // kolejne wariacje
10 == '010'; // i następne
isNaN(null) == false; // null zostanie przekształcony do 0
// który oczywiście nie jest NaN
// poniższe zwracają false
10 == 010;
10 == '-10';
Aby uniknąć powyższych problemów, należy koniecznie korzystać ze ściełego operatora równości. Mimo, że pozwala to uniknąć wiele typowych problemów to nadal istnieje wiele innych, które powstają na bazie słabego typowania języka JavaScript.
Konstruktory typów wbudowanych, takich jak Number
lub String
, zachowują się
inaczej kiedy są poprzedzone słowem kluczowym new
a inaczej kiedy nie są.
new Number(10) === 10; // False, Object i Number
Number(10) === 10; // True, Number i Number
new Number(10) + 0 === 10; // True, ponieważ dokonano jawnej konwersji
Korzystanie z wbudowanych typów jak Number
jako konstruktora tworzy nowy obiekt
typu Number
, natomiast opuszczenie słowa kluczowego new
powoduje, że funkcja
Number
zachowuje się jak konwerter.
Ponadto, użycie literałów lub wartości nieobiektowych zaowocuje jeszcze większą ilością rzutowań (koercją) typów.
Najlepszym rozwiązaniem jest jawne rzutowanie do jednego z trzech typów.
'' + 10 === '10'; // true
Konkatenacja pustego stringu i wartości powoduje rzutowanie do typu String.
+'10' === 10; // true
Zastosowanie unarnego operatora + spowoduje rzutowanie do typu Number.
Używając dwukrotnie operatora negacji, dowolna wartość może zostać zrzutowana do typu Boolean
!!'foo'; // true
!!''; // false
!!'0'; // true
!!'1'; // true
!!'-1' // true
!!{}; // true
!!true; // true
eval
?Funkcja eval
uruchomi podany string jako kod JavaScript w lokalnym zasięgu (scopie).
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
Niestaty, eval
zostanie wykonana w lokalnym zasięgu tylko wtedy, gdy zostanie wywołana
bezpośrednio i nazwa wywoływanej funkcji równa się eval
.
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
Należy unikać stosowania eval
o ile to tylko możliwe. W 99.9% przypadków można
osiągnąć ten sam efekt nie używając eval
.
eval
w przebraniuFunkcje wykonywane po upływie czasu setTimeout
i setInterval
mogą przyjąć string jako pierwszy argument. String ten zawsze będzie wykonywany
w globalnym zasięgu, ponieważ funkcja eval
jest w tym wypadku wywoływana pośrednio.
Funkcja eval
jest również problematyczna od strony bezpieczeństwa, ponieważ
wykonuje każdy kod, który zostanie do niej przekazany i nigdy nie należy
jej używać na stringach nieznanego lub niezaufanego pochodzenia.
Funkcja eval
nie powinna być w ogóle używana. Każdy kod, który jej używa
powinien zostać sprawdzony pod względem działania, wydajności i bezpieczeństwa.
W przypadku gdy użycie eval
jest niezbędne do działania, wówczas taki kod
należy ponownie przemyśleć i ulepszyć aby nie wymagał użycia eval
.
undefined
i null
JavaScript ma dwie różne wartości dla pustych
wartości, bardziej użyteczną
z tych dwóch jest undefined
.
undefined
undefined
jest typem z dokładnie jedną wartością: undefined
.
Język również definiuje globalną zmienną, która ma wartość undefined
- zmienna
ta jest nazwana undefined
. Jednakże jest to zmienna a nie stała, czy słowo
kluczowe. Oznacza to, że możliwe jest nadpisanie wartości tej zmiennej.
Kilka przykładów kiedy wartość undefined
jest zwracana:
undefined
,return
,return
, która nic jawnie nie zwraca,undefined
.undefined
Ponieważ globalna zmienna undefined
zawiera tylko kopię prawdziwej wartości typu
undefined
, przypisanie nowej wartości do tej zmiennej nie zmienia wartości
typu undefined
.
Jednak aby porównać coś z wartością undefined
, trzeba odczytać wartość undefined
.
Aby uchronić swój kod przed możliwym nadpisaniem zmiennej undefined
, korzysta
się z powszechnej techniki dodania dodatkowego parametru do
anonimowego wrappera, do którego nie zostanie przekazany
argument.
var undefined = 123;
(function(something, foo, undefined) {
// undefined o lokalnym zasięgu znowu
// odnosi się do poprawnej wartości
})('Hello World', 42);
Kolejnym sposobem na osiągnięcie tego samego efektu jest użycie deklaracji zmiennej wewnątrz wrappera.
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
Jedyną różnicą pomiędzy tymi sposobami są dodatkowe 4 bajty przeznaczone na słowo
kluczowe var
i spację po nim.
null
Podczas gdy undefined
w kontekście języka jest używany jak null w sensie
tradycyjnych języków, null
w JavaScript (jako literał i jako typ) jest po
prostu kolejnym typem danych.
Jest wykorzystywany we wnętrzu JavaScript (np. deklaracji końca łańcucha prototypów
poprzez ustawienie Foo.prototype = null
), ale prawie w każdym przypadku można go
zastąpić przez undefined
.
Mimo że JavaScript ma składnię podobną do języka C, to nie wymusza stosowania średników w kodzie źródłowym. Istnieje możliwość ich pominięcia.
JavaScript nie jest językiem bez średników, tak na prawdę potrzebuje średników aby zinterpretować kod źródłowy. Jednakże parser JavaScript automatycznie wstawia średniki o ile napotka błąd parsowania związany z brakiem średnika.
var foo = function() {
} // błąd parsowania, oczekiwany był w tym miejscu średnik
test()
Parser dodaje średnik, i próbuje jeszcze raz sparsować skrypt.
var foo = function() {
}; // bez błędu parser kontynuuje
test()
Automatyczne wstawianie średników jest uważane za jeden z największych błędów konstrukcji języka, ponieważ może ono zmienić zachowanie kodu.
Kod poniżej nie ma żadnych średników, więc parser zdecyduje, w których miejscach je wstawi.
(function(window, undefined) {
function test(options) {
log('testing!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'long string to pass here',
'and another long string to pass'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
Poniżej znajduje się rezultat "zgadywania" parsera.
(function(window, undefined) {
function test(options) {
// Nie wstaniony średnik, linie zostały połączone
log('testing!')(options.list || []).forEach(function(i) {
}); // <- wstawiony
options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- wstawiony
return; // <- wstawiony, psując deklarację return
{ // potraktowane jako definicja bloku
// etykieta oraz pojedyncze wyrażenie
foo: function() {}
}; // <- wstawiony
}
window.test = test; // <- wstawiony
// Kolejna połączona linia
})(window)(function(window) {
window.someLibrary = {}; // <- wstawiony
})(window); //<- wstawiony
Parser drastycznie zmienił działanie powyższego kodu. W niektórych przypadkach zmienił go źle.
W przypadku, gdy w następnej linii znajduje się nawias, parser nie wstawi średnika.
log('testing!')
(options.list || []).forEach(function(i) {})
Kod ten zostanie zmieniony w poniższą linię.
log('testing!')(options.list || []).forEach(function(i) {})
Jest bardzo prawdopodobne, że log
nie zwróci fukcji. Co za tym idzie
powyższy kod wyrzuci błąd TypeError
oznajmując, że undefined is not a
function
- undefined
nie jest funkcją.
Zaleca się, aby nigdy nie pomijać średników, pozostawiać nawias otwierający
w tej samej linii co odpowiadająca mu definicja i nigdy nie pozostawiać deklaracji
if
/ else
bez nawiasów - nawet, jeżeli są jednolinijkowe. Wszystkie te uwagi nie
tylko pomagają poprawić spójność kodu, ale też zapobiegają zmianie działania
kodu przez parser JavaScript.
setTimeout
i setInterval
Ponieważ JavaScript jest asynchroniczny, istnieje możliwość zaplanowania wykonania
funkcji przy użyciu funkcji setTimeout
i setInterval
.
function foo() {}
var id = setTimeout(foo, 1000); // zwraca liczbę typu Number > 0
Powyższe wywołanie setTimeout
zwraca ID budzika i planuje wywołanie foo
za
około tysiąc milisekund. foo
zostanie wykonana dokładnie jeden raz.
Nie ma pewności, że kod zaplanowany do wykonania wykona się dokładnie po
upłynięciu zadanego czasu podanego jako parametr do setTimeout
, ponieważ zależy
to od dokładności zegara w silniku JavaScript, który wykonuje kod oraz od tego,
że inny kawałek kodu może zablokować wątek, ponieważ JavaScript jest tylko
jednowątkowy.
Funkcja, która została przekazana jako pierwszy parametr zostanie wykonana w
globalnym zasięgu, co oznacza, że this
wewnątrz tej funkcji
będzie wskazywać na obiekt global.
function Foo() {
this.value = 42;
this.method = function() {
// this wskazuje na obiekt global
console.log(this.value); // wypisze undefined
};
setTimeout(this.method, 500);
}
new Foo();
setInterval
Podczas gdy setTimeout
wywołuje podaną funkcję tylko raz, setInterval
-
jak wskazuje nazwa - będzie wykonywać funkcję w odstępach czasowych co X
milisekund. Jednakże korzystanie z tej funkcji jest odradzane.
Kiedy wykonywany kod zablokuje możliwość uruchomienia zaplanowanej funkcji,
setInterval
będzie próbować uruchamiać daną funkcję, co będzie powodować
kolejkowanie wykonania tej samej funkcji kilkukrotnie. Może się to zdarzyć
szczególnie przy krótkim interwale.
function foo(){
// coś co blokuje wykonanie na 1 sekundę
}
setInterval(foo, 100);
W powyższym kodzie kod foo
zostanie wywołany tylko raz i zablokuje wywołanie na
jedną sekundę.
Podczas, gdy funkcja foo
blokuje wykonanie, setInterval
będzie planować kolejne
wywołania foo
. W momencie, gdy pierwsze wywołanie foo
się zakończy,
w kolejce do wywołania będzie już czekało kolejne dziesięć wywołań tej funkcji.
Najprostszą, jak również najbardziej kontrolowaną sytuacją, jest użycie setTimeout
wewnątrz wywoływanej funkcji.
function foo(){
// coś co blokuje wykonanie na 1 sekundę
setTimeout(foo, 100);
}
foo();
Powyższy kod nie tylko hermetyzuje wywołanie setTimeout
, ale też zapobiega
kolejkowaniu wywołań funkcji i daje dodatkową kontrolę. W tym przypadku funkcja
foo
może zdecydować czy powinna się wywołać ponownie, czy też nie.
Usuwanie budzików i interwałów dokonywane jest przez przekazanie odpowiedniego ID
do clearTimeout
lub clearInterval
, w zależności z jakiej funkcji zostało
zwrócone ID.
var id = setTimeout(foo, 1000);
clearTimeout(id);
Ponieważ nie istnieje wbudowana metoda usuwania wszystkich budzików i/lub interwałów, do osiągnięcia tego efektu konieczne jest użycie metody 'brute force'.
// usunięcie "wszystkich" budzików
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
Nadal mogą istnieć jakieś budziki, na które powyższy kawałek kodu nie zadziała. Ponieważ ID było z innego przedziału, zamiast korzystania z metody brute force, zaleca się śledzić wszystkie numery ID budzików, aby można je było usunąć.
eval
Do setTimeout
i setInterval
można również przekazać string jako pierwszy
parametr zamiast obiektu funkcji, jednakże nigdy nie należy korzystać z tej
możliwości, ponieważ wewnętrznie setTimeout
i setInterval
wykorzystują eval
.
function foo() {
// zostanie wykonane
}
function bar() {
function foo() {
// nigdy nie zostanie wywołane
}
setTimeout('foo()', 1000);
}
bar();
Ponieważ eval
nie zostało wywołane w tym przypadku wprost, to
string przekazany do setTimeout
zostanie uruchomiony w zasięgu globalnym.
Co za tym idzie, lokalna zmienna foo
z zasięgu bar
nie zostanie użyta.
Kolejnym zaleceniem jest niestosowanie stringów do przekazywania argumentów do funkcji, która ma zostać wywołana przez budzik.
function foo(a, b, c) {}
// NIGDY nie należy tak robić
setTimeout('foo(1,2, 3)', 1000)
// zamiast tego należy skorzystać z anonimowej funkcji
setTimeout(function() {
foo(1, 2, 3);
}, 1000)
Nigdy nie należy przekazywać stringu jako parametru do setTimeout
lub
setInterval
. Jest to wyraźną oznaką bardzo złego kodu. Jeżeli potrzebne jest
przekazanie argumentów do funkcji, należy skorzystać z anonimowej funkcji i
wewnątrz niej dokonać przekazania argumentów.
Ponadto, należy unikać korzystania z setInterval
, ponieważ planista może
zablokować wykonanie JavaScriptu.