Przejdź do treści

Recenzja: "Tajniki języka JavaScript. Zakresy i domknięcia"

Historia zaczęła się na minionym WarsawJS, gdzie drugim prelegentem był Dominik Lubański. Prelekcja jest dostępna na kanale WarsawJS, jednak klikając na ten link wejdziecie bezpośrednio do momentu, który rozpoczyna moją przygodę z YDKJS ("You Don't Know JS").

Baner promujący artykuł

Jak to się zaczęło?

Dominik pod koniec swojej prelekcji powiedział, że "ciśnie" wszystkich w swojej firmie, aby przeczytali "od deski do deski" wszystkie książki You Don't Know JS. Cenię sobie kompetencje oraz doświadczenie w programowaniu Dominika, dlatego też jego słowa wziąłem sobie do serca i pomimo tego, że nie pracujemy razem, to sam przysiadłem do przeczytania YDKJS.

Nigdy nie czytałem YDKJS. Wydawało mi się to zbyteczne. Myślałem sobie:

YDKJS? Kolejna zwykła książka traktująca o JavaScript. Przeczytałem ich tak wiele, że nie zaszkodzi jeśli pominę tę serię.

Myślałem tak aż do momentu tej prelekcji.

Postawiłem sobie nowy cel: przeczytać całą serię!

Dlaczego YDKJS?

Wydawnictwo Helion.pl trochę pomogło i stworzyło specjalną ofertę promocyjną na swojej stronie: helion.pl/seria-JS.phtml, gdzie dostępna jest lista 5 książek You Don't Know JS autorstwa Kyle Simpsona. Książki, są przetłumaczone na język polski.

Nie będę ukrywał, że książki lepiej mi się czyta w języku ojczystym. Mój poziom angielskiego wystarczy mi do komunikacji z obcokrajowcami, ale nie pozwala na płynne (a co najważniejsze, zachowując to samo tempo) czytanie podręczników.

Wczoraj na łamach fanpaga WarsawJS zamieściłem link do tej promocji.
Nasuwa się pytanie: czy warto?

Po kilku minutach od publikacji pojawił się komentarz (pod tym wpisem), że książka jest dostępna za darmo na publicznym profilu projektu YDKJS.

To prawda. Projekt dostępny jest tutaj: github.com/getify/You-Dont-Know-JS

Jednak uważam, że prawdziwe czytanie książki jest TYLKO w wersji papierowej.

Tak zostałem wychowany i to już się nie zmieni. Oczywiście nie mówię tego bez porównania. Kupiłem sobie jakiś czas temu (może to już 2 lata) Kindla. Mam kilka ebooków na nim. Jednak... bez rewelacji. Nie czuję tego klimatu. Brakuje mi dotyku papieru. No cóż.

Może i jestem osobą, która lubi nowinki techniczne, jednak książki mają to do siebie, że muszę je czuć podczas czytania. Lubię dbać o nie, obkładać szarym papierem jak w bibliotece.

Dobra. Wracamy do meritum!


Kilka dni temu kupiłem wszystkie dostępne 5 książek z serii. Szósta książka jest w przygotowaniu (pewnie obecnie trwa proces tłumaczenia oryginału). Termin publikacji to sierpień, tego roku.

Przedwczoraj zabrałem się za czytanie i napisałem o tym na Twitterze.
Zauważył ją autor książki, Kyle, polajkował i zaretweetował z komentarzem: "Polish translation".

Zrobiło mi się miło. Jemu pewnie również.

Jak czytam książki?

Wspomniana książka składa się z około 100 stron.
Więc na przeczytanie całej potrzebuje około 2-3 godzin.

Generalnie, gdy czytam książki to robię sobie notatki, ale nie takie normalne notatki gdzieś obok na kartce. Nie piszę również długopisem ani ołówkiem po książce - uważam to za pewnego rodzaju złamanie zasad.

Pisanie długopisem albo ołówkiem po książce to bluźnierstwo! Piotr Kowalski

Gdy czytam książki to naklejam stickery (takie małe karteczki), na których notuję jakieś informacje. Przykładem niech będzie moja prelekcja na "Angular Warsaw" nt. mojego podejścia do frameworka AngularJS po przeczytaniu dwóch książek. Zobaczcie slajdy.

Moje notatki

Tym razem nie było inaczej. W sumie użyłem 11-tu karteczek, gdzie napisałem sobie notatki w związku z daną zawartością strony, na której karteczka znalazła swoje miejsce.

Może przedstawię te kilkanaście punktów, gdzie chciałem napisać komentarz.

Sticker 1. Strona 18. Tekst z karteczki: LHS - co jest celem przypisania? RHS - co jest źródłem przypisania?

Co się kryje za terminem LHS? Rozwinięcia tego akronimu próżno szukać w książce. Jest za to coś ważniejszego, czyli wytłumaczenie pojęcia. W skrócie chodzi o to, że silnik gdy widzi taki kod:

var a = 5;

