Wprowadzenie do JavaScript
ECMAScript 5, Web APIs, testowanie aplikacji
Bartosz Szczeciński
16.05.2018
@btmpl
medium.com/@baphemot
software developer, konsultant
https://szczecinski.eu
https://medium.com/@baphemot/
https://www.youtube.com/watch?v=2pL28CcEijU
JavaScript to jedno-wątkowy, lekki, interpretowany (lub korzystający z technologii JIT) język programowania.
Mimo iż JS oferuje prototypowy model pracy z obiektami umożliwia on pracę z wieloma paradygmatami tj. programowanie proceduralne oraz funkcyjne.
Początki języka
Autorem języka JavaScrip jest Brendan Eich - w ciągu 10 dni, w 1995 roku pierwszą wersję języka znanego dziś jako "JavaScript".
Początkowo język ten miał działać w przeglądarce i dać przewagę przeglądarce Netscape Navigator nad Internet Explorerem i Microsoftem.
Dzisiaj "JavaScript" jest wielo-platformowym językiem działającym w silnikach oddzielonych od przeglądarki i na wszelkiego rodzaju platformach.
ECMAScript i wersje JS
JavaScript nie jest defacto językiem a implementacją języka ECMA-262 (ECMAScript) zdefiniowanego przez ECMA. Dlatego też często spotkamy się z określeniami "ES", "ECMAScript" etc.
Jest to szczególnie istotne, jako iż JavaScript jest znakiem towarowym korporacji Oracle, która wielokrotnie pokazywała, że jest gotowa bronić swojej "własności intelektualnej".
ECMAScript i wersje JS
Aktualna wersja ECMAScript to ES8.
ES5 pozostaje wciąż istotną częścią ekosystemu, jako iż jest to najnowsza wersja wspierana natywnie przez większość dostępnych na rynku przeglądarek.
Zmiany pomiędzy ES5 a ES6 są dosyć istotne. Kolejne wersje przynoszą już mniejsze zmiany.
JS dzisiaj i jutro
Obecnie standardem jest wciąż dostarczanie do przeglądarek kodu JavaScript w wersji ES5.
Dzięki narzędziom takim jak Babel możliwe jest tworzenie kodu w nowszych standardach (w tym z wykorzystaniem notacji niedostępnej jeszcze w standardzie ECMAScript) i następnie "transpilowanie" go do ES5.
Na warsztatach skupimy się jednak na tworzeniu bezpośrednio w ES5.
Składnia jest to zbiór słów kluczowych, wyrażeń, operatorów instrukcji i wartości składających się na kod źródłowy aplikacji w danym języku programowania.
var me = {
age: 34,
name: 'Bartosz'
};
function isPersonOfLegalAge(person) {
if (person.age >= 18) {
return true;
} else {
return false;
}
}
if (isPersonOfLegalAge(me)) {
me = withBeer(me);
} else {
me = withCola(me);
}
me.cheers();
if (isPersonOfLegalAge(me)) {
me = with🍺(me);
} else {
me = with🥤(me);
}'cola'.length // 4
'🥤'.length // 2JS wewnętrznie używa kodowania UTF-16
(chyba że autor silnika zdecyduje inaczej)
'🥤'.split('') // (2) ["�", "�"]Powoduje to kilka problemów:
'👨👩👧👦 '.length; //
[...'👨👩👧👦 ']; //11
"👨", "", "👩", "", "👧", "", "👦", " "
https://twitter.com/wesbos/status/769228067780825088
<body>
<h1>Witaj świecie</h1>
<script>
var wspieramJs = true;
</script>
</body>Osadzanie bezpośrednio w kodzie strony:
<body>
<h1>Witaj świecie</h1>
<script src="./script.js"></script>
</body>Dołączenie zewnętrznego pliku z kodem JavaScript:
<script src="./script.js" async></script>
<script src="./script.js" defer></script>
// komentarz w jednej linijce
/*
komentarz blokowy
*/
/**
* popularna forma komentarza blokowego
*/// <!--
var name = 'Bartosz';
// -->Komentarz dla starych IE, które w innym wypadku wyświetlały kod jako treść strony
Automated Semicolon Insertion
var _name = 'Bartosz';
var _age = 34;
var name = 'Bartosz'
var age = 34var a = 3;
var a
a
=
3var a
a
=
3
// a = 3
var i = 0, j = 0
i
++
j
i //
j //function test() {
return
( 1 + 1 )
}
test(); //
var i = 0
[1,2,3].pop(); //0
1
undefined
call to pop of undefined
poprawne nazewnictwo
zastrzeżone słowa
| break | do | instanceof | typeof |
| case | else | new | var |
| catch | finally | return | void |
| continue | for | switch | while |
| debugger | function | this | with |
| default | if | throw | |
| delete | in | try |
| class | enum | extends | super |
| const | export | import |
| implements | let | private | public | yield |
| interface | package | protected | static |
Struktura obiektu danych informująca interpreter lub kompilator o sposobie w jaki developer zamierza wykorzystać dane (operować na nich), dzięki czemu możliwa jest odpowiednia optymalizacja pamięci, w której dane są przechowywane.
undefined
Domyślna wartość zmiennej, nadawana w momencie kiedy nie podamy żadnej wartości, pole nie istnieje, lub wyraźnie nadamy wartość "undefined"
var age; // undefined
var name = undefined;
var me = {
};
me.name; // undefinedBoolean
Prosty typ danych przyjmujący wartość "true" lub "false".
var isOfLegalAge = false;
var isHappy = true;Number
Wartości numeryczne w JS traktowane są jako liczby zmiennoprzecinkowe oddzielone znakiem kropki.
var age = 34;
var height = 185.5;
height = 186,1; Przecinek jest operatorem w JS! Wynik to 186!
0.1 + 0.2 = ?0.1 + 0.2 = 0.30000000000000004https://0.30000000000000004.com/
// 1
Number - stałe
JS deklaruje także kilka stałych pomocnych przy pracy z wartościami liczbowymi:
0 / 0; // NaN
5 / 0; // Infinity
String
Łańcuchy znaków w JS mogą być zapisane przy użyciu znaku " lub ' - różnicą jest konieczność "escapowania" niektórych znaków specjalnych
var name = 'Bartosz';
var lastName = 'Szczeciński';
var pseudonim = 'BTM';
// var fancyName = "Bartosz "BTM" Szczeciński";
// var fancyName = "Bartosz \"BTM\" Szczeciński";
var fancyName = 'Bartosz "BTM" Szczeciński'
fancyName = name + ' ' + pseudonim + ' ' + lastName;
var multiLine = 'Tekst w wielu linijkach' +
'musi być łączony znakiem +';Null
Wartość logiczna oznaczając brak wartości, ale nie brak danych.
Często używa się odróżnienia "undefined" i "null" (lub 0, false) dla oznaczenia, że dane zostały zweryfikowane (np. pobrane z serwera) ale ich wartość jest nieznana tak, by nie próbować pobrać ich ponownie.
Object
Typ złożony pozwalający na przechowywanie w nim wielu innych typów prostych oraz złożonych.
var person = {
name: 'Bartosz',
age: 34
};
var workshop = {
leadBy: person,
topic: 'Intro to JS',
attendees: 10
};
workshop.attendees; // 10
workshop.leadBy.name; // 'Bartosz'Array
var tablica0 = new Array(1,2,3,4);
var tablica1 = [1,2,3,4];
var tablica2 = [];
tablica2[0] = 1;
tablica2[2] = 3;
tablica2; // [1, , 3]
tablica2.test = 4;
tablica2; // [1, empty, 3, test: 4]Function
Funkcje w JS także są obiektami
dowiemy się o tym więcej później :))
Date
Obiekt Date pozwala na podstawową pracę z datami. Kolejne wersje JS rozszerzają możliwości dodając obiekty daty w danym locale etc.
// poniższe przykłady zakładają datę 7 maja 2018
var date = new Date();
date; // Mon May 7 2018 20:53:52 GMT+0200 (Środkowoeuropejski czas letni)
date.getYear(); //
date.getFullYear(); //
date.getDay(); //
date.getDate(); //
date.getMonth(); // 118
2018
4
1 - poniedziałek
4
Math
Obiekt Math posiada kilka funkcji oraz stałych pomocnych przy pracach ze wzorami i operacjami matematycznymi.
Math.PI; // 3.141592653589793
Math.E; // 2.718281828459045
Math.round(2.65); // 3
Math.floor(2.65); // 2
Math.random(); // 0-1JSON (JavaScript Object Notation) nie jest w prawdzie typem danych, ale jest używany na tyle często, że warto o nim wspomnieć. Notacja JSON pozwala na zmianę typów prostych / obiektów na string i późniejsze odtworzenie.
var person = {
name: 'Bartosz',
age: 34
}
var asJson = JSON.stringify(person); // "{"name":"Bartosz","age":34}"
var person2 = JSON.parse(asJson);Podczas zmiany na JSON usuwane są obiekty, których nie można zserializować.
W przypadku wystąpienia cyklicznych referencji silnik zgłosi błąd.
typeof
typeof true //
typeof 1 //
typeof 'test' //
typeof { name: 'Bartosz' } //
typeof null //
typeof [1,2,3] //
typeof function() {} //
typeof NaN //
typeof "1" + 1 //boolean
number
string
object
object
string1
function
object
number
Do sprawdzania typu danych należy używać operatora "typeof", który zwraca tekstową nazwę typu wartości po prawej stronie operatora.
Przy większości operacji JS jest "sprytny" i postara się samodzielnie określić jaki typ powinny mieć zmienne tak, by operacja "miała sens".
Nie powinniśmy jednak na tym polegać.
var a = 1;
var b = "2";
a + b //
var a = 1;
var b = [1,2,3];
a + b //
1 + {} //
{} + 1 // "12"
"11,2,3"
"1[object Object]"
"1"
// na number
typeof parseInt("1"); // number 1
typeof Number("1"); // number 1
typeof Number("one"); // number NaN
// na string
typeof String(1); // string
typeof ("" + 1); // string "1"
// na boolean
typeof "true"; // string
typeof !"true"; // boolean true
typeof !!"true"; // boolean falsePrzy większości operacji JS jest "sprytny" i postara się samodzielnie określić jaki typ powinny mieć zmienne tak, by operacja miała sens.
Nie powinniśmy jednak na tym polegać.
Element składni języka pozwalający na przypisywanie, modyfikowanie i porównywanie typów danych.
var a = 1;
var a = 1, b = 2, c = 3;var a = 5;
var b = 0;
b += 5; // b = b + 5;
b -= 2; // b = b - 5;
a |= 2; // a = a | 2 = 7W celu porównania dwóch wartości możemy użyć operatorów matematycznych: == <= >= < > !=
2 == 2 //
2 < 2 //
2 > 1 //
1 == "1" //
1 < 2 < 3 //
3 > 2 > 1 //true
false
true
true
true
false
2 == "2"; // true
2 === "2"; // false
2 <== 2 // trueNaN == NaN; //false
Porównywać można tylko typy proste - porównywanie typów złożonych porównuje ich "referencje"
[1, 2, 3] === [1, 2, 3]; // false
{ age: 34 } === { age: 34 }; // false
var array = [1, 2, 3];
var array2 = array;
array === array2; // true
array2 = [1, 2, 3];
array === array2; // falseOperator + w zależności od kontekstu dokonuje operacji dodania lub połączenia wartości tekstowych obiektu.
1 + 2; // 3
1 + "2"; // 12
1 + [1, 2]; // "112"
(new Date()) + 1; "Mon May 10 2018 21:50:49 GMT+0200 (Środkowoeuropejski czas letni)1"JS oferuje oczywiście kilka operatorów logicznych.
var a = 0 || 1; // 1
var a = 0 && 1; // 0
if (condition1 && condition2) {
// oba założenia są prawdziwe
}
if (condition1 || condition2) {
// przynajmniej jedno założenie jest prawdziwe
}ternary operator
Operator potrójny przydatny jest tam, gdzie chcemy wykonać prostą operację logiczną i przypisać jedną z 2 wartości do zmiennej.
var result = true ? "prawda" : "kłamstwo";Oczywiście technicznie można je łączyć w bardziej zaawansowane formy, ale czy na pewno tego chcemy?
var result = test1() && test2() ? true : test3() ? "maybe" : false;Operator przecinka powoduje wykonanie wyrażenia przed przecinkiem a następnie zwrócenie wartości po przecinku (niezależnie od wyniku wyrażenia przed).
Ponieważ zapis ten jest stosowany rzadko i może prowadzić do problemów ze zrozumieniem kodu nie zaleca się go stosować.
var a;
a = 186,1;
a; // 1
function test() {
// usuń dane z bazy
}
a = test(),2;
a; // 2, a dane zostały usunięte ...var a = 3;
var b = 6;
var c = a & b; // AND
/*
0011
0110
----
0010 = 2
*/
var c = a | b; // OR
/*
0011
0110
----
0111 = 7
*/
var c = a | b; // XOR
/*
0011
0110
----
0101 = 5
*/1 << 2
/*
0001
0100 = 4
*/
8 >> 1
/*
1000
0010 = 2
*/eval
Czasem może zachodzić potrzeba wykonania kodu dynamicznego, np. zbudowanego na podstawie danych uzyskanych od klienta. Zwyczajowo używa się wtedy np. skończonych maszyn stanu, ale można też użyć konstruktu języka - eval - pozwalającego na wykonanie dowolnego polecenia i zwrócenie wartości.
var wynik = eval('1 + 2');
wynik; // 3eval
Kod wykonywany w eval ma dostęp do wszystkich zmiennych w swoim zasięgu, tak jakby był zapisany bezpośrednio w kodzie strony. Również tworzone w nim zmienne dodawane są do tego zakresu.
var test = 1;
eval('test++');
test; //2
eval('var mojaZmienna = 3');
mojaZmienna; // 3Element składni instruujący język o sposobie przepływu danych i informujący o operacjach, które należy wykonać.
if / else / else if
Operatory kondycji logicznych if-then pozwalają na sterowanie przepływem aplikacji w oparciu o założenia, które zwracają wartość true lub false
function isOfLegalAge(age) {
if (age < 18)
return 'Przepraszamy, funkcja dostępna tylko dla osób pełnoletnich.';
else
return 'OK'
}if (warunek1)
return 'wynik 1';
else if (warunek2)
return 'wynik 2';
else
return 'wynik3';Istnieje możliwość łączenia kondycji w łańcuchy else-if:
https://github.com/BTMPL/es5-workshop
W przypadku kiedy gałąź kondycji powinna zawierać więcej niż jedno wyrażenie lub deklarację, należy utworzyć blok kodu ograniczony przez znaki { i }
if (warunek1)
return 'wynik 1';
else if (warunek2) {
wykonajInneRzeczy();
return 'wynik 2';
}
else {
return 'wynik3';
}W przypadku gdy warunek logiczny może przyjąć inne wartości niż true / false, zaleca się użycie operatora switch:
switch (dzienTygodnia) {
case 1:
return 'Poniedziałek';
break;
case 2:
return 'Wtorek';
break;
case 3:
return 'Środa';
break;
case 4:
return 'Czwartek';
break;
case 5:
return 'Piątek';
break;
case 6:
return 'Sobota';
break;
case 7:
return 'Niedziela';
break;
}W przypadku braku operatora "break" poszczególne case łączą się:
switch (dzienTygodnia) {
case 1:
console.log('Poniedziałek');
case 2:
console.log('Wtorek');
break;
case 3:
console.log('Środa');
break;
case 4:
console.log('Czwartek');
break;
case 5:
console.log('Piątek');
break;
case 6:
console.log('Sobota');
break;
case 7:
console.log('Niedziela');
break;
}
// dla 1 - Poniedziałek oraz Wtorek W przypadku braku odpowiedniego case, deklarujemy case "default":
switch (dzienTygodnia) {
case 6:
case 7:
return 'WEEKEND!';
default:
return 'Niestety, czas do pracy :(';
}
// dla 6 lub 7 - "WEEKEND"
// dla pozostałych wartości "Niestety, czas do pracy :("for, while, do while, for in
for (var i = 0; i < 10 ; i++) {
console.log(i);
}
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
for (var i = 0, e = 10 ; i < e ; i++) {
console.log(i);
}
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
for (var i = 10, e = 0 ; i > e ; i--) {
console.log(i);
}
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
var array = ['a', 'b', 'c'];
for(var i in array) {
console.log();
}
// 0, 1, 2var a = 0;
while(a < 10) {
console.log(a);
a = a + 1;
}
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
var a = 0;
do {
console.log(a);
a = a + 1;
} while (a < 10);
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10for (var i = 0; i < 10 ; i--) {
console.log(i);
}
// ?Nieskończona pętla!
przerwanie i kontynuowanie
Czasami może zachodzić potrzeba przerwania pętli w wypadku innym niż osiągnięcie jej założeń lub pominięcie jakiejś iteracji. W tym celu dysponujemy słowami kluczowymi break i continue
for(var i = 0 ; i < 10 ; i++) {
if (i % 2 === 1) {
// jeżeli licznik pętli jest nieparzysty, przejdź do kolejnej iteracji
continue;
}
if (obliczWartosc(i) === 5) {
// jeżeli spełniony jest dodatkowy warunek, przerwij całkowicie pętle
break;
}
console.log(i);
}Wyrażenie "with" pozwala na "wyciągnięcie" pól obiektu do wewnętrznego bloku tak, jakby były one zmiennymi. Z uwagi na niejednoznaczne przesłanianie się zmiennych nie zaleca się stosowania tego konstruktu.
var person = {
name: 'Bartosz',
age: 34
};
with (person) {
console.log(name); // 'Bartosz'
console.log(age); // 34
}Zbiór wyrażeń i poleceń połączony o określonej nazwie (lub anonimowy) umożliwiający wielokrotne wywołanie bez potrzeby ponownej deklaracji i posiadający możliwość zwrócenia wyniku do punktu wywołania.
definiowanie
Istnieje kilka notacji deklarowania funkcji, z czego najczęściej spotyka się tylko 2 (o tym co je różni przeczytasz w "hoisting").
function test1() {
return 'funkcja test1';
}
var test2 = function() {
return 'funkcja test2';
}
var test3 = new Function('return test3');Deklaracja funkcji wymaga podania ciała funkcji oraz opcjonalnie słowa kluczowego "function" lub wywołania konstruktora obiektu Function.
Zwyczajowo w celu wywołania funkcji używamy zmiennej, do której została ona przypisana i dodajemy znacznik () (w którym możemy przekazać opcjonalnie argumenty funkcji).
function test1() {
console.log('funkcja test1');
}
test1(); // 'funkcja test1';Funkcja może zwrócić dowolny typ danych poprzedzając go słowem kluczowym return, z zaznaczeniem, że może ona zwrócić tylko raz i zwrócenie natychmiast kończy wykonywanie funkcji. Jeżeli chcesz zwrócić więcej danych, użyj tablicy lub obiektu.
function test1() {
return 42;
}
var wynik = test1();
wynik // '42';Funkcjami czystymi (pure function) nazywamy funkcje, których wartość wyjściowa zależy zawsze tylko i wyłącznie od wartości wejściowej (nie wywołują one żadnych innych funkcji anie nie odczytują zmiennych innych niż argumenty) oraz nie modyfikują one żadnych innych danych w swoim zakresie ani argumentów.
var dwa = 2;
function test1() {
return 40 + dwa; // funkcja nie jest już czysta
}
var wynik = test1();
wynik // '42';
function test2() {
window.usunDaneZBazy(); // funkcja nie jest czysta, "efekt uboczny"
}
test2();
metody vs funkcje
Metodami nazywamy funkcje, które zostały przypisane do obiektu:
var person = {
test: function() {
return 'To jest funkcja "test" w obiekcie "person"
}
};
person.test();call, apply
Funkcje mogą być także wywołane używając metody 'call' lub 'apply' przypisanej do obiektu Function (więcej o ich zastosowaniu dowiesz się w rozdziale dot. Obiektów)
function test1() {
return 'funkcja test1';
}
test1.call(); // 'funkcja test1'
test1.apply(); // 'funkcja test1'Deklarując funkcję można określić ilość i nazwy parametrów, z jakimi może zostać ona wywołana. Jeżeli funkcja deklaruje jakąś zmienną, ale podczas jej wywoływania nie przekażemy wartości, danemu argumentowi nadana zostanie wartość undefined.
function test(argument1, argument2) {
return argument1 / argument2;
}
test(10, 2); // 5
test(10); // 10 / undefined = NaNwartości domyślne
JS w wersji ES5 nie pozwala na definiowanie wartości domyślnych dla zmiennych - w zamian tego można nadać wartości domyślne zmiennym w ciele funkcji.
function test(argument1, argument2) {
argument1 = argument1 || 0;
argument2 = argument2 || 0;
return argument1 / argument2;
}
test(10, 2); // 5
test(10); // 10 / 0 = Infinitynieokreślona ilość argumentów
Jeżeli nie chcemy określać ile argumentów może przyjąć nasza funkcja, możemy skorzystać z niejasnego obiektu arguments, który dostępny jest w ciele funkcji i zawiera tablicę wartości, z jakimi została ona wywołana.
function sum() {
var ret = 0;
for(var i = 0 ; i < arguments.length ; i++) {
ret += arguments[i];
}
return ret;
}
sum(1, 2, 3, 4); // 10funkcje jako argumenty (callback)
W języku JS funkcje traktowane są jako tzw. "obiekty pierwszej klasy" - oznacza to, że mogą być one przekazywane jako argumenty do innych funkcji.
Funkcje te (przekazane jako argument) nazywamy "zwrotnymi" lub "callbackami":
function test() {
return 'to jest funkcja test()';
}
function test2() {
return 'a to jest funkcja test2()';
}
function callMeMaybe(func) {
return func.call();
}
callMeMaybe(test); // 'to jest funkcja test()'
callMeMaybe(test2); // 'a to jest funkcja test2()'Fragment programu w którym widoczne jest, lub do którego dostęp posiada dane wyrażenie lub polecenie.
W ES5 istnieje tylko jeden specyfikator zmiennych - var - użycie go deklaruje funkcję w jednym z dwóch "zasięgów":
var age = 34;
function whatsMyAge() {
console.log(age);
}
whatsMyAge(); // 34
function whatsMyName() {
var name = 'Bartosz';
}
whatsMyName();
console.log(name); // ReferenceErrorWyjątkiem od poprzedniej reguły jest definicja zmiennej w funkcji i przy pominięciu specyfikatora var:
function test() {
name = 'Bartosz';
}
test();
console.log(name); // W takim wypadku zmienna nie zostanie zadeklarowana w zasięgu funkcji, ale zamiast tego zostanie zadeklarowana globalnie - jako window.name
UWAGA: działanie to zmienia się w 'strict-mode'
"Bartosz"
IIFE
Czasami mamy potrzebę wykonać jakieś obliczenia, dla których pomocne było by zadeklarowanie kilku zmiennych, ale nie chcemy "zaśmiecać" globalnego zakresu, ani nie chcemy też definiować funkcji. W takim wypadku możemy użyć tzw. IIFE (immediately-invoked function expression - natychmiastowo wywołane wyrażenie funkcyjne).
var age = (function() {
var rokUrodzenia = 1984;
var rok = (new Date()).getFullYear();
return rok - rokUrodzenia;
})();
console.log(age);shadowing
Ponieważ JS nie posiada specyfikatorów zakresu możliwe jest tzw. przesłonięcie ("shadowing") oraz ich ponowna deklaracja:
var person = {
name: 'Bartosz'
};
function sayHello() {
var person = 5;
return person.name;
}
sayHello(); // undefined
function sayHello(person) {
return person.name;
}
sayHello(); // TypeErrorclosure
Każda funkcja utrzymuje w swoim zasięgu referencje do wszystkich zmiennych, do których miała dostęp w momencie jej deklarowania:
function uniqueIdFactory() {
var id = 0;
return function() {
id++;
return id;
}
}
var next = uniqueIdFactory();
next(); // 1;
next(); // 2
next(); // 3Zaawansowane wykorzystanie closure
for(var i = 0 ; i < 5 ; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}Fakt, że closure przetrzymują referencje, może być czasem nieprzydatny, wyobraźmy sobie poniższy przykład:
Powyższy kod wywoła 5 razy funkcję "setTimeout", która po upływie 100 ms wywoła funkcję, która z kolei wyświetli wartość zmiennej i.
Jakie kolejne wartości pokaże kod?
5, 5, 5, 5, 5Zaawansowane wykorzystanie closure
for(var i = 0 ; i < 5 ; i++) {
(function() {
var j = i;
setTimeout(function() {
console.log(j);
}, 100);
}())
}W celu zmiany tego zachowania możemy połączyć closure i IIFE:
Każda pętle utworzy swój własny anonimowy zakres, w którym utworzy swoją własną kopię zmiennej i jako j i wyświetli ją po 100 ms za pomocą mechanizmu closure.
0, 1, 2, 3, 4"Przeniesienie do góry zasięgu"
Hoisting to mechanizm pozwalający na wywołanie funkcji lub zmiennej zanim zostanie ona zdefiniowana, bez otrzymania komunikatu TypeError
JS wyróżnia dwa oddzielne typy hositowania - dla zmiennych i dla funkcji.
Funkcji
Hoistowanie funkcji przenosi jej definicje i deklarację do początku zasięgu, dzięki czemu możemy bez problemu ja wywołać:
var name = 'Bartek';
powitaj(name); // 'Witaj, Bartek'
function powitaj(kogo) {
console.log('Witaj, ' + kogo);
}Zmiennej
Hoistowanie zmiennej przenosi jej deklarację ale nie definicję. Na skutek tego nie otrzymamy błędu parsera, ale nie będziemy mieć dostępu do wartości zmiennej:
powitaj(name); // 'Witaj, '
var name = 'Bartek';
function powitaj(kogo) {
console.log('Witaj, ' + kogo);
}Funkcji jako zmiennej
Funkcje zdefiniowane bezpośrednio w wyrażeniu nie są hoistowane!
var name = 'Bartek';
powitaj(name);
var powitaj = function(kogo) {
console.log('Witaj, ' + kogo);
}// TypeError - "powitaj" nie jest funkcją! (jest undefined)
var array0 = [1, 2, 3, 4];
var array1 = new Array(1, 2, 3, 4);
var array2 = '1234'.split('');JS oferuje kilka sposobów tworzenia tablic, z czego najpopularniejsze to tzw. literał albo użycie konstruktora obiektu Array.
Niektóre obiekty mogą także definiować metody, pozwalające na przekształcenie ich do tablicy.
var array0 = [1, 2, 3, 4];
array[1]; // "2"
array[5]; // "undefined"Dostęp do danych w tablicy odbywa się przez indeks elementu umieszczony w nawiasach kwadratowych.
Indeksy tablicy numerowane są od zera!
Główny obiekt Array definiuje wiele metod i właściwości pozwalających na pracę z tablicami, kilka wartych uwagi to:
var array = [0, 1, 2];
array.length; // 3
var array2 = array.slice(0, 2); // [0, 1];
var array3 = array2.concat(1); // [0, 1, 1];
var array4 = array3.concat([8, 9]); // [0, 1, 1, 8, 9];
array4.push('a'); // [0, 1, 1, 8, 9, 'a']
array4.indexOf(8); // 3
array4.join('-'); // '0-1-1-8-9-a'
Array.isArray(array4); // trueDodatkowo dostępne jest także wiele metod przydatnych w programowaniu funkcyjnym (FP) tj.: map, forEach, reduce, sort
W poprzednich rozdziałach używaliśmy pętli for lub while w celu wylistowania wartości z tablicy. Częściej wykorzystywanym mechanizmem jest iterowanie z użyciem przygotowanych przez Array metod:
[1, 2, 3, 4].forEach(function(value, index) {
console.log('Na pozycji ' + index + ' znajduje się wartość ' + value);
});
var kwadraty = [1, 2, 3, 4].map(function(value) {
return value * value;
});
kwadraty; // [1, 4, 9, 16]Istnieje kilka mechanizmów usuwania elementów z tablicy, który z nich stosujemy zależ od tego czy posiadamy indeks elementu oraz czy nie przeszkadza nam, że w tablicy mogą powstać dziury:
var array = [1, 2, 3];
delete array[1];
array; // [1, ,3];
var array = [1, 2, 3];
array = array.filter(function(item, index) {
if (index === 1) return false;
return true;
})
array; // [1, 3]bonus: obliczenie kosztu posiłku
literały
Podobnie jak w przypadku tablic istnieje kilka sposobów na tworzenie obiektów. Najczęściej spotykany to literał:
var person = {
name: 'Bartosz',
age: 34,
social: [{
name: 'Twitter',
url: 'https://twitter.com/btmpl'
}, {
name: 'Medium',
url: 'https://medium.com/@baphemot
}]
};operator new
Inną opcją tworzenia obiektów jest wywołanie operatora new oraz funkcji:
function Person() {
this.name = 'Bartosz';
this.age = 34;
}
var me = new Person();
me.name; // 'Bartosz'Ciało takiej funkcji często nazywa się konstruktorem.
właściwości (pola) i metody
Obiekty mogą mieć dowolną ilość właściwości / pól (są to "zmienne obiektu") oraz metod ("funkcje obiektu"). Mogą być one zdefiniowane przy definicji obiektu oraz dodane później:
var person = {
name: 'Bartosz',
age: 34,
getMood: function() {
return 'Nervous';
}
}
person.getMood(); // 'Nervous';
person.getMood = function() {
return 'OK';
}
person.getMood(); // 'OK'dostęp do danych obiektu
Aby uzyskać dostęp do danych obiektu stosujemy notację nazwaObiektu.nazwaWłasności:
var person = {
name: 'Bartosz',
}
person.name; // 'Bartosz';Wynika z tego, że nazwy własności mają takie same ograniczenia, jak nazwy zmiennych ... ?
dostęp "tablicowy"
Można użyć tzw. dostępu tablicowego, by obejść to "ograniczenie":
var person = {
name: 'Bartosz',
}
person['🥤'] = 'Pepsi Max';
person['Miejsce Zamieszkania'] = 'Łódź';
person;
/*
{
name: 'Bartosz',
🥤: 'Pepsi Max',
Miejsce Zamieszkania: 'Łódź'
}
*/dostęp obiektu do samego siebie
Czasami potrzebujemy by obiekty miały możliwość wejrzenia "w siebie" i zwrócenia jakiś danych. W tym przypadku używamy słowa kluczowego this (więcej o this w dalszych rozdziałach).
var person = {
name: 'Bartosz',
getName: function() {
return this.name
}
}
person.getName(); // 'Bartosz'dostęp obiektu do samego siebie
var person = {
name: 'Bartosz',
lastName: 'Szczeciński',
fullName: this.name + ' ' + this.lastName,
getName: function() {
return this.name
}
};
person.fullName; // ?this "znajdowane" jest i przypisywane w momencie wywołania wyrażenia, dlatego powyższy zapis jest niepoprawny. Object Literal nie może używać this bezpośrednio w momencie swojej deklaracji.
W takim wypadku this będzie wskazywał na window lub this funkcji nadrzędnej.
dostęp obiektu do samego siebie
function personFactory() {
this.name = 'Bartosz',
this.lastName = 'Szczeciński',
this.fullName = this.name + ' ' + this.lastName,
this.getName = function() {
return this.name
}
};
var person = new personFactory();
person.fullName; // 'Bartosz Szczeciński'Tego typu dostęp do this możliwy jest natomiast w przypadku funkcji, ponieważ this jest dla nas predefiniowane.
w językach opartych o klasy
class Dog {
legs = 4;
bark() {
return 'Woof!'
}
}
class ChowChow extends Dog {
bark() {
return '';
}
}
class Shibe extends Dog {
legs = 4;
}
var dog = new Dog();
dog.bark(); // "Woof!"
dog.legs; // 4
var dog = new ChowChow();
dog.bark(); // ""
dog.legs; // 4
var dog = new Shibe();
dog.bark(); // "Woof!"
dog.legs; // 4w JS
JavaScript nie posiada klas w rozumieniu C++ czy Java. Pozwala on jednak na tworzenie obiektów z funkcji za pomocą operatora new i oferuje tzw. "dziedziczenie prototypowe".
Każdy obiekt posiada wartość prototype opisującą "szablon" czy "dzielone dane" obiektu.
dziedziczenie prototypowe
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
return 'Woof!';
}
Dog.prototype.getName = function() {
return this.name;
}
var szarik = new Dog('Szarik');
szarik.bark(); // "Woof!"
szarik.getName(); // "Szarik"dziedziczenie prototypowe
function Dog(name) {
this.name = name;
}
var szarik = new Dog('Szarik');
szarik.__proto__.bark = function() {
return 'Woof!';
}
szarik.bark(); // "Woof!"
var azor = new Dog("Azor");
azor.bark(); // "Woof!"Dostęp do prototypu funkcji możliwy jest przez zapis nazwaFunkcji.prototype
Jeżeli dysponujemy zaś obiektem (instancją funkcji) możemy użyć niejawnego pola obiekt.__proto__
dziedziczenie prototypowe
function Dog() {
}
Dog.prototype.bark = function() {
return 'Woof!';
}
Dog.prototype.legs = 4;
function Shiba() {
}
Shiba.prototype = Object.create(Dog.prototype);W celu zaimplementowania dziedziczenia w JS, musimy zatem sprawić, że różne typy obiektów będą posiadały ten sam prototyp.
dziedziczenie prototypowe
var test = function() {
}
typeof test; // "function"
test.__proto__; // f() { [native code] }
typeof test.__proto__.__proto__; // "object"
test.__proto__.__proto__; // { constructor: f, ... }
typeof test.__proto__.__proto__.__proto__; // nulldziedziczenie prototypowe
function test() {
}
var obj1 = new test();
obj1.mojaFunkcja(); // TypeError
Object.prototype.mojaFunkcja = function() {
return 'Hej!';
}
obj1.mojaFunkcja(); // "Hej!";dziedziczenie prototypowe
Właściwości obiektów przeszukiwane są w górę łańcucha prototypów, poczynając od instancji.
Object.prototype.mojaFunkcja = function() {
return 'Jestem w prototypie Object';
}
function test() {
}
test.prototype.mojaFunkcja = function() {
return 'Jestem w prototypie test';
}
var obj1 = new test();
obj1.mojaFunkcja = function() {
return 'Jestem w obiekcie test';
}
obj1.mojaFunkcja();ryzyko definiowania metod prototypowych
Z uwagi na możliwość definiowania włąsnych funkcji prototypowych istnieje ryzyko zdefiniowania funkcji, która zostanie dodan w przyszłej wersji języka i jej nadpisania.
Sytuacja taka nie jest bez precedensu. Popularna biblioteka MooTools definiuje własne implementacje metod Array.flatten i innych, które są niekompatybilne z wytycznymi ECMA Script
https://github.com/tc39/proposal-flatMap/pull/56
weryfikacje instancji i metod
Jeżeli potrzebujemy sprawdzić, czy dany obiekt jest instancją danej funkcji, lub czy posiada on własną implementację danej metody możemy użyć operatora instanceof oraz metody prototypowej hasOwnProperty
function Dog() {
this.bark = function() {
return 'Woof!';
}
}
Dog.prototype.wiggleTail = function() {
this.goodBoy = true;
}
var szarik = new Dog();
szarik instanceof Dog; // true
szarik.hasOwnProperty('bark'); // true
szarik.hasOwnProperty('wiggleTail'); // false
szarik.__proto__.hasOwnProperty('wiggleTail'); // trueDziedziczenie po wielu klasach
JavaScript umożliwia także dziedziczenie po wielu klasach.
Domyślnym mechanizmem jest wciąż dziedziczenie prototypowe na zasadzie:
A dziediczy po B
B dziedziczy po C
D dziedziczy po C
więc D dziedziczy po A
Dziedziczenie po wielu klasach
function Animal() {
}
Animal.prototype.legs = true;
function Dog() {
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.tail = true;
function Shiba() {
}
Shiba.prototype = Object.create(Dog.prototype);
var dog = new Shiba();
dog.tail; // true
dog.legs; // true
dog instanceof Shiba; // true
dog instanceof Dog; // true
dog instanceof Animal; // true
var animal = new Animal();
animal.legs; // true
animal.tail; // falseDziedziczenie po wielu klasach
Drugim sposobem jest dziediczenie po wielu klasach poprzez połączenie ich prototypów. Wciąż daje nam to dostęp do wszystkich właściwości klas "nadrzędnych" ale łamie polimorfizm.
Dziedziczenie po wielu klasach
function Animal() {
}
Animal.prototype.legs = true;
function Dog() {
}
Dog.prototype.tail = true;
function Shiba() {
}
Object.assign(Shiba.prototype, Dog.prototype, Animal.prototype);
var dog = new Shiba();
dog.tail; // true
dog.legs; // true
dog instanceof Shiba; // true
dog instanceof Dog; // false
dog instanceof Animal; // false
JavaScript (ES5) pozwala na wielokrotne definiowanie tej samej zmiennej i jednoczesną zmianę typu danych:
var name = 'Bartek';
var name = 42;
var name = {
value: 'Bartek'
};Działanie takie jest jednak niezalecane ponieważ wprowadza one zamieszanie i utrudnia czytanie kodu aplikacji.
Mutowaniem nazywamy zmianę wartości obiektu lub tablicy bez ponownego przypisania całej zmiennej:
var person = {
name: 'Bartek'
};
person.name = 'Bartosz';
person.name; // BartoszDziałanie takie często może być niepożądane ponieważ może prowadzić do trudnych do wychwycenia błędów:
var person = {
name: 'Bartek'
};
function sayName(ofPerson) {
console.log(ofPerson.name);
// DEBUG: czemu to nie działa ?!?!?
// muszę sprawdzić jutro po pracy (i oczywiście nie usuwamy kodu - nigdy ...)
ofPerson.name = 'test';
}
sayName(person); // 'Bartek';
person.name; // 'test';zapobieganie mutowaniu
Aby zabezpieczyć się przed takimi sytuacjami możemy użyć funkcji Object.freeze()
var person = {
name: 'Bartek'
};
Object.freeze(person);
function sayName(ofPerson) {
console.log(ofPerson.name);
// DEBUG: czemu to nie działa ?!?!?
// muszę sprawdzić jutro po pracy (i oczywiście nie usuwamy kodu - nigdy ...)
ofPerson.name = 'test'; // operacja została "po cichu" zignorowana
}
sayName(person); // 'Bartek';
person.name; // 'Bartek';Object.freeze
Należy mieć na uwadze, że Object.freeze powoduje tzw. "płytką niemutowalność". Właściwości-właściwości obiektu wciąż można zmieniać!
var person = {
name: 'Bartek',
data: {
age: 34
}
};
Object.freeze(person);
person.name = 'Test';
person.name; // 'Bartek';
person.data.age = 18;
person.data.age; // "18"Czym jest this i na co pozwala?
Jak już widzieliśmy wcześniej, każdy obiekt posiada w sobie niejawny obiekt this. Reprezentuje on "ten" obiekt.
Zakres globalny również posiada swój "this" - jest nim sam obiekt zakresu globalnego, np. "window".
"this" określane jest w momencie wywołania kodu.
var name = 'Bartosz';
var person = {
name: this.name,
age: 34,
getAge: function() {
return this.age;
}
}name; // Bartosz
person.name; // 'Bartosz'
age; // ReferenceError
person.getAge(); // 34;
Jak wskazać wartość "this"
Domyślnie, wartość "this" jest "obiektem znajdującym się po lewo od kropki".
var person = {
name: 'Bartosz',
getName: function() {
return this.name;
}
}
person.getName();function Dog() { this.sound = 'Woof!'; }
function Cat() { this.sound = 'Meow!'; }
function makeSound() {
return this.sound;
}
Dog.prototype.makeSound = makeSound;
Cat.prototype.makeSound = makeSound;var obj1 = new Dog();
var obj2 = new Cat();
obj1.makeSound(); // "Woof!"
obj2.makeSound(); // "Meow!"Jak wskazać wartość "this"
Jeżeli skopiowaliśmy referencję metody do zmiennej i wywołujemy ją bezpośrednio, this przyjmuje wartość this dla kontekstu wywołania ...
var person = {
name: 'Bartosz',
getName: function() {
return this.name;
}
}
var getName = person.getName;
getName(); // undefined
window.getName();Jak zmienić wartość "this": call, apply
Czasem zachodzi potrzeba wywołania funkcji, która operuje na "this", ale z innym obiektem podstawionym jako "this". Najczęściej przydatne jest to w przypadku callbacków komunikujących się miedzy obiektami lub kontekstami wykonania kodu.
W tym celu JS udostępnia 2 mechanizmy: call oraz apply. Pozwalają one na wywołanie funkcji ze wskazaną wartością jako "this" i przekazanie argumentów po przecinku (Comma) lub jako tablica (Array).
Jak zmienić wartość "this": call, apply
var person = {
name: 'Bartosz',
getGreeting: function(typPowitania) {
return typPowitania + ', ' + this.name + ' - jak się masz?'
}
};
person.getGreeting('Witaj'); // 'Witaj, Bartosz - jak się masz?'
person.getGreeting.call({
name: 'Bartek'
}, 'Dzień dobry'); // 'Dzień dobry, Bartek - jak się masz?'wykorzystanie apply przy dziedziczeniu
Jeżeli klasa nadrzędna wykonuje operacje w swoim konstruktorze, utworzenie klasy pochodnej nie wywoła tych operacji - w tym wypadku to developer zmuszony jest ręcznie wywołać konstruktor klasy nadrzędnej.
function Animal(name) {
this.name = name;
this.sound = '???';
this.makeSound = function() {
return this.name + ': ' + this.sound;
}
}
function Dog(name) {
this.sound = 'Woof!';
}
Dog.prototype = Animal.prototype;
var obj1 = new Dog('Szarik');
obj1.makeSound(); // TypeErrorfunction Animal(name) {
this.name = name;
this.sound = '???';
this.makeSound = function() {
return this.name + ': ' + this.sound;
}
}
function Dog(name) {
Animal.call(this, name);
// Animal.apply(this, arguments);
this.sound = 'Woof!';
}
Dog.prototype = Animal.prototype;
var obj1 = new Dog('Szarik');
obj1.makeSound(); // 'Szarik: Woof!'Jak utrwalić wartość "this".
Często zachodzi potrzeba wywoływania funkcji z ustalonym this więcej niż jednokrotnie, lub zachodzi podejrzenie, że przekazują funkcję i wywołując ją "później" możemy zgubić oryginalną intencję wartości "this".
Pomówmy trochę o "pętli zdarzeń" i "stosie" w JS.
Jak zmienić wartość "this": bind
JS udostępnia mechanizm "przywiązywania" (bind) this do funkcji.
function Animal(name) {
this.name = name;
this.sound = '???';
this.makeSound = function() {
console.log(this.name + ': ' + this.sound);
}
}
var obj1 = new Animal('test');
obj1.makeSound(); // 'test: ???'
setTimeout(obj1.makeSound, 1000); // 'undefined: undefined'function Animal(name) {
this.name = name;
this.sound = '???';
this.makeSound = function() {
console.log(this.name + ': ' + this.sound);
}
}
var obj1 = new Animal('test');
obj1.makeSound(); // 'test: ???'
setTimeout(obj1.makeSound.bind(obj1), 1000); // 'test: ???'Dodatkowe zastosowania bind
bind używany jest także do tzw częściowej aplikacji funkcji.
function mnoz(a, b) {
return a * b;
}
mnoz(2, 5); // 10
mnoz(5, 5); // 25
var mnozRazy10 = mnoz.bind(this, 10);
mnozRazy10(2); // 20
mnozRazy10(9); // 90Niektórzy ludzie, kiedy staną w obliczu problemu pomyślą: "Wiem, użyję wyrażeń regularnych."
Teraz mają już dwa problemy
- Jamie Zawinski
wg. Wikipedii
(ang. regular expressions, w skrócie regex lub regexp)
wzorce, które opisują łańcuchy symboli.
Wyrażenie regularne pozwalają na zdefiniowanie wzorca, który zostanie użyty na stringu, dzięki czemu możemy zweryfikować czy spełnia on założenia wzorca ("pasuje do") i dodatkowo uzyskać wszystkie jego elementy pasujące do tzw. "pułapek" we wzorcu.
Wyrażenie regularne najczęściej wykorzystuje się do weryfikacji (czy podany ciąg znaków to telefon, email etc.) oraz do "wyciągnięcia" danych ze stringu.
weryfikacja e-mail
'baphemot@gmail.com'.toLowerCase().match(new RegExp(/([a-z0-9]+)@([a-z]+).([a-z]+)/))
/*
Array [
"baphemot@gmail.com",
"baphemot",
"gmail",
"com",
index: 0,
input: "baphemot@gmail.com",
groups: undefined
]
*/
((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b
\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9]
(?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]
|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:
[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])Emaile są jednak trochę bardziej skomplikowane
Throw i Error
W JS występuje wiele typów błędów, większość z nich nie jest błędami krytycznymi.
Error; // podstawowy typ, po którym dziedziczą wszystkie błędy
new Array(-1); // RangeError
var name = 'Bartosz';
console.log(naem); // ReferenceError
while (true === true)
console.log('Wszystko wygląda OK ...');
} // SyntaxError
undefined.toString(); // TypeError
decodeURI('http://example.com/%1/test'); // URIErrorThrow i Error
Użytkownik może samemu wywołać błąd.
new SyntaxError('W naszym kodzie nie pozwalamy na używanie znaków } !');Oczywiście możemy tworzyć nowe typy błędów korzystając z OOP.
function CustomError(foo, message, fileName, lineNumber) {
Error.apply(this, arguments);
this.cosWlasnego = 42;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
}
}
CustomError.prototype = Error.prototype;try / catch / finally
W naszej aplikacji będą występować błędy.
function roznicaWiekuOsob(osoba1, osoba2) {
if (osoba1.personalData.age > osoba2.personalData.age)
return osoba1.personalData.age - osoba2.personalData.age;
else
return osoba2.personalData.age - osoba1.personalData.age;
}
var me = {
personalData: {
age: 34
}
};
var barackObama = {
personalData: {
age: 61
}
}
roznicaWiekuOsob(me, barackObama);
roznicaWiekuOsob(30, 15);try / catch / finally
Możemy albo przygotować się na wszystkie możliwości:
function roznicaWiekuOsob(osoba1, osoba2) {
if (!osoba1 || !osoba1.personalData) return undefined;
if (!osoba2 || !osoba2.personalData) return undefined;
if (osoba1.personalData.age > osoba2.personalData.age)
return osoba1.personalData.age - osoba2.personalData.age;
else
return osoba2.personalData.age - osoba1.personalData.age;
}
try / catch / finally
try {
// spróbuj
}
catch (err) {
// ... złap błąd ...
}
finally {
// ostatecznie ...
}Ewentualnie możemy użyć notacji try-catch:
try / catch / finally
function roznicaWiekuOsob(osoba1, osoba2) {
try {
if (osoba1.personalData.age > osoba2.personalData.age)
return osoba1.personalData.age - osoba2.personalData.age;
else
return osoba2.personalData.age - osoba1.personalData.age;
}
catch (err) {
return undefined;
}
}try / catch / finally
function roznicaWiekuOsob(osoba1, osoba2) {
try {
if (osoba1.personalData.age > osoba2.personalData.age)
return osoba1.personalData.age - osoba2.personalData.age;
else
return osoba2.personalData.age - osoba1.personalData.age;
}
catch (err) {
if (err instanceof TypeError) {
console.warn('Proszę przekazać obiekty!');
return
}
throw err;
}
}Niestety, JS nie pozwala na zdefiniowanie typu błędu do złapania - łapane są wszystkie typy. Możemy użyć detekcji typów by obsłużyć tylko niektóre z nich.
console
Narzędziem "najniższego poziomu" jeżeli chodzi o debugowanie aplikacji jest obiekt console pozwalający na wyświetlanie informacji w konsoli przeglądarki.
(Nie jest to funkcjonalność JS!)
console
var person = {
name: 'test'
}
console.log(person);
person.name = 'Bartek';stack trace
function fun1() {
fun2();
}
function fun2() {
fun3();
}
function fun3() {
return a + 5;
}
fun1();W przypadku nieobsłużonego wyjątku (lub gdy wywołany zostanie ręcznie - np. console.trace) zwrócony zostaje tzw. "stack tracwe" czyli lista funkcji, których wywołanie doprowadziło do wystąpienia błędu:
breakpoint / debugger
JS dostarcza nam słowo kluczowe debugger, wywołanie którego spowoduje wywołanie debuggera aktualnie używanego silnika (np. w przeglądarce):
Wiele przeglądarek oferuje także opcję "break on exception"
Czemu?
JS zawiera wiele "błędów" i "dziwactw".
Gdyby autorzy chcieli je usunąć, mogli by popsuć kod, który na nich polega.
W celu "ulepszenia" JS wprowadzono tzw. "strict mode", który zmienia działanie niektórych funkcji języka i w ES5 działa on w trybie "opt-in"
Jak aktywować
'use strict'
var name = 'Bartosz';
age = 34;function test() {
'use strict'
console.log(this);
}
test();dla całego pliku
dla pojedynczej funkcji
(Najważniejsze) różnice
'use strict'
name = 'Bartosz'; // ReferenceErrorname = 'Bartosz'; // window.name
name; // "Bartosz"010; // 8'use strict'
010; // SyntaxError
function Person(name) {
this.name = name;
}
Person('test'); // OK!
var obj = new Person('test'); // OK!'use strict'
function Person(name) {
this.name = name;
}
Person('test'); // TypeError
var obj = new Person('test'); // OK!Jednym z najważniejszych Web API jakie udostępniają przeglądarki jest DOM - Document Object Model - pozwalające na bezpośrednią interakcję (odczytywanie i modyfikowanie) drzewiastej struktury dokumentu HTML.
To dzięki temu możemy tworzyć interaktywne strony internetowe, oraz korzystać z bibliotek i frameworków jak React, Angular etc.
<!doctype html>
<html>
<head>
<title>Moja strona internetowa</title>
<meta charset="utf-8" />
</head>
<body>
<h1>
Witam na mojej stronie internetowej!
</h1>
<p>
Znajdziesz tutaj wiele informacji na tematy takie jak:
</p>
<ul>
<li>informatyka</li>
<li>filmy</li>
<li>muzyka</li>
<li>i wiele innych!</li>
</ul>
<p id="link"><a href="#">więcej o mnie</a></p>
<form action="?" method="post">
<input type="text" name="email" value="Twój adres" />
<input type="submit" value="Zapisz się na newsletter" />
</form>
<script src="./script.js"></script>
</body>
</html>Interakcja ze strukturą dokumentu
Głównym interfejsem do DOM jest obiekt document, który udostępnia wiele metod pozwalających na interakcję ze strukturą dokumentu - zarówno odczytywanie jak
i modyfikację.
Każde zapytanie wyszukiwania powinno rozpocząć się od document (lub od już wcześniej uzyskanego elementu).
Domyślnie otrzymujemy 3 "skrótowe" elementy:
document.documentElement; // <html>
document.head; // <head>
dobument.body; // <body>odczytywanie struktury
Najczęściej stosowana operacja to odczytywanie struktury.
W tym celu obiekt document udostępnia nam kilka metod pozwalających na wyszukanie elementów w oparciu o różne kryteria:
document.getElementsByTagName
document.getElementById
document.getElementsByClassName
document.querySelector / querySelectorAll
odczytywanie struktury
// "skrócony" dostęp do elementu <head>
document.head;
// odnajdź element o atrybucie id z wartością "link"
document.getElementById('link');
// znajdź *pierwszy* element <a> osadzony w elemencie <p>
document.querySelector('p a');
// znajdź wszystkie elementy <p> i zwróć ich kolekcję (prawie Array!)
document.querySelectorAll('p')
// odnajdź elementy <a> osadzone w elemencie o id="link"
document.getElementById('link').querySelectorAll('a');relacje między węzłami
Mając już odnaleziony węzeł, możemy przemierzać drzewo relatywnie wzg. naszego węzła:
var link = document.getElementById('link');
link.parentNode; // <body>
link.children[0]; // <a href="#">więcej o mnie</a>
link.firstChild; // <a href="#">więcej o mnie</a>
document.querySelector('ul').firstElementChild; // <li>informatyka</li>
document.querySelectorAll('li')[1].nextElementSibling; // <li>muzyka</li>
relacje między węzłami
różnica między node a element
#text
interakcja z elementem - odczytywanie danych
Po uzyskaniu dostępu do znacznika możemy (w zależności od jego typu) wchodzić w interakcję z jego atrybutami i metodami.
W celu weryfikacji typu węzła możemy użyć
atrybutu .nodeType i .nodeName
var p = document.querySelector('p#link');
p.nodeName; // "P"
p.nodeType; // "1"
/*
1 = element HTML - <p>
3 = tekst - #text
4 = komentarz - <!-- komentarz -->
9 = dokument - #document
10 = dokument definicji - <!doctype html>
*/interakcja z elementem - odczytywanie danych
Każdy znacznik HTML posiada własną, unikalną i rozbudowaną reprezentację w postaci obiektu danej klasy:
var input = document.querySelector('input');
input; // <input type="text" name="email" value="Twój adres" />
input.constructor.name; // "HTMLInputElement"
input.value; // "Twój adres"
input.getAttribute('value'); // "Twój adres"var p = document.querySelector('p#link');
p; // <p id="link"><a href="#">więcej o mnie</a></p>
p.constructor.name; // "HTMLParagraphElement"
p.innerHTML; // '<a href="#">więcej o mnie</a>'interakcja z elementem - zapisywanie danych
Większość atrybutów można zmienić bezpośrednio poprzez interakcję z węzłem:
var input = document.querySelector('input');
input; // <input type="text" name="email" value="Twój adres" />
input.value; // "Twój adres"
input.value = 'Proszę, podaj swój adres e-mail';
input.value; // "Proszę, podaj swój adres e-mail";Niektóre można zmienić także lub tylko używając wyspecjalizowanych metod:
var input = document.querySelector('input');
input; // <input type="text" name="email" value="Twój adres" />
input.value; // "Twój adres"
input.setAttribute('value', 'Proszę, podaj swój adres e-mail');
input.value; // "Proszę, podaj swój adres e-mail";Link "więcej o mnie" nie prowadzi nigdzie. Zmień go używając JS tak, by prowadził on do Twojej strony (FB, Linkedin, blog etc.)
tworzenie nowych węzłów
DOM pozwala na zmianę zawartości węzła używając .innerHTML:
var element = document.querySelector('ul');
element.innerHTML = element.innerHTML + '<li>test</li>'Rozwiązanie takie jednak nie powinno być używane do dynamicznego tworzenia węzłów. Przydaje się ono np. w przypadku, kiedy mamy fragment HTML jako string (np. z API) i musimy dodać go do strony, ale nie zamierzamy go parsować i ręcznie budować struktury.
używając JS, dodaj kilka tematów do listy
document.createElement
Jeżeli mamy listę informacji, które chcemy dodać do strony (np. tablica obiektów) a nie string, powinniśmy użyć document.createElement
var li = document.createElement('li');
li.innerText = 'nowe informacje już niebawem!';
document.querySelector('ul').appendChild(li);document.createElement;
document.createTextNode;
document.createComment;element.appendChild
element.insertBefore(otherChild);
element.replaceChild(otherChild);
element.removeChild(childToRemove);
element.remove();Używając JS, dodaj do listy kilka tematów, ale niech
"i wiele innych!" będzie na końcu.
Nie używaj .innerHTML - usuń tamtą implementację :)
manipulowanie klasami CSS
Klasy CSS można modyfikować używając setAttribute/getAttribute, ale ponieważ operacja ta jest bardzo częsta, dysponujemy obiektem .classList który ułatwia te modyfikację:
var el = document.querySelector('.main'); // <li class="main">informatyka</a>
el.classList.remove('main');
el.nextElementSibling.classList.add('main');.add
.remove
.toggle
.contains
O window mówiliśmy już w sekcji o ES5 i ustaliliśmy, że jest to obiekt globalnego zasięgu, w którym przechowywane są różne zmienne.
window jest obiektem definiowanym przez przeglądarkę i poza przetrzymywaniem zmiennych udostępnia on nam dostęp do wielu funkcji okna przeglądarki tj. nawigacja, otwieranie i zamykanie kart, komunikacja z użytkownikiem etc. oraz do wielu dodatkowych Web API.
Jako zakres globalny window posiada też referencję do siebie samego.
window.window
window.window.window
window.window.window.window
window.window.window.window.
Komunikacja z użytkownikiem
Podstawowe metody komunikacji z użytkownikiem to alert, prompt i confirm.
alert('Witaj na mojej stronie!');
var imie = prompt('Podaj swoje imię', 'Wartość domyślna');
var wynik = confirm('Czy na pewno chcesz usunąć element?');Po wejściu na stronę zapytaj użytkownika o jego imię i wyświetl je na stronie w formie "Witaj, $IMIE na mojej stronie internetowej!".
Jeżeli użytkownik nie podał imienia, pokaż "Witaj na mojej stronie internetowej!" (bez przecinka).
Manipulowanie adresem strony
Obiekt window.location pozwala nam na odczytanie i kontrolowanie "adresu URL" z danego okna:
window.location; // { replace: f, assign: f, href: "file:///Users/Bartek ...."
window.location.reload();
window.location.href = window.location.href;
window.location.href = 'https://www.google.com';
window.location.hash = '#Fragment';Zarządzanie oknem
JavaScript może otworzyć nowe okno / zakładkę, oraz może zamknąć okno które sam otworzył.
var win = window.open('http://example.com');
win.close();Funkcja .open przyjmuje 3 parametry:
Zarządzanie oknem
window udostępnia nam też wiele danych o rozmiarze i położeniu okna, oraz pozwala nimi manipulować.
window.innerHeight - wysokość przeznaczona dla zawartości
window.innerWidth - szerokość
window.scrollTo(x, y) - pozwala na przewinięcie strony
window.print() - wyświetl interfejs drukowania strony
window.postMessage() - pozwala na komunikowanie się pomiędzy stronami / oknami w ramach jednej sesji przeglądarki
Zdarzenia odłożone w czasie
Innym istotnym API jest setTimeout i setInterval pozwalające nam na zdefiniowanie funkcji, która zostanie wywołana "później" lub "co X milisekund"
function przypomnijSie() {
alert('Hej, jesteś na mojej stronie już 5 sekund!');
}
setTimeout(przypomnijSie, 5000);function przypomnijSie() {
alert('Hej, jesteś na mojej stronie już 5 sekund!');
}
var timeoutId = setTimeout(przypomnijSie, 5000);
clearTiemout(timeoutId);Dobrym zwyczajem jest po sobie "posprzątać" i usunąć wszelkie timeouty, które jeszcze się nie wywołały:
Zdarzenia powtarzające się
setInterval pozwala nam na zdefiniowanie zadań, które wywołają się co określony interwał czasowy.
Możemy dzięki temu cyklicznie aktualizować UI lub sprawdzać, czy użytkownik wykonał już daną operację.
var countdown = 10;
var intervalId = setInterval(bomba, 1000);
function bomba() {
countdown--;
if (countdown === 0) {
alert('BOOM!');
clearInterval(intervalId);
}
console.log(countdown + ' ...');
}Dodaj na stronie zegarek, wskazujący aktualną godzinę, minuty i sekundy. Zegarek powinien aktualizować się co sekundę.
Dane możesz pobrać z obiektu Date; w jaki sposób będziesz je aktualizował zależy od Ciebie.
Zegar powinien mieć formę:
HH:MM:SS
Przeglądarki standardowo udostępniają dwa magazyny danych:
localStorage pozwalający na przechowywanie wartości tekstowych aż do czasu ich usunięcia przez użytkownika
sessionStorage podobnie jak localStorage, jednak wartości przechowywane są do końca sesji przeglądarki.
Oba dziedziczą po interfejsie Storage.
Obiekty typu storage dysponują prostym api:
Storage.length - zwraca ilość obiektów aktualnie przechowywanych
Storage.setItem(key, value) - przypisuje string (value) do danego klucza
Storage.getItem(key) - zwraca string przypisany do danego klucza
Storage.removeItem(key, value) - usuwa wpis o podanym kluczu
Storage.clear() - czyści storage
Storage.key(index) - zwraca nazwę n-tego klucza obiektu
localStorage.setItem('name', 'Bartek');
var person = {
name: 'Barosz',
lastName: 'Szczeciński'
};
localStorage.setItem('person', person); //
localStorage.setItem('person', JSON.stringify(person));
var otherPerson = JSON.parse(localStorage.getItem('person'));Nie ma błędu - "[object Object]"
pobieranie wszystkich danych
for(var i = 0 ; i < localStorage.length ; i++) {
var key = localStorage.key(i);
var value = localStorage.getItem(key);
console.log(value);
}
// albo
var keys = Object.keys(localStorage);
keys.forEach(function(key) {
var value = localStorage.getItem(key);
console.log(value);
});Nasza strona pyta użytkownika za każdym razem o imię (chyba, że już to was denerwowało i wyłączyliście ;)).
Zadbajmy o to, żeby nasza strona pytała użytkownika o imię tylko raz.
Zdarzenia (Events) to czynności, które użytkownik wykonuje podczas interakcji ze stroną, które "emitowane" są przez poszczególne jej elementy i mogą zostać "obsłużone" przez JS.
Zdarzenia opisują także mechanizmy zachodzące w samej przeglądarce (zmiana stanu dokumentu, zakończenie pobierania zasobów, komunikat z innej strony etc.)
Każdy "handler" wywoływany jest z obiektem danego zdarzenia, zawierającym jego meta-informacje.
dodawanie zdarzeń
Z historycznych względów możliwe jest dodanie handlera do elementu używając atrybutu on*eventName* i zapisanie w nim kodu, jaki powinien zostać wykonany:
<a href="#" onClick="alert('Hej!')">Ten sposób jest ograniczony i wstawianie większego kodu wiąże się z koniecznością tworzenia wielu "zbędnych" funkcji. Również modyfikowanie tak dodanych zdarzeń jest utrudnione.
dodawanie zdarzeń
Ewolucją poprzedniego kodu jest poniższa notacja, niestety ona również jest ograniczona pod niektórymi względami:
function hej() {
alert('Heeej!');
}
document.querySelector('p#link a').onclick = hej;interakcja między sposobami przypisania
Użycie właściwości onclick na elemencie, który miał już dodany atrybut onclick w dowolny inny sposób usuwa poprzednio dopisany handler:
<a href="#" onClick="alert('Witaj!')">
// ...
function hej() {
alert('Heeej!');
}
document.querySelector('p#link a').onclick = hej;Po kliknięciu linku wyświetli się tyko komunikat "Heeej!"
dodawanie zdarzeń
"Poprawnym" sposobem dodani obsługi zdarzenia jest użycie na elemencie metody addEventHandler, podać typ zdarzenia jakie chcemy obsłużyć oraz przekazać referencję na funkcje, która powinna zostać wywołana:
document.querySelector('p#link a').addEventListener('click', function() {
alert('Papa!');
});function pozegnaj() {
alert('Papa!');
}
document.querySelector('p#link a').addEventListener('click', pozegnaj);function pozegnaj() {
alert('Papa!');
}
document.querySelector('p#link a').addEventListener('click', pozegnaj()); // ŹLE!usuwanie zdarzeń
W celu usunięcia nasłuchiwania na zdarzenie, musimy użyć metody removeEventListener, podać typ zdarzenia oraz referencję na już dodany "handler":
function pozegnaj() {
alert('Papa!');
}
document.querySelector('p#link a').addEventListener('click', pozegnaj);document.querySelector('p#link a').removeEventListener('click', pozegnaj);document.querySelector('p#link a').addEventListener('click', function() {
alert('Papa!');
});
// NIE DA SIĘ USUNĄĆ!Dodaj zdarzenie do guzika wysłania formularza, tak by po kliknięciu pobierana była wartość pola tekstowego.
Następnie zweryfikuj czy w wartości znajduje się znak @.
Jeżeli tak, pokaż podziękowanie.
Jeżeli nie, pokaż komunikat błędu.
Parametry funkcji
Każdy "handler" wywoływany jest z obiektem danego zdarzenia, zawierającym jego meta-informacje.
document.querySelector('input[type="text"]').addEventListener('click', function(ev) {
console.log(ev)
});
https://developer.mozilla.org/en-US/docs/Web/API
Parametry funkcji
Każdy handler wywoływany jest z this które wskazuje na obiekt obsługujący zdarzenie.
var person = {
getName: function() { console.log(this); }
}
document.querySelector('form').addEventListener('click', person.getName);
Parametry funkcji
Aby upewnić się, że this jest "właściwe" i wskazuje na obiekt "person", możemy użyć .bind
var person = {
getName: function() { console.log(this); }
}
document.querySelector('form').addEventListener('click', person.getName);
"bubbling"
function handleClick(event) {
console.log(event.target, event.currentTarget, event.target === event.currentTarget);
}
document.querySelector('ul li:first-child').addEventListener('click', handleClick);
document.querySelector('ul').addEventListener('click', handleClick);
document.querySelector('body').addEventListener('click', handleClick);"bubbling"
Jeżeli jakikolwiek z rodziców klikniętego elementu również posiada dodany handler dla danego typu elementów, domyślnie zostanie on również wywołany.
Zjawisko to nazywane jest "bubblingiem" - kolejność wywołania handlerów jest "do góry" od faktycznie klikniętego elementu.
Obiekt event posiada atrybut target
i currenTarget pozwalający nam określić, czy wywołany handler dotyczy zdarzenia aktualnie obsługiwanego elementu.
"bubbling"
Zachowanie to może zostać powstrzymane przez wywołanie metody stopPropagation na obiekcie event.
function handleClick(e) {
e.stopPropagation()
console.log(event.target, event.currentTarget, event.target === event.currentTarget);
}
document.querySelector('ul li:first-child').addEventListener('click', handleClick);
document.querySelector('ul').addEventListener('click', handleClick);
document.querySelector('body').addEventListener('click', handleClick);"capture phase"
Na podstawie https://javascript.info/bubbling-and-capturing
Powstrzymywanie pozostałych zdarzeń.
Jeżeli na elemencie dodanych jest wiele zdarzeń, wywoływane są one w kolejności w jakiej zostały dodane (i w odpowiedniej fazie). Jeżeli chcemy nie tylko powstrzymać zdarzenie przed propagacją "wyżej" ale także zatrzymać wszystkie inne zdarzenia na danym obiekcie możesz użyć metody
.stopImmediatePropagation
document.querySelector('input[type="submit"]').addEventListener('click', function(event) {
event.stopImmediatePropagation();
// ....
});Powstrzymywanie domyślnych zachowań.
Zapewne zauważyłeś, że nawet kiedy podamy błędny email i zobaczymy komunikat błędu, formularz jest i tak wysyłany? Dzieje się tak ponieważ nasz element submit ma też swoje domyślne zachowanie - wysyła formularz w którym jest osadzony. Aby to zmienić używamy metody preventDefault.
document.querySelector('input[type="submit"]').addEventListener('click', function(event) {
event.preventDefault();
// ....
});istotne zdarzenia
document
- DOMContentLoaded - wywoływany po wczytaniu całej zawartości strony
element
- MouseEvent: click, dblclick, contextmenu, mouseover, mousemove, wheel
- KeyboardEvent: keydown, keyup, keypress
- TouchEvent: touchstart, touchend, touchcancel, tuochmove
- UIEvent: scroll, load, unload
- FocusEvent: blur, focus (te zdarzenia pomijają bubbling!)
- Event: submit, reset
https://developer.mozilla.org/en-US/docs/Web/Events
AJAX nie jest technologią. AJAX to nie biblioteka. AJAX to nie funkcja.
AJAX to idea połączenia kilku technologii, umożliwiająca nowe sposoby interakcji ze stronami internetowymi poprzez ulepszona komunikację z serwerem.
Asynchronous JavaScript And XML
Na szczęście to ostatnie głównie z nazwy.
Typowa sesja HTTP
Minusy
Implementacja
XMLHttpResponse
var xml = new XMLHttpRequest();
xml.open('GET', 'https://jsonplaceholder.typicode.com/posts');
xml.onreadystatechange = function() {
if(xml.readyState == 4) {
if(xml.status === 200 || xml.status === 304) {
console.log(xml.responseText);
}
else {
console.log('Wystąpił błąd: ', xml.status);
}
}
}
xml.send();XMLHttpResponse
.open(metoda, url, async, user, password) - przygotowuje połączenie
.send(content) - wysyła żądanie, z opcjonalnym ciałem (dla POST, PATCH, PUT)
.abort() - zatrzymuje trwające żądanie
.setRequestHeader(header, value) - pozwala na ustawienie nagłówków żądania
.getResponseHeader(header) - pobiera dany nagłówek odpowiedzi
.getAllResponseHeaders() - *string* z wszystkich nagłówków odpowiedzi
XMLHttpResponse
onreadystatechange - zdarzenie uruchamiane przy zmianie stanu połączenia
readyState - aktualny stan połączenia
responseText, responseXML - odpowiedź w odpowiednim formacie
status - kod HTTP odpowiedzi
statusText - "czytelny dla człowieka" status odpowiedzi (np. "Not Changed")
XMLHttpResponse
function get(url, success, failure) {
var xml = new XMLHttpRequest();
xml.open('GET', url);
xml.onreadystatechange = function() {
if(xml.readyState == 4) {
if(xml.status === 200 || xml.status === 304) {
success.call(null, xml.responseText);
}
else {
failure.call(null, xml.status, 'Wystąpił błąd');
}
}
}
xml.send();
}
get('https://jsonplaceholder.typicode.com/posts', function(responseBody) {
console.log(responseBody);
}, function(errCode, errInfo) {
console.log(errCode);
});Pobierz listę postów z adresu http://vps88089.ovh.net:3000/posts i dodaj
5 z nich jako lista uporządkowana do strony blog.html
Utwórz (używając JS) strukturę:
<h4>Na blogu:</h4>
<ul>
<li>wartość "title" pierwszego wyniku</li>
<li>wartość "title" drugiego wyniku</li>
<li>wartość "title" trzeciego wyniku</li>
<li>wartość "title" czwartego wyniku</li>
<li>wartość "title" piątego wyniku</li>
</ul>Ściągawka: document.createElement, element.innerText, element.innerHTML
Po kliknięciu na każdy z elementów utworzonej poprzednio listy:
- przekieruj użytkownika na adres #/post/ID_POSTU
- pobierz szczegóły postu z adresu
http://vps88089.ovh.net:3000/posts/ID_POSTU
- pobierz komentarze do danego postu z adresu http://vps88089.ovh.net:3000/comments?postId=ID_POSTU
- wyświetl szczegóły postu
- wyświetl nicki (wszystko przed @) osób komentujących oraz ich komentarze
Ściągawka: element.addEventListener, window.location.hash, string.split, element.innerHTML
Jeżeli przejdziesz teraz na szczegóły postu na Twoim blogu, i odświeżysz stronę, szczegóły postu nie są załadowane.
Naprawmy ten błąd :)
Przy okazji, dodajmy link "wróć do strony głównej" i upewnijmy się, że po przejściu na stronę główną ładowane są posty.
Ściągawka: window.location.hash, obiekt window emituje zdarzenie "hashchange"
(ale nie jest ono wywoływane przy pierwszym ładowaniu strony!)
Dodajmy formularz komentowania na naszej stronie. Formularz powinien mieć pole na adres email oraz na tekst komentarza. Po wypełnieniu (i upewnieniu się, że podano jakąś treść) dane prześlijmy w formacie JSON na adres http://vps88089.ovh.net:3000/comments
Struktura komentarza to: author (string), body (string) i postId (number)
Ściągawka: xml.setRequestHeader('Content-type', 'application/json'), JSON.stringify
inne, warte uwagi