Dziedziczenie prototypowe
Z wyjątkiem typów danych null
i undefined
każdy prosty typ danych ma prototyp, czyli odpowiedni obiekt opakowujący, który udostępnia metody do pracy z wartościami. Gdy wywoływana jest metoda lub wyszukiwanie właściwości w przypadku typu prymitywnego, JavaScript otula ten typ w tabeli pod spodem i wywołuje metodę lub wykonuje wyszukiwanie właściwości w obiekcie otulającym.
Na przykład litera ciągły nie ma własnych metod, ale możesz wywołać metodę .toUpperCase()
dzięki odpowiadającemu obiektowi String
:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
Nazywa się to dziedziczeniem prototypowe – dziedziczeniem właściwości i metod z odpowiedniego konstruktora wartości.
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
Za pomocą tych konstruktorów możesz tworzyć typy prymitywne zamiast definiowania ich za pomocą wartości. Na przykład konstruktor String
tworzy obiekt string, a nie ciąg znaków: obiekt, który zawiera nie tylko naszą wartość ciągu znaków, ale też wszystkie dziedziczone właściwości i metody konstruktora.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
W większości przypadków obiekty wynikowe zachowują się tak, jak wartości, których użyliśmy do ich zdefiniowania. Na przykład, mimo że zdefiniowanie wartości liczbowej za pomocą konstruktora new Number
powoduje utworzenie obiektu zawierającego wszystkie metody i właściwości prototypu Number
, możesz używać na tych obiektach operatorów matematycznych tak samo jak w przypadku literałów liczbowych:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
Bardzo rzadko będziesz musiał używać tych konstruktorów, ponieważ wbudowane dziedziczenie prototypowe w JavaScript oznacza, że nie przynoszą one żadnych praktycznych korzyści. Tworzenie typów prymitywnych za pomocą konstruktorów może też prowadzić do nieoczekiwanych wyników, ponieważ wynik jest obiektem, a nie prostym literałem:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
Może to utrudniać korzystanie z operatorów porównywania ścisłego:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Automatyczne wstawianie średnika (ASI)
Podczas parsowania skryptu interpretery JavaScript mogą używać funkcji automatycznego wstawiania średnika (ASI), aby próbować poprawiać przypadki pominięcia średnika. Jeśli parsujący JavaScript napotka nieprawidłowy token, spróbuje dodać przed nim średnik, aby naprawić potencjalny błąd składni, o ile spełniony jest co najmniej jeden z tych warunków:
- Ten token jest oddzielony od poprzedniego znakiem końca wiersza.
- Ten token to
}
. - Poprzedni token to
)
, a wstawione średnik to końcowy średnik w wyraźeniudo
…while
.
Więcej informacji znajdziesz w regułach dotyczących automatycznego rozszerzania zakresu wyszukiwania.
Na przykład pominięcie średników po tych instrukcjach nie spowoduje błędu składni, ponieważ jest to instrukcja aso:
const myVariable = 2
myVariable + 3
> 5
ASI nie może jednak uwzględnić wielu instrukcji w tym samym wierszu. Jeśli na tym samym wierszu wpiszesz więcej niż 1 instrukcję, rozdziel je średnikami:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI to próba poprawiania błędów, a nie rodzaj elastyczności składniowej wbudowanej w język JavaScript. Pamiętaj, aby w odpowiednich miejscach używać średników, aby nie polegać na tym, że kod będzie poprawny.
Tryb ścisły
Standardy określające sposób pisania kodu JavaScript ewoluowały znacznie dalej niż wszystko, co zostało rozważane na etapie wczesnego projektowania tego języka. Każda nowa zmiana oczekiwanego zachowania JavaScript musi zapobiegać błędom w starszych witrynach.
ES5 rozwiązuje niektóre od dawna znane problemy z semantyką JavaScriptu bez konieczności zmiany dotychczasowych implementacji. Wprowadziliśmy „tryb ścisły”, czyli sposób na włączenie bardziej restrykcyjnego zestawu reguł językowych w przypadku całego skryptu lub pojedynczej funkcji. Aby włączyć tryb ścisły, użyj literału ciągu znaków "use strict"
, a następnie średnika na pierwszym wierszu skryptu lub funkcji:
"use strict";
function myFunction() {
"use strict";
}
Tryb ścisły zapobiega wykonywaniu niektórych „niebezpiecznych” działań lub używaniu przestarzałych funkcji, wyświetla wyraźne błędy zamiast typowych „cichych” błędów i zabrania używania składni, które mogą kolidować z przyszłościowymi funkcjami języka. Na przykład wczesne decyzje projektowe dotyczące zakresu zmiennej zwiększały prawdopodobieństwo, że deweloperzy przez pominięcie słowa kluczowego var
nieświadomie „zanieczyścią” zakres globalny podczas deklarowania zmiennej, niezależnie od kontekstu zawierającego:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Nowoczesne środowisko uruchomieniowe JavaScript nie może poprawić tego zachowania bez ryzyka uszkodzenia witryny, która z niego korzysta, czy to przez pomyłkę, czy celowo. Nowoczesny JavaScript zapobiega temu, umożliwiając deweloperom korzystanie z trybu ścisłego w przypadku nowych działań i włączając go domyślnie tylko w kontekście nowych funkcji języka, które nie spowodują problemów ze starszymi implementacjami:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
Musisz zapisać "use strict"
jako ciąg znaków.
Szablon dosłowny (use strict
) nie zadziała. Musisz też umieścić "use strict"
przed kodem wykonalnym w jego odpowiednim kontekście. W przeciwnym razie tłumacz je ignoruje.
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
według odwołania, według wartości;
Każda zmienna, w tym właściwości obiektu, parametry funkcji oraz elementy w tablicy, zestawie lub mapie, może zawierać wartość prymitywną lub wartość referencyjną.
Gdy wartość prymitywna jest przypisywana z jednej zmiennej do drugiej, silnik JavaScripta tworzy kopię tej wartości i przypisuje ją do zmiennej.
Gdy przypiszesz zmiennej obiekt (instancje klasy, tablice i funkcje), zamiast tworzyć nową kopię tego obiektu, zmienna zawiera odwołanie do zapisanej pozycji obiektu w pamięci. Z tego powodu zmiana obiektu, do którego odwołuje się zmienna, powoduje zmianę tego obiektu, a nie tylko wartości zawartej w tej zmiennej. Jeśli np. zainicjujesz nową zmienną za pomocą zmiennej zawierającej odwołanie do obiektu, a następnie użyjesz nowej zmiennej, aby dodać do tego obiektu właściwość, właściwość i jej wartość zostaną dodane do pierwotnego obiektu:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
Jest to ważne nie tylko w przypadku modyfikowania obiektów, ale też wykonywania ścisłych porównań, ponieważ ścisłe równość obiektów wymaga, aby obie zmienne odwoływały się do tego samego obiektu, aby uzyskać wartość true
. Nie mogą one odwoływać się do różnych obiektów, nawet jeśli są one identyczne pod względem struktury:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
Przydział pamięci
JavaScript używa automatycznego zarządzania pamięcią, co oznacza, że podczas tworzenia nie trzeba jej przydzielać ani oddzielać. Szczegóły dotyczące sposobów zarządzania pamięcią przez silniki JavaScriptu wykraczają poza zakres tego modułu, ale zrozumienie sposobu przydzielania pamięci jest przydatne podczas pracy z wartościami odniesienia.
W pamięci są 2 „obszary”: „stół” i „sterta”. Stos przechowuje dane statyczne (wartości proste i odwołania do obiektów), ponieważ stałą ilość miejsca potrzebnego do przechowywania tych danych można przydzielić przed wykonaniem skryptu. Stos przechowuje obiekty, które wymagają dynamicznie przydzielanego miejsca, ponieważ ich rozmiar może się zmieniać podczas wykonywania. Pamięć jest uwalniana przez proces zwany „zbieraniem śmieci”, który usuwa z pamięci obiekty bez odwołań.
Wątek główny
JavaScript jest językiem jednowątkowym z modelem wykonania „synchronicznego”, co oznacza, że może wykonywać tylko jedno działanie naraz. Ten sekwencyjny kontekst wykonania nazywa się wątkiem głównym.
Główny wątek jest współużytkowany przez inne zadania przeglądarki, takie jak analizowanie kodu HTML, renderowanie i ponowne renderowanie części strony, uruchamianie animacji CSS oraz obsługa interakcji z użytkownikiem, od prostych (takich jak wyróżnianie tekstu) do złożonych (takich jak interakcje z elementami formularza). Dostawcy przeglądarek znaleźli sposoby na optymalizację zadań wykonywanych przez główny wątek, ale bardziej złożone skrypty nadal mogą zużywać zbyt dużo zasobów głównego wątku i wpływać na ogólną wydajność strony.
Niektóre zadania mogą być wykonywane w procesach w tle zwanych Web Workers, z pewnymi ograniczeniami:
- Pętle wątków mogą działać tylko na samodzielnych plikach JavaScript.
- Mają one znacznie ograniczony dostęp do okna przeglądarki i interfejsu użytkownika.
- Ich możliwości komunikacji z wątkiem głównym są ograniczone.
Te ograniczenia sprawiają, że są one idealne do wykonywania skoncentrowanych, wymagających zasobów zadań, które w przeciwnym razie mogłyby zajmować wątek główny.
Stos wywołań
Struktura danych służąca do zarządzania „kontekstami wykonania” (czyli aktywnie wykonywanym kodem) to lista zwana stacją wywołań (często nazywana po prostu „stacją”). Gdy skrypt jest po raz pierwszy wykonywany, interpreter JavaScripta tworzy „globalny kontekst wykonania” i przekazuje go do stosu wywołań. Instrukcje w tym kontekście globalnym są wykonywane pojedynczo, od góry do dołu. Gdy podczas wykonywania kontekstu globalnego interpreter napotyka wywołanie funkcji, przesuwa „kontekst wykonania funkcji” wywołania na wierzchołek stosu, wstrzymuje kontekst wykonania funkcji globalnej i wykonuje kontekst wykonania funkcji.
Za każdym razem, gdy wywoływana jest funkcja, kontekst jej wykonania jest przesuwany na wierzchołek stosu, tuż nad bieżącym kontekstem wykonania. Stos wywołań działa według zasady „ostatni na wejściu, pierwszy na wyjściu”, co oznacza, że wykonywane jest ostatnie wywołanie funkcji, które znajduje się najwyżej w stosie, i trwa to do momentu jego zakończenia. Gdy funkcja zostanie wykonana, interpreter usuwa ją ze stosu wywołań, a kontekst wykonania zawierający to wywołanie funkcji staje się najwyższym elementem na stosie i wznawia wykonywanie.
Te konteksty wykonania przechwytują wszystkie wartości potrzebne do ich wykonania. Określają też zmienne i funkcje dostępne w zakresie funkcji na podstawie jej kontekstu nadrzędnego oraz określają i ustalają wartość słowa kluczowego this
w kontekście funkcji.
pętla zdarzeń i kolejka wywołań zwrotnych,
Takie sekwencyjne wykonywanie oznacza, że zadania asynchroniczne, które zawierają funkcje wywołania zwrotnego, takie jak pobieranie danych z serwera, reagowanie na interakcje użytkownika lub oczekiwanie na liczniki czasu ustawione za pomocą funkcji setTimeout
lub setInterval
, będą albo blokować wątek główny do czasu zakończenia zadania, albo niespodziewanie przerwą bieżący kontekst wykonania w momencie dodania kontekstu wykonania funkcji wywołania zwrotnego do stosu. Aby temu zaradzić, JavaScript zarządza zadaniami asynchronicznymi za pomocą „modelu współbieżności” opartego na zdarzeniach, który składa się z „pętli zdarzeń” i „kolejki wywołań zwrotnych” (czasami nazywanej „kolejką wiadomości”).
Gdy zadanie asynchroniczne jest wykonywane na wątku głównym, kontekst wykonania funkcji wywołania zwrotnego jest umieszczany w kole wywołań zwrotnych, a nie na szczycie stosu wywołań. Petla zdarzeń to wzór, który czasami nazywa się reaktorem, który stale sprawdza stan stosu wywołań i kolejki wywołań zwrotnych. Jeśli w kole wywołań zwrotnych są zadania, a pętla zdarzeń stwierdzi, że stos wywołań jest pusty, zadania z kole wywołań zwrotnych są przesyłane do stosu pojedynczo w celu wykonania.