...wykonuje 2 operacje. Pierwsza to definicja zmiennej:

var a;

druga to:

a = 5;

Ten drugi etap jest wyciągnięciem zmiennej w trybie do przypisania, czyli LHS
Drugi termin, czyli RHS oznacza, że silnik chce użyć zmienną. I tak o to prosty przykład:

console.log(a);

Mamy tutaj pobranie wartości zmiennej.

Nigdy nie słyszałem, o takich nazwach trybów dostępu. Oczywiście, używałem obu tych form, jednak nie wiedziałem, że jest taka terminologia (zasada działania) zaimplementowana w silniku.

  • LHS oznacza Left Hand Side
  • RHS oznacza Right Hand Side

Dla dociekliwych, polecam przeczytać wpis opisujący bardziej dokładnie LHS i RHS:
https://john-dugan.com/hoisting-in-javascript/

Sticker 2. Strona 30. Tekst z karteczki: Wyszukiwanie zakresu zostanie zatrzymane po znalezieniu pierwszego dopasowania!

Karteczka czysto informacyjna. Jak silnik nie znajdzie zmiennej w zakresie, gdzie jest żądanie pobrania wartości, to szuka w zakresie wyższym. Jeśli znajdzie to przestaje szukać. Chociaż oczywiste, to warto zanotować.

Sticker 3. Strona 34. Tekst z karteczki: Wyciekająca zmienna globalna (with)

Kto używa with w dzisiejszych czasach? Chyba tylko Firebug. Sticker przypomina, że with jest bardzo złe bo, jeśli w obiekcie, który jest brany pod uwagę przez with nie znajdziemy właściwości, którą chcemy zaktualizować, to będzie ona ustawiona jako globalna. Przykład:

var dog = {
    name: ''
};
with (dog) {
    name = 'doggy';
    surname = 'what?';
}
console.log(dog.name); // "doggy"
console.log(dog.surname); // undefined
console.log(window.surname); // "what?"
Sticker 4. Strona 46. Tekst z karteczki: ...czyli arrow function to wyrażenie funkcyjne!

Zgadza się! Arrow Function to nie funkcja a wyrażenie funkcyjne! Dlaczego? Każda funkcja podczas definicji musi posiadać nazwę, a ze względu na to, że w składni AF nie można zdefiniować nazwy to jest to wyrażenie funkcyjne! Dziwne, bo przecież są funkcje anonimowe, czyli takie, które nie posiadają nazwy. Temat warty dyskusji.

Sticker 5. Strona 55. Tekst z karteczki: Ciekawy zabieg wydajnościowy zw. z { }

Uważam to za interesujące, ponieważ w JavaScript mamy zasięg leksykalny, czyli funkcyjny. Zasięg zaczyna się na początku definicja funkcji i kończy wraz z jej końcem. Wyjątkiem jest konstrukcja try...catch, gdzie w bloku catch mamy zasięg blokowy! Czyli od klamerki { do klamerki }.

try {
    throw new Error('test');
} catch (error) {
    console.log(error); // zmienna `error` jest dostępna TYLKO pomiędzy `{}`
}

W specyfikacji ECMAScript 6 mamy wsparcie do zasięgu blokowego, gdy definiujemy zmienną wyrażeniami: let albo const.

Pewną optymalizacją jest, aby nie definiować zmiennych na początku funkcji (w zasięgu blokowym), skoro używamy jej tylko w jednym miejscu. Przykład na pewno rozjaśni sprawę.

function doSomething() {
    let e = 0;

    bar(foo);

    var r = 'abc';

    for (let t = 0; t < 3; t++) {
        setTimeout(function () {
            console.log(t);
        }, 400);
    }

    let startLabel = 'start' + e;
    let endLabel = 'end' + (10 - e);
    console.log([startLabel, endLabel])
}

Na powyższym przykładzie widać, że zmienna e jest zdefiniowana bardzo wysoko. Lepiej od strony wydajności jest zdefiniować ją blisko użycia i opakować w klamerki:

function doSomething() {
    bar(foo);

    var r = 'abc';

    for (let t = 0; t < 3; t++) {
        setTimeout(function () {
            console.log(t);
        }, 400);
    }

    {
        let e = 0;
        let startLabel = 'start' + e;
        let endLabel = 'end' + (10 - e);
        console.log([startLabel, endLabel])
    }
}

Tworzymy wtedy zasięg TYLKO dla zmiennych e, startLabel i endLabel. No i tylko w tym zasięgu zmienna e jest potrzebna. Nie ma sensu jej wciskać do zasięgu zewnętrznego.

Sticker 6. Strona 63. Tekst z karteczki: Bardzo dziwny kawałek kodu, dający jednak jasno do zrozumienia, że to funkcje są pierwotnie "wynoszone" (hoisting)

Może zacytuje kawałek kodu, o którym mowa:

foo(); // 1

var foo;

function foo() {
    console.log(1);
}

foo = function () {
    console.log(2);
};

