Poniżej kilka podstawowych informacji z JS, przydatne podczas przygotowywania się do rozmowy kwalifikacyjnej.

1. Interpretowany vs kompilowany

JavaScript jest językiem interpretowanym, to znaczy, że nie musi być kompilowany z kodu źródłowego do kodu maszynowego. Języki te są interpretowane podczas wykonywania kodu, co może skutkować błędami w run-time.

Obok języków interpretowanych mamy także języki kompilowane (np. Java czy TypeScript), które wymagają dodatkowego procesu tzw. kompilacji, czyli „przetłumaczenia” składni języka na kod maszynowy. Proces ten umożliwia wyłapanie błędów już na tym etapie (compile-time errors).


2. Transpilacja

Nowoczesny javascript może nie być wspierany w starszych przeglądarkach. Aby rozwiązać ten problem, podczas budowania projektu możemy użyć Babela do przetłumaczenia (transpilacji) nowoczesnej składni na taką, którą te przeglądarki potrafią zinterpretować.


3. Języki wielu paradygmatów

Paradygmat programowania definiuje sposób patrzenia programisty na przepływ sterowania i wykonywanie programu komputerowego. W języku JavaScript odnaleźć możemy szereg paradygmatów:

  • Imperatywny
  • Proceduralny
  • Obiektowy (klasyczny oraz przy użyciu dziedziczenia prototypowego)
  • Funkcjonalny

4. W przeglądarce i na serwerze

W dużym skrócie JavaScript używa wbudowanego w przeglądarkę silnik (np. w Chrome V8). Dzięki niemu, poprzez API dostarczane przez przeglądarkę, może oddziaływać na drzewo DOM (to jest obiekt reprezentujący to, co widzimy w oknie).
W przeglądarce this wskazuje na obiekt Window (chyba, że JavaScript pracuje w trybie strict mode, wtedy this jest undefined).

Możliwe jest także uruchomienie skryptów JavaScript na serwerach przy użyciu NodeJs. Tam jednak nie ma DOM, a co za tym idzie this wskazuje na module.exports .


5. Dynamiczne typowanie

JavaScript posiada typowanie dynamiczne, które w odróżnieniu od typowania statycznego, pozwala na przypisanie innego typu do wcześniej zdefiniowanej zmiennej:

var a = 34; 
typeof(a); // number

a = 'sample';
typeof(a); // string

6. Zmienne

Deklaracja oraz inicjalizacja

Mamy 3 metody deklaracji zmiennych. Są to var, let oraz const.
Żeby pójść dalej, musimy ustalić, czym są deklaracja i inicjalizacja.

let a; // deklaracja
console.log(a) ; // undefined
a = 5; // inicjalizacja
console.log(a); // 5

Jak widać więc, deklaracja odnosi się do zarezerwowania miejsca dla zmiennej. Inicjalizacja natomiast wiąże się z przypisaniem do tego miejsca wartości. Można to porównać do podpisania koperty (deklaracja) oraz włożeniem do koperty listu (inicjalizacja).

Hoisting

Wszystkie zmienne są hositowane. Oznacza to, że deklaracja zmiennej jest „wynoszona”. Jednak to, co różni te zmienne,
to zakres w jakim są wynoszone.

  • var ma zakres funkcji, a więc deklaracja wynoszona jest na górę funkcji, w której zmienna została zainicjalizowana
  • let i const mają zakres blokowy, a więc wynoszone są na górę bloku kodu, zamkniętego w klarach { }.

Przyjrzyjmy się dokładnie, o co chodzi, z tym „wynoszeniem”, próbując odwołać się do zmiennej:

console.log(myVar);     // ReferenceError: myVar is not defined (deklaracja wewnątrz funkcji, nie wychodzi poza funkcję)

function test() {

  console.log(myVar);   // undefined; (wyniesiona deklaracja w ramach funkcji, (przenika przez blok {}))
  console.log(myLet);   // ReferenceError: myLet is not defined (nie znaleziono deklaracji w tym bloku {})
  
  {                     // Otwarcie nowego zakresu BLOKOWEGO

    console.log(myVar); // undefined; (deklaracja wyniesiona, znana już wcześniej)
    console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
                        // wyniesiona deklaracja, bo jest w tym bloku, jednak nie jest
                        // jeszcze zainicjalizowana (tzw. temporal dead zone)
    
    var myVar = 'varValue';
    let myLet = 'letValue'; // inicjalizacja
  
    console.log(myVar); // varValue;
    console.log(myLet); // letValue

  }
};

