Wprowadzenie do JavaScript
ECMAScript 5, Web APIs, testowanie aplikacji
Bartosz Szczeciński
16.05.2018
@btmpl
medium.com/@baphemot
Bartosz Szczeciński
software developer, konsultant
https://szczecinski.eu
https://medium.com/@baphemot/
Cele
-
Kurs JavaScript dla programistów nie mających wcześniejszej styczności z JS
-
Interakcja JS z przeglądarką
-
Praca z serwerem zdalnym w technologii REST
- Testowanie aplikacji JS przy użyciu mocha+karma
What the ... JavaScript?
https://www.youtube.com/watch?v=2pL28CcEijU
Wymagania
- Komputer z dostępem do internetu
- Przeglądarka internetowa
- Ulubiony edytor kodu
- Podstawowa wiedza w zakresie języków programowania
- Podstawy HTML / CSS
Logistyka
- Wszystkie materiały są / będą dostępne on-line
- Masz pytanie, komentarz lub wątpliwość? Podnieś rękę - nie musisz czekać do końca warsztatu.
- Pytania dodatkowe / wykraczające poza zakres szkolenia? Sesja Q&A na końcu szkolenia, zapytaj mnie w przerwie albo na @btmpl
- "Pan Szczeciński" to mój tata - możecie po prostu mówić mi po imieniu ;-)
JavaScript
JavaScript
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.
JavaScript
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.
JavaScript
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".
JavaScript
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.
JavaScript
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
Składnia
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.
Przykładowy kod JS
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();
Kodowanie znaków
if (isPersonOfLegalAge(me)) {
me = with🍺(me);
} else {
me = with🥤(me);
}
'cola'.length // 4
'🥤'.length // 2
JS wewnętrznie używa kodowania UTF-16
(chyba że autor silnika zdecyduje inaczej)
'🥤'.split('') // (2) ["�", "�"]
Powoduje to kilka problemów:
Więcej niż kilka ...
'👨👩👧👦 '.length; //
[...'👨👩👧👦 ']; //
11
"👨", "", "👩", "", "👧", "", "👦", " "
https://twitter.com/wesbos/status/769228067780825088
Dołączanie JS do stron HTML
<body>
<h1>Witaj świecie</h1>
<script>
var wspieramJs = true;
</script>
</body>
Osadzanie bezpośrednio w kodzie strony:
- parsowany synchronicznie (blokuje renderowanie strony)
- wystąpienie błędu zatrzymuje parsowanie kodu w danym bloku
Dołączanie JS do stron HTML
<body>
<h1>Witaj świecie</h1>
<script src="./script.js"></script>
</body>
Dołączenie zewnętrznego pliku z kodem JavaScript:
- domyślnie parsowany synchronicznie
- wystąpienie błędu powoduje zaprzestanie parsowania pliku
- rekomendowanym jest używanie rozszerzenia .js ale w zależności od konfiguracji serwera i przeglądarki może to być dowolne rozszerzenie
- domyślnie podczas wczytywania pliku następuje "zawieszenie się" strony
- skrypty możemy osadzać w <head> jeżeli chcemy by były dostępne "od razu jak tylko strona zacznie się renderować" albo pod koniec <body> w przeciwnym wypadku
Dołączanie JS do stron HTML
<script src="./script.js" async></script>
<script src="./script.js" defer></script>
- atrybut async spowoduje równoległe pobranie pliku, ale po pobraniu może "zawiesić" przeglądarkę na czas jego wykonania
- brak gwarancji kolejności
- atrybut defer działa podobnie jak async, ale kod zostanie wykonany dopiero w momencie kiedy strona będzie wyrenderowana
- wykonane w kolejności deklaracji
Komentarze
// 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
ASI
Automated Semicolon Insertion
var _name = 'Bartosz';
var _age = 34;
var name = 'Bartosz'
var age = 34
var a = 3;
var a
a
=
3
var 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
Zmienne
poprawne nazewnictwo
- zmienna to rodzaj "nazwanego pudełka" w którym przetrzymywane są dane
- deklaracja zmiennej poprzedzona jest słowem var
- nazwa zmiennej musi składać się z minimum 1 znaku
- musi zaczynać się od:
- litery
- znaku _
- znaku $ - kolejnymi znakami mogą być litery, znaki diakrytyczne, cyfry, _ i $ (oraz kilka "niewidzialnych" znaków)
Zmienne
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 |
Typy danych
Typy danych
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.
Typy proste
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; // undefined
Typy proste
Boolean
Prosty typ danych przyjmujący wartość "true" lub "false".
var isOfLegalAge = false;
var isHappy = true;
Typy proste
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.30000000000000004
https://0.30000000000000004.com/
// 1
Typy proste
Number - stałe
JS deklaruje także kilka stałych pomocnych przy pracy z wartościami liczbowymi:
0 / 0; // NaN
5 / 0; // Infinity
Typy proste
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 +';
Typy proste
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.
Typy złożone
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'
Typy złożone
Array
- Typ złożony pozwalający na przechowywanie wielu typów prostych lub złożonych w określonej kolejności.
- Tablice są numerowane od 0
- Tablice w JS nie są typowymi tablicami znanymi z innych języków, są one podobne do Map (oraz są Obiektami)
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]
Typy złożone
Function
Funkcje w JS także są obiektami
dowiemy się o tym więcej później :))
Typy złożone predefiniowane
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
Typy złożone predefiniowane
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-1
JSON
JSON (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.
Sprawdzanie typu
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.
Konwersja między typami
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".
- jeżeli wartość jest prymitywem - nie nic rób
- spróbuj wywołać obj.toString()
- spróbuj wywołać obj.valueOf()
- jeżeli nic nie pomoże, TypeError
Konwersja między typami
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"
Konwersja między typami
// 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 false
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ć.
Operatory
Operatory
Element składni języka pozwalający na przypisywanie, modyfikowanie i porównywanie typów danych.
Przypisanie
- W celu przypisania wartości do zmiennej używamy pojedynczego znaku równości.
- Możemy przypisać wiele wartości w jednej linijce, oddzielając kolejne operacje przypisania przecinkiem.
var a = 1;
var a = 1, b = 2, c = 3;
- Istnieją także kombinowane operatory przypisania wykonujące operacje matematyczne i logiczne
var a = 5;
var b = 0;
b += 5; // b = b + 5;
b -= 2; // b = b - 5;
a |= 2; // a = a | 2 = 7
Porównanie
W 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 // true
NaN == NaN; //
false
Porównanie
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; // false
Dodawanie / łączenie
Operator + 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"
Operatory logiczne
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
}
Operator "potrójny" lub "warunkowy"
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;
Przecinek
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 ...
Operator bitowy
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
*/
Wykonywanie kod dynamicznego
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; // 3
Wykonywanie kod dynamicznego
eval
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; // 3
Polecenia
Polecenia
Element składni instruujący język o sposobie przepływu danych i informujący o operacjach, które należy wykonać.
Kondycje
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:
Ćwiczenie
sprawdzanie wieku
https://github.com/BTMPL/es5-workshop
Bloki kodu
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';
}
Switch
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;
}
Switch
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
Switch
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 :("
Pętle
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, 2
var 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, 10
for (var i = 0; i < 10 ; i--) {
console.log(i);
}
// ?
Nieskończona pętla!
Pętle
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);
}
With
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
}
Ćwiczenie
odwracanie stringu
Funkcje
Funkcje
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.
Deklarowanie
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.
Wywoływanie
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';
Zwracanie danych
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';
Funkcje czyste
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();
Wywoływanie
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();
Wywoływanie
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'
Argumenty
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 = NaN
Argumenty
wartoś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 = Infinity
Argumenty
nieokreś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); // 10
Argumenty
funkcje 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()'
Ćwiczenie
powitanie
Zasięg
Zasięg
Fragment programu w którym widoczne jest, lub do którego dostęp posiada dane wyrażenie lub polecenie.
Zasięg domyślny
W ES5 istnieje tylko jeden specyfikator zmiennych - var - użycie go deklaruje funkcję w jednym z dwóch "zasięgów":
- jeżeli użyjemy go w funkcji, zmienna ta będzie widoczna w tej funkcji i wszystkich jej blokach
- jeżeli użyjemy go poza funkcją, zmienna będzie miała zasięg globalny
var age = 34;
function whatsMyAge() {
console.log(age);
}
whatsMyAge(); // 34
function whatsMyName() {
var name = 'Bartosz';
}
whatsMyName();
console.log(name); // ReferenceError
Zasięg funkcji - wyjątek
Wyją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"
Zasięg funkcji
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);
Zasięg funkcji
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(); // TypeError
Zasięg funkcji
closure
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(); // 3
Zasięg funkcji
Zaawansowane 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, 5
Zasięg funkcji
Zaawansowane 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
Ćwiczenie
licznik
Hoisting
Hoisting
"Przeniesienie do góry zasięgu"
Hoisting
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.
Hoistowanie
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);
}
Hoistowanie
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);
}
Hoistowanie
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)
Więcej o: Tablice
Sposoby tworzenia tablic
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.
Dostęp do danych w 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!
Metody i właściwości obiektu Array
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); // true
Dodatkowo dostępne jest także wiele metod przydatnych w programowaniu funkcyjnym (FP) tj.: map, forEach, reduce, sort
Iterowanie po tablicy
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]
Usuwanie elementów z tablicy
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]
Ćwiczenia
średnia,
unikaty,
takie same
bonus: obliczenie kosztu posiłku
Więcej o: Obiekty
Tworzenie obiektów
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
}]
};
Tworzenie obiektów
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.
Tworzenie obiektów
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'
Tworzenie obiektów
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 ... ?
Tworzenie obiektów
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ź'
}
*/
Ćwiczenie
imię babci
Tworzenie obiektów
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'
Tworzenie obiektów
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.
Tworzenie obiektów
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.
Ćwiczenie
fix this
Dziedziczenie
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; // 4
Dziedziczenie
w 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
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
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
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
dziedziczenie prototypowe
- każdy obiekt w JS musi posiadać dokładnie jeden prototyp
- każdy prototyp może posiadać swój prototyp
- łańcuch prototypów musi być skończony
- na końcu łańcucha znajduje się null
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__; // null
Dziedziczenie
dziedziczenie prototypowe
- oznacza to, że metody prototypowe Object dostępne są dla wszystkich typów obiektów w JS
- jeżeli zdefiniujemy własność Object.prototype.mojaFunkcja będzie ona dostępna retroaktywnie dla wszystkich obiektów
function test() {
}
var obj1 = new test();
obj1.mojaFunkcja(); // TypeError
Object.prototype.mojaFunkcja = function() {
return 'Hej!';
}
obj1.mojaFunkcja(); // "Hej!";
Dziedziczenie
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();
Dziedziczenie
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
Dziedziczenie
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'); // true
Dziedziczenie
Dziedziczenie 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
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; // false
Dziedziczenie
Dziedziczenie 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
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
Ćwiczenie
mój pies
Re-definiowanie, mutowanie
i niemutowalność zmiennych
Re-definiowanie zmiennych
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.
Mutowanie
Mutowaniem nazywamy zmianę wartości obiektu lub tablicy bez ponownego przypisania całej zmiennej:
var person = {
name: 'Bartek'
};
person.name = 'Bartosz';
person.name; // Bartosz
Dział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';
Immutability
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';
Immutability
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"
This
This
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;
This
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!"
This
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();
This
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).
This
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?'
This
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(); // TypeError
function 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!'
This
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.
This
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: ???'
This
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); // 90
Ćwiczenie
każdy zna moje imię
Wyrażenia regularne
Niektórzy ludzie, kiedy staną w obliczu problemu pomyślą: "Wiem, użyję wyrażeń regularnych."
Teraz mają już dwa problemy
- Jamie Zawinski
Czym są wyrażenie regularne
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.
Przykładowe zastosowania
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
Error Handling & Debugging
Error handling
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'); // URIError
Error handling
Throw 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;
Error handling
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);
Error handling
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;
}
Error handling
try / catch / finally
try {
// spróbuj
}
catch (err) {
// ... złap błąd ...
}
finally {
// ostatecznie ...
}
Ewentualnie możemy użyć notacji try-catch:
- block try jest obowiązkowy
- catch lub finally muszą wystepować (ale może wystąpić tylko jeden lub drugi)
- złapany błąd można rzucić "wyżej"
- jeżeli błąd nie zostanie obsłużony przez żaden z zakresów "wyżej", aplikacja obsłuży go domyślnie
Error handling
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;
}
}
Error handling
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.
Logowanie
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!)
Logowanie
console
var person = {
name: 'test'
}
console.log(person);
person.name = 'Bartek';
Debuggowanie
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:
Debuggowanie
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"
Strict Mode
Strict mode
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"
Strict mode
Jak aktywować
'use strict'
var name = 'Bartosz';
age = 34;
function test() {
'use strict'
console.log(this);
}
test();
dla całego pliku
dla pojedynczej funkcji
Strict mode
(Najważniejsze) różnice
- nowe słowa zastrzeżone: "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"
- każda zmienna musi być zadeklarowana
'use strict'
name = 'Bartosz'; // ReferenceError
name = 'Bartosz'; // window.name
name; // "Bartosz"
- zabronione jest definiowanie funkcji w funkcjach
- zabronione jest modyfikowanie this w funkcjach nie będących metodami lub konstruktorami
010; // 8
'use strict'
010; // SyntaxError
- wartości w systemie ósemkowym są niedozwolone
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!
Web APIs
DOM
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.
DOM
<!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>
DOM
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>
DOM
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
DOM
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');
DOM
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>
DOM
relacje między węzłami
DOM
różnica między node a element
#text
- wszystkie elementy są nodami (element dziedziczy po node)
- nie wszystkie nody są elementami
DOM
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>
*/
DOM
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>'
DOM
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";
Ćwiczenie
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.)
DOM
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.
Ćwiczenie
używając JS, dodaj kilka tematów do listy
DOM
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();
Ćwiczenie
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ę :)
DOM
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
Window
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.
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?');
Ćwiczenie
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).
Window
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';
Window
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:
- adres URL strony, która ma zostać otworzona
- nazwa okna, pod jaką zostanie otworzone (będzie dostępne w nim jako window.name :))
- konfiguracja okna jako string (resizable=yes,scrollbars=yes,status=yes,width=500,height=300)
Window
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
Window
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:
Window
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 + ' ...');
}
Ćwiczenie
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
Magazyn Danych
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.
Magazyn Danych
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
Magazyn Danych
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]"
Magazyn Danych
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);
});
Ćwiczenie
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
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.
Zdarzenia
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.
Zdarzenia
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;
Zdarzenia
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!"
Zdarzenia
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!
Zdarzenia
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ĄĆ!
Ćwiczenie
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.
Zdarzenia
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
Zdarzenia
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);
Zdarzenia
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);
Zdarzenia
"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);
Zdarzenia
"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.
Zdarzenia
"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);
Zdarzenia
"capture phase"
Na podstawie https://javascript.info/bubbling-and-capturing
Zdarzenia
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();
// ....
});
Zdarzenia
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();
// ....
});
Zdarzenia
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
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.
AJAX
Typowa sesja HTTP
AJAX
Minusy
- stan interfejsu użytkownika nie jest zachowany, co powoduje utratę wprowadzonych danych, chyba, że przesyłamy je pomiędzy klientem a serwerem
- konieczność przesyłania dodatkowych danych - spowolnienie procesu; konieczność podmiany całej strony
- długi czas oczekiwania na odpowiedź serwera - poczucie braku interaktywności
AJAX
Implementacja
- większość przeglądarek udostępnia aktualnie dwa interfejsy komunikacji z serwerem dla technologii AJAX: XMLHttpRequest oraz fetch
- nie pokrywają się one w 100% funkcjonalnością, więc oba są jeszcze przydatne
- IE nie obsługuje fetch :(
AJAX
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();
AJAX
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
AJAX
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")
AJAX
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);
});
Ćwiczenie
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
Ćwiczenie
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
Ćwiczenie
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!)
Ćwiczenie
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
DOM
inne, warte uwagi
- audio - odtwarzanie, nagrywanie, text-to-speach
- grafika - canvas: 2D, 3D, akcelerowana grafika, kamera
- komunikacja - fetch, WebRTC
- geolokacja
Wprowadzenie do JavaScript
By btmpl
Wprowadzenie do JavaScript
Standardzie ECMAScript 5, WebAPI, praca z REST.
- 663