Tak to zinterpretuje silnik:

function foo() {
    console.log(1);
}

foo(); // 1

foo = function () {
    console.log(2);
};

Interesujący smaczek języka!

Sticker 7. Strona 73. Tekst z karteczki: Domknięcie to funkcja, która ma dostęp do zmiennych lokalnych (zdefiniowanych w tym samym zakresie leksykalnym co dana funkcja) musi być uruchomiona na zewnątrz tego zakresu!

IIFE to nie domknięcie. Szok. Przez tyle lat tak myślałem. Prawdziwe domknięcie polega na tym, że uruchamiamy funkcję z zakresu, gdzie nie ma dostępnej danej zmiennej. Przykład zobrazuje to najlepiej.

function MyModule() {
    var secret = 123;

    function getSecret() {
        return secret;
    }

    return {
        getSecret: getSecret
    };
}

var mod = MyModule();
mod.getSecret(); // Uruchamiamy funkcję, która ma dostęp do zmiennej "secret". Tylko w ten sposób do niej się dobierzemy.
Sticker 8. Strona 80. Tekst z karteczki: Piękna definicja wzorca Singleton

Najpopularniejszy wzorzec projektowy, który bardzo często słyszę na rozmowach rekrutacyjnych jako odpowiedź na pytanie: A jakie znasz wzorce projektowe? SINGLETON!

Świetny przykład znajduje się na 80-tej stronie. Może skrócę go do wersji minimum:

var foo = (function () {
    'use strict'; // tego w książce nie było

    var secret = 12345;

    function getSecret() {
        return secret;
    }

    return {
        getSecret: getSecret
    };
}());

foo.getSecret();

Pięknie się tworzy takie implementacje.

Sticker 9. Strona 84. Tekst z karteczki: Błąd: zamiast module foo from "foo"; powinno być import foo from "foo";

No i mamy (niestety) pierwszy błąd w książce, bynajmniej znaleziony przeze mnie. Pod koniec 5-tego rozdziału autor chciał opisać moduły w ES6. Nazwa podrozdział "Pyszne moduły". Niestety kod źródłowy w przykładzie posiada rażące błędy.


Stworzyłem projekt na GitHubie opisujący błędny kod, wraz z kodem poprawionym.
Zapraszam do zapoznania się: github.com/piecioshka/errors-in-ydkjs-scope-closures-book


A o to lista błędów:

  1. pierwszy problem to ścieżki, do plików lokalnych trzeba odnosić się poprzez ./ na początku
  2. nie ma w specyfikacji ECMAScript 6 słowa operatora module (taki w stylu import)
  3. nie ma takiej składni eksportowania jak export hello; (lista dostępnych możliwości), trzeba eksportować tak export {hello};
  4. nie można zmienić nazwy importowanego modułu import foo from "./foo";, należy wskazać, że API z pliku foo.js, będzie w obiekcie poprzez takie kod: import * as foo from "./foo"; (lista wszystkich możliwości importowania)
  5. jeżeli chcemy z modułu wyciągnąć tylko jedną funkcję to trzeba opakować ja w curly braces, zamiast import hello from "bar"; trzeba import {hello} from "bar";
Sticker 10. Strona 92. Tekst z karteczki: Trochę szkoda, że nikt nie wspomniał o Babel.js

W książce jako transpiler wymieniony byl tylko Traceur. Z tego co pamiętam, to ma on zdecydowanie mniejsze pokrycia specyfikacji niż Babel.js. Babel ma bardzo przyjemne wsparcie do m.in Webpacka i jest zdecydowanie bardziej popularniejszy w społeczności.

Sticker 11. Strona 94. Tekst z karteczki: TODO: skorzystać z projektu "let-er"

let-er - nowe narzędzie - warto się zapoznać! Projekt autorstwa Kyle Simpsona pozwala na kompilację bloku kodu:

let (x = "foo") {
    console.log(x); // "foo"
}

Do zapisuj znanego nam z ES6:

{
    let x = "foo";
    console.log(x); // "foo"
}

Narzędzie posiada opcję konwersji do ES3. Jak już wcześniej wspomniałem, w JavaScripcie jest zasięg funkcyjny, z 1 wyjątkiem, konstrukcja try...catch, w której mamy zasięg blokowy. Przykład:

try { throw "foo" } catch (x) {
    console.log(x);
}

Jeżeli chodzi o mnie to projektu raczej nie wykorzystam. Nie widzę praktycznego zastosowania.

Moje doświadczenie

Może trochę słów o książce. Czytało mi się ją szybko. Nie miałem jakiś problemów ze zrozumiem, o co autorowi chodzi. Książka dla mnie jest odświeżeniem tematu zakresów i domknięć. Polecam ją każdemu, kto zaczyna swoją przygodę z JavaScriptem.

Mam nadzieje, że inne książki z serii będą miały podobny styl. Dziękuję Dominik, że zaszczepiłeś we mnie ten pomysł na przeczytanie całej serii.