Dodatek

Dziedziczenie prototypowe

Z wyjątkiem null i undefined każdy podstawowy typ danych ma prototyp – odpowiadający mu kod towarzyszący obiektu, który zapewnia metody pracy z wartościami. Po wywołaniu wyszukiwania metody lub właściwości w elemencie podstawowym JavaScript opakowuje obiekt podstawowy i wywołuje tę metodę lub przeprowadza wyszukiwanie właściwości w obiekcie wrapper.

Na przykład literał ciągu znaków nie ma własnych metod, ale możesz wywołać w nim metodę .toUpperCase() dzięki odpowiedniemu otokowi obiektu String:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Jest to tzw. dziedziczenie prototypowe, które obejmuje właściwości i metody 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ć elementy podstawowe, zamiast definiować je tylko według ich wartości. Na przykład użycie konstruktora String powoduje utworzenie obiektu ciągu, a nie literału ciągu znaków – obiektu, który zawiera nie tylko wartość ciągu znaków, ale też wszystkie odziedziczone 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."

Obiekty powstałe w ten sposób zachowują się zwykle tak samo jak wartości użyte do ich zdefiniowania. Na przykład: chociaż zdefiniowanie wartości liczbowej za pomocą konstruktora new Number powoduje, że obiekt zawiera wszystkie metody i właściwości prototypu Number, możesz używać na nich 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

Używaj ich bardzo rzadko, ponieważ wbudowane w JavaScript dziedziczenie prototypowe sprawia, że nie zapewniają one żadnych praktycznych korzyści. Tworzenie podstawowych konstruktorów również może prowadzić do nieoczekiwanych wyników, ponieważ są one obiektem, a nie prostym literałem:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Może to skomplikować korzystanie z rygorystycznych operatorów porównania:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Automatyczne wstawianie średników (ASI)

Podczas analizy skryptu interpretery JavaScriptu mogą korzystać z funkcji automatycznego wstawiania średników (ASI), aby poprawiać wystąpienia pominiętych średników. Jeśli parser JavaScript napotka niedozwolony token, spróbuje dodać przed nim średnik, aby naprawić potencjalny błąd składni, o ile spełniony zostanie co najmniej jeden z tych warunków:

  • Token ten jest oddzielony od poprzedniego tokena podziałem wiersza.
  • Ten token to }.
  • Poprzedni token to ), a wstawiony średnik byłby średnikiem zakończenia instrukcji do...while.

Więcej informacji znajdziesz w zasadach ASI.

Na przykład pominięcie średników po tych instrukcjach nie spowoduje błędu składni z powodu ASI:

const myVariable = 2
myVariable + 3
> 5

Jednak ASI nie może uwzględnić wielu wyciągów w tym samym wierszu. Jeśli w jednym wierszu wpiszesz więcej niż jedno polecenie, rozdziel je średnikami:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI to próba korekty błędów, a nie rodzaj elastyczności składniowej wbudowanej w JavaScript. Pamiętaj, by w odpowiednich miejscach używać średników, aby nie polegać na ich poprawnym kodzie.

Tryb ścisły

Standardy określające sposób pisania JavaScriptu znacznie się rozwinęły. Każda nowa zmiana oczekiwanego działania JavaScriptu musi uniknąć powodowania błędów w starszych witrynach.

Standard ES5 rozwiązuje niektóre długotrwałe problemy z semantyką JavaScriptu bez naruszania istniejących implementacji. Jest to możliwe dzięki wprowadzeniu „trybu rygorystycznego”, który pozwala na bardziej restrykcyjny zestaw reguł językowych dotyczących całego skryptu lub pojedynczej funkcji. Aby włączyć tryb ścisły, w pierwszym wierszu skryptu lub funkcji użyj literału ciągu znaków "use strict", po którym następuje średnik:

"use strict";
function myFunction() {
  "use strict";
}