Jak widać, var nie ma problemów z przenikaniem przez bloki, a podczas odwołania się do zmiennej dostaniemy undefined. Dodatkowo, przenikając może nadpisać zmienne o tej samej nazwie.
W przypadku let i const dostaniemy błąd, a wykonanie kodu zostanie przerwane, co jest pożądane.

Temporal dead zone

Przy okazji powyżej, zaobserwować możemy tzw. temporal dead zone. Jest to strefa między wyniesioną deklaracją zmiennej, a jej inicjalizacją zmiennej.

let vs const

Rozumiemy już podstawowe różnice między var a nowymi, zdefiniowanymi w ES6 let i const. Jednak jest jedna znaczna różnica, między let i const, a ma związek z możliwością przypisania referencji do nowych wartości.

W let możemy przypisywać bez większych problemów nowe wartości do tej samej zmiennej:

let myLet = 'originalLet';
myLet = 'modifiedLet'; 
console.log(myLet); // modifiedLet 

Jednak w przypadku const, Js nie pozwoli nam na przypisanie nowej wartości.

const myConst = 'originalConst';
myConst = 'modifiedConst'; // Uncaught TypeError: Assignment to constant variable.

UWAGA const chroni nas jedynie przed zmianą referencji. Oznacza to, że możemy mutować stan consta:

const car = {
  brand: 'Volvo'
}
car.brand = 'Syrena'
console.log(car); // { brand: "Syrena" }

Jeżeli zależy nam na stworzeniu prawdziwie stałej i niemutowalnej wartości, powinniśmy skorzystać z np. Object.freeze().
Podsumowując, należy unikać stosowania var, a by default używać consta.


Domknięcia

Według definicji, domknięcie (closures) jest funkcją skojarzoną z odwołującym się do niej środowiskiem.
Żeby dobrze zrozumieć w czym rzecz, dowiedzmy się, jak działają zasięgi (scopes) w Js.
Rozważmy poniższy kod:

// ZASIĘG 1
const outside = "I'm outside";

