Wstęp

Autorzy

Ten przewodnik jest dziełem dwóch uroczych użytkowników Stack Overflow, Ivo Wetzel (Treść) oraz Zhang Yi Jiang (Projekt).

Współtwórcy

Tłumaczenie

Hosting

JavaScript Garden znajduje się na serwerach GitHub, ale dzięki wsparciu Cramer Development posiadamy również mirror na serwerze JavaScriptGarden.info.

Licencja

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.

Obiekty

Wykorzystanie obiektów i ich właściwości

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 jako typy danych

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}; 

Dostęp do właściwości

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.

Usuwanie 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.

Notacja właściwości

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.

Prototyp

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.

Wyszukiwanie własności

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.

Właściwość prototype

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.

Wydajność

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 natywnych prototypów

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

Wnioski

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

Wnioski

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.

Pętla 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.

Filtrowania przy użyciu 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.

Wnioski

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

Deklaracje funkcji i wyrażenia funkcyjne

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.

Deklaracja 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() {}

Wyrażenie funkcyjne

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.

Nazwane wyrażenia funkcyjne

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.

Jak działa 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.

Zasięg globalny

this;

Używanie this w globalnym zasięgu, zwróci po prostu referencję do obiektu global.

Wywołanie funkcji

foo();

Tutaj this również będzie wskazywało na obiekt global

Wywoływanie metody

test.foo(); 

W tym przypadku this będzie wskazywało na test.

Wywołanie konstruktora

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.

Jawne ustawienie 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.

Częste pułapki

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ół.

Metody przypisywania

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ę.

Domknięcia i referencje

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.

Emulowanie prywatnych zmiennych

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.

Dlaczego zmienne prywatne działają?

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.

Domknięcia wewnątrz pętli

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.

Unikanie problemu z referencją

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)
}

Obiekt 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.

Konwersja do tablicy

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ść.

Przekazywanie argumentów

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);
};

Parametry formalne i indeksy argumentów

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);

Mity i prawdy o wydajności

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

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.

Fabryki

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.

Tworzenie nowych obiektów korzystając z fabryk

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.

  1. Zużywa więcej pamięci, ponieważ tworzony obiekt nie współdzieli metod poprzez prototyp.
  2. Aby móc dziedziczyć fabryka musi skopiować wszystkie metody z dziedziczonego obiektu lub przypisać ten obiekt, z którego się dziedziczy, jako prototyp do nowo utworzonego obiektu.
  3. Porzucenie łańcucha prototypów tylko ze względu na opuszczone słowo kluczowe new jest sprzeczne z duchem języka.

Wnioski

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.

Zasięg zmiennych i przestrzenie nazw

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.

Zmora globalnych zmiennych

// 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.

Lokalne zmienne

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.

"Hoisting" - wywindowanie, podnoszenie

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 = {};
}

Kolejność rozwiązywania nazw

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:

  1. Jeżeli wewnątrz aktualnego zasięgu znajduje się deklaracja var foo, skorzystaj z niej.
  2. Jeżeli jeden z parametrów fukcji został nazwany foo, użyj go.
  3. Jeżeli funkcja została nazwana foo, skorzystaj z tego.
  4. Przejdź do zewnętrznego zasięgu i przejdź do kroku #1.

Przestrzenie nazw

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(){}());

Wnioski

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.

Tablice

Iterowanie po tablicach oraz właściwościach tablic

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.

Iteracja

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.

Właściwość 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ę.

Wnioski

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ść.

Konstruktor 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); 

Wnioski

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.

Typy

Równość i porównania

JavaScript posiada dwa różne sposoby równościowego porównywania obiektów.

Operator równości

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

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.

Porównywanie obiektów

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).

Wnioski

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.

Operator 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.

Tablica typów JavaScript

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.

Klasa obiektu

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.

Testowanie niezdefiniowania zmiennej

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.

Wnioski

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.

Operator 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.

Porównywanie obiektów utworzonych klas

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

Stosowanie instanceof na natywnych typach

new 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.

Wnioski

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.

Rzutowanie typów

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

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.

Rzutowanie do typu String

'' + 10 === '10'; // true

Konkatenacja pustego stringu i wartości powoduje rzutowanie do typu String.

Rzutowanie do typu Number

+'10' === 10; // true

Zastosowanie unarnego operatora + spowoduje rzutowanie do typu Number.

Rzutowanie do typu Boolean

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

Jądro

Dlaczego nie należy używać 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 przebraniu

Funkcje 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.

Problemy z bezpieczeństwem

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.

Wnioski

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.

Wartość 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:

  • dostęp do (niemodyfikowalnej) zmiennej globalnej undefined,
  • wyjście z funkcji, która nie ma deklaracji return,
  • deklaracja return, która nic jawnie nie zwraca,
  • poszukiwanie nieistniejącej właściwości,
  • parametr funkcji, który nie został jawnie przekazany podczas wywołania funkcji,
  • wszystko czemu została przypisana wartość undefined.

Obsługa przypadku zmiany wartości 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.

Zastosowanie 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.

Automatyczne wstawianie średnika

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.

Jak działa wstawianie

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.

Nawiasy

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ą.

Wnioski

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.

Inne

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();

Kolejkowanie wywołań z 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.

Radzenie sobie z możliwymi blokadami

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.

Ręczne usuwanie budzików

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);

Usuwanie wszystkich budzików

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ąć.

Ukryte wykorzystanie 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)

Wnioski

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.