Tryb ścisły zapobiega określonych „niebezpiecznym” działaniom lub wycofanym funkcjom, generuje jednoznaczne błędy w miejsce typowych „cichych” błędów i uniemożliwia używanie składni, które mogą kolidować z przyszłymi funkcjami językowymi. Na przykład na wczesnym etapie decyzji związanych z zakresem zmiennych deweloperzy z większym prawdopodobieństwem „zanieczyszczają” zakres globalny podczas deklarowania zmiennej (niezależnie od jej kontekstu) przez pominięcie słowa kluczowego var:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

Współczesne środowiska wykonawcze JavaScriptu nie są w stanie skorygować tego zachowania bez ryzyka uszkodzenia witryny korzystającej z tego kodu – błędnie lub celowo. Zamiast tego współczesny JavaScript zapobiega takiej sytuacji, umożliwiając programistom włączanie trybu ścisłego na potrzeby nowych zadań i włączanie trybu ścisłego tylko w kontekście nowych funkcji językowych, które nie zakłócają starszych implementacji:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Musisz zapisać "use strict" jako literał ciągu. Literał szablonu (use strict) nie zadziała. Musisz też umieścić ciąg "use strict" przed kodem wykonywalnym we właściwym kontekście. W przeciwnym razie tłumacz je zignoruje.