function() {
  // ZASIĘG 2
  const inside = "I'm inside";

  console.log(outside); // "I'm outside" <-- zmienna w zasięgu rodzica, jest dostęp!
  console.log(inside);  // "I'm inside"  <-- zmienna w tym samm zasięgu, jest dostęp!
  console.log(superNested );  // Uncaught ReferenceError: superNested is not defined  <-- brak dostępu;
  
  function() {
    // ZASIĘG 3
    const superNested = "I'm super nested";

    console.log(outside);       // "I'm outside" <-- zmienna w zasięgu "dziadka", jest dostęp!
    console.log(inside);        // "I'm inside"  <-- zmienna w zasięgu rodzica, jest dostęp!
    console.log(superNested );  // "I'm super nested " <-- zmienna w tym samm zasięgu, jest dostęp!
}

console.log(outside); // "I'm outside" <-- zmienna w tym samm zasięgu, jest dostęp!
console.log(inside);  // Uncaught ReferenceError: inside is not defined  <-- brak dostępu
console.log(superNested);  // Uncaught ReferenceError: superNested is not defined  <-- brak dostępu

Możemy zaobserwować, że zmienne „szukane” są w bieżącym zakresie oraz w zakresach wszystkich rodziców po kolei.
A więc do góry. Natomiast nie można uzyskać dostępu do zmiennych, zadeklarowanych w dzieciach.
Jeżeli zmienna nie zostanie znaleziona w żadnym z zakresów, wówczas dostaniemy błąd Uncaught ReferenceError.
Dostęp do danych funkcji i danych rodziców nazywamy zakresem leksykalnym (lexical scope).

Skoro rozumiemy już, w jaki sposób działają zasięgi w js, rozważmy poniższy przykład, aby zrozumieć, czym są domknięcia.

// ZASIĘG 1
const brand = 'VW';

function getModel() {
  // ZASIĘG 2
  const model = 'Touran';
  
  // To jest domknięcie! Funkcja wewnętrzna ma dostęp do zmiennych własnych oraz do zmiennych rodzica.
  function domkniecieFn() {   
    return `${brand} - ${model}`
  }
  return domkniecieFn; // Zwracam referencję do funkcji (nie wywołuję jej)
}

console.log(brand); // VW
// Próba odwołania się do zmiennej wenątrz funkcji, która tworzy swój zasięg
console.log(model); // Uncaught ReferenceError: model is not defined

const myModel = getModel();    // zaaplikowano częściowo (partially applied)
// Dostęp do zmiennej `model` przez wykorzystanie domknięcia!
console.log(myModel())        // VW - Touran
console.log(getModel()())     // VW - Touran <-- Skorzystanie z podwójnego wywołania 

Wracając do definicji, domknięcie to zatem nic innego jak stworzenie jednej funkcji (domknięcia) wewnątrz drugiej. Dzięki domknięciom uzyskujemy dostęp do danych funkcji lub rodziców tej funkcji, czyli do zasięgu leksylaknego innej fukncji.
Domknięcia, z racji tego, że są to funkcje zwracane przez inne funkcje, musimy wywoływać dwa razy.

Po co są domknięcia?

Jednym z powodów jest stworzenie cos na wzór zmiennych prywatnych. Kod może być lepiej w lepszy i bezpieczniejszy sposób zorganizowany. Innym powodem jest możliwość zamknięcia (zamrożenia) stanu danych z funkcji z domknięciem.


Callback

Jeżeli przekazujemy funkcję jako argument do innej funkcji, rówczas nazywamy ją callbackiem.

function getModel(brand, fn) {
  const model = `${brand} - Touran`;
  fn(model);
};

getModel('VW', function(model) {
  // To jest callback
  console.log(`Model: ${model}`); // Model: VW - Touran
});

Jak widać w powyższym przykładzie, funkcja getModel w parametrze otrzymała funkcję (tzw. callback). Funkcja ta przyjmuje 2 parametry, z czego jeden z tych parametrów jest model, do którego nie mielibyśmy normalnie dostępu. Otrzymaliśmy zatem dostę do zmiennych ukrytych wewnątrz innej funkcji.


This

Pora zająć się czymś, z czym nawet doświadczeni deweloperzy mają problem. Zobaczmy na początku czym jest this?

function printThis() {
 console.log(this); 
};
printThis(); // Window {window: Window, self: Window, document: ... }

Widzimy więc, że wykonany w konsoli przeglądarki kod, będzie wskazywał na obiekt globalny window.

Sterowanie kontekstem wywołania za pomocą call, apply oraz bind

Wykorzystajmy metodę call do zmiany kontekstu wywołania tej funkcji.

printThis().call([]); // []
printThis().call(''); // String("")

Można więc powiedzieć, że this jest zmienne i zależy od kontekstu wywołania. Do sterowania kontekstem możemy użyć 3 funkcji: call, apply oraz bind;

const car1 = { model: 'Touran' };
const car2 = { model: 'Golf' };
const car3 = { model: 'Polo' };

function printModel(brand) {
  console.log(`${brand} - ${this.model}`);
};

printModel.call(car1, 'VW'); // VW - Touran (call przyjmuje argumenty po przecinku (c-comma))
printModel.apply(car2, ['VW']); // VW - Golf (apply przyjmuje tablicę argumentów (a-array))

// Przypisanie kontekstu za pomocą bind
const assignedContextCar = printModel.bind(car3);
// Już nie trzeba przypisywać kontekstu, wystarczy podać argumenty funkcji
assignedContextCar('VW'); // VW - Polo

Kontekst operatora new

Funkcje w js możesz również wywoływać za pomocą operatora new. Podczas takiego wywołania, wewnątrz funkcji, this ustawiony zostanie na ten obiekt (bez new będzie wskazywać na obiekt globalny!)

CarMaker = function() { 
  this.model = 'Tesla';
};

myCar = new CarMaker();
console.log(myCar); // CarMaker { model: "Tesla" }

Arrow functions a this

Arrow functions nie mają swojego this. Użycie this w funkcjach strzałkowych spowoduje odwołanie się do zakresu rodzica (o 1 wyżej). Nie mają zatem tego kontekstu i nie można go im ustawić.


Nowości w ES6

  • const i let oraz zakres blokowy
  • arrow functions
  • class
  • template strings
  • litarały zapisu (skrócenie zapisu obiektów { sample: sample } jako { sample })
  • parametry domyślne
  • rest i spread
  • destructuring
  • interatory
  • generatory
  • pętla for-of
  • promisy
  • moduły