Poniżej kilka podstawowych informacji z JS, przydatne podczas przygotowywania się do rozmowy kwalifikacyjnej.
- 1. Interpretowany vs kompilowany
- 2. Transpilacja
- 3. Języki wielu paradygmatów
- 4. W przeglądarce i na serwerze
- 5. Dynamiczne typowanie
- 6. Zmienne
- Domknięcia
- Callback
- This
- Nowości w ES6
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ą callback
iem.
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