(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

Wg, według wartości

Każda zmienna, w tym właściwości obiektu, parametry funkcji i elementy tablicy, zestawu lub mapy, może zawierać wartość podstawową lub wartość referencyjną.

Gdy wartość podstawowa jest przypisywana z jednej zmiennej do drugiej, mechanizm JavaScript tworzy kopię tej wartości i przypisuje ją do zmiennej.

Gdy przypisujesz do zmiennej obiekt (wystąpienia klas, tablice i funkcje), zamiast tworzyć nową kopię tego obiektu, zmienna zawiera odwołanie do pozycji obiektu zapisanego w pamięci. Dlatego zmiana obiektu, do którego odwołuje się zmienna, powoduje zmianę tego, do którego odwołuje się zmienna, a nie tylko jej wartości. Jeśli np. zainicjujesz nową zmienną ze zmienną zawierającą odwołanie do obiektu, a potem za pomocą nowej zmiennej dodasz właściwość do tego obiektu, 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 także wykonywania rygorystycznych porównań, ponieważ ścisła równość obiektów wymaga, aby obie zmienne zwracały się do tego samego obiektu w celu obliczenia wartości true. Nie mogą się odwoływać do różnych obiektów, nawet jeśli ich struktura są identyczne:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Alokacja pamięci

W języku JavaScript stosuje się automatyczne zarządzanie pamięcią, co oznacza, że w trakcie programowania nie trzeba jej wyraźnie przydzielać ani przydzielać jej pamięci. Szczegóły metod zarządzania pamięcią przez silniki JavaScript wykraczają poza zakres tego modułu, ale zrozumienie sposobu przydzielania pamięci zapewnia kontekst przydatny podczas pracy z wartościami referencyjnymi.

Pamięć ma 2 „obszary”: „stos” i „stertę”. Stos przechowuje dane statyczne – wartości podstawowe i odwołania do obiektów, ponieważ stałą ilość miejsca potrzebną do przechowywania tych danych można przydzielić przed wykonaniem skryptu. Sterta przechowuje obiekty, które wymagają dynamicznie przydzielanego miejsca, ponieważ ich rozmiar może zmieniać się podczas wykonywania. Pamięć jest zwalniana przez proces o nazwie „śmieciowanie”, który usuwa z pamięci obiekty bez odniesień.

Wątek główny

JavaScript jest językiem jednowątkowym i „synchronicznym” modelem wykonywania, co oznacza, że może wykonywać tylko jedno zadanie naraz. Taki kontekst wykonywania sekwencyjnego jest nazywany wątkiem głównym.

Wątek główny 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ługę interakcji użytkowników – od prostych (np. podświetlania tekstu) po złożone (np. interakcja z elementami formularza). Dostawcy przeglądarek znaleźli sposoby optymalizowania zadań wykonywanych w wątku głównym, ale bardziej złożone skrypty mogą nadal wykorzystywać zbyt dużą część zasobów głównego wątku, co może mieć wpływ na ogólną wydajność strony.

Niektóre działania można wykonywać w wątkach w tle o nazwie procesy robocze, ale obowiązują pewne ograniczenia:

  • Wątki instancji roboczych mogą działać tylko na samodzielnych plikach JavaScript.
  • Mają znacznie ograniczony dostęp do okna i interfejsu przeglądarki lub nie mają go wcale.
  • Możliwości komunikacji z wątkiem głównym są ograniczone.

Te ograniczenia sprawiają, że są one idealnym rozwiązaniem w przypadku skoncentrowanych zadań wymagających dużej ilości zasobów, które w innym przypadku mogłyby zajmować główny wątek.

Stos wywołań

Struktura danych używana do zarządzania „kontekstami wykonywania” – czyli aktywnie wykonywanym kodem – to lista o nazwie stos wywołań (często po prostu „stos”). Przy pierwszym uruchomieniu skryptu interpreter JavaScript tworzy „globalny kontekst wykonywania” i przekazuje go do stosu wywołań, a instrukcje wewnątrz tego kontekstu globalnego są wykonywane po kolei, od góry do dołu. Gdy interpreter napotyka wywołanie funkcji podczas wykonywania kontekstu globalnego, przekazuje „kontekst wykonania funkcji” dla tego wywołania na początek stosu, wstrzymuje globalny kontekst wykonywania i wykona kontekst wykonania funkcji.

Przy każdym wywołaniu funkcji kontekst wykonania tej funkcji jest przekazywany na górę stosu, tuż nad bieżącym kontekstem wykonania. Stos wywołań działa na zasadzie „pierwsze wejście, pierwsze wyjście”, co oznacza, że ostatnie wywołanie funkcji, które znajduje się najwyżej w stosie, jest wykonywane i kontynuuje, aż się zamknie. Po zakończeniu tej funkcji tłumacz usuwa ją ze stosu wywołań, a kontekst wykonania, który zawiera wywołanie tej funkcji, staje się ponownie najwyższym elementem w stosie i wznawia wykonanie.

Te konteksty wykonania rejestrują wszelkie wartości niezbędne do ich wykonania. Określają też zmienne i funkcje dostępne w ramach zakresu funkcji na podstawie jej kontekstu nadrzędnego oraz określają i ustawiają wartość słowa kluczowego this w kontekście funkcji.

pętla zdarzeń i kolejka wywołań zwrotnych.

To wykonanie sekwencyjne oznacza, że zadania asynchroniczne, które obejmują funkcje wywołania zwrotnego, takie jak pobieranie danych z serwera, odpowiadanie na interakcję użytkownika lub oczekiwanie na liczniki czasu ustawione za pomocą setTimeout lub setInterval, powodują zablokowanie głównego wątku do momentu ukończenia zadania lub nieoczekiwanie przerywają bieżący kontekst wykonywania w momencie dodania do stosu kontekstu wykonywania funkcji wywołania zwrotnego. Aby rozwiązać ten problem, JavaScript zarządza zadaniami asynchronicznymi za pomocą opartego na zdarzeniach „modelu równoczesności”, który składa się z „pętli zdarzeń” i „kolejki wywołań zwrotnych” (czasami nazywanej „kolejką wiadomości”).

Gdy zadanie asynchroniczne jest wykonywane w wątku głównym, kontekst wykonania funkcji wywołania zwrotnego jest umieszczany w kolejce wywołań zwrotnych, a nie na stosie wywołań. Pętla zdarzeń to wzorzec nazywany czasem reaktorem, który na bieżąco sprawdza stan stosu wywołań i kolejki wywołań zwrotnych. Jeśli w kolejce wywołań zwrotnych znajdują się zadania, a pętla zdarzeń ustali, że stos wywołań jest pusty, zadania z kolejki wywołań zwrotnych są pojedynczo przekazywane do stosu w celu ich wykonania.