Javascript Syntax
Inne tutoriale: Java essentials
Osadzanie skryptu w HTML-u
W HTML-u skrypty umieszczane są wwenątrz tagów script
.
W praktyce skrypty można tak pisać, ale nie jest to wygodne, dlatego używa się osadzania skryptów.
<script src="plik_ze_sryptem.js"></script>
<script>
////Tutaj skrypty
</script>
Zmienne
Warning
Zmienne nie mogą zaczynać się od liczb, ani nazywać się jak słowa kluczowe w JS (new
function
class
etc)
/* let 3liczba = 43; */
/* let function = 3; */
let $function = 43; // ale za to $ jest dozwolony jako znak
Typy zmiennych
Warto pamiętać, że JS ma dynamiczne typowanie, czyli nie musimy definiować typów zmiennych, ponieważ to jest sprawdzane dynamicznie w trakcie pracy.
Poza tym możemy nadpisywać zmienne innymi typami.
let mojaZmienna = 43;
mojaZmienna = "teraz slowo";
Zalecane formatowanie dla zmiennych to camelCase
.
Podstawowe
//Number - zawsze zmiennoprzecinkowa (także dla całkowitych)
let wiek = 22;
//String - wartpamiętać, że można je definiować na 3 sposoby "", '' i ``
let imie = "Marian";
// Boolean
let isTrue = true;
//Undefined
let jeszczeNieOkreslone;
// Null
let nic = null;
// Symbol (ES2015) - unikatowa wartość, której nie można zmienić
Dla sprawdzania typów ożywamy operatora typeof
let zmienna = 54;
console.log(typeof zmienna);
//>number
typeof "Slowo";
//>"string"
let nieWiadomo;
console.log(typeof nieWiadomo);
//>undefined
Warto zwrócić uwagę na to co daje tutaj null
. (Jest to wyjątek z którym trzeba się pogodzić)
console.log(typeof null);
//>object
Tablice (arrays)
Są to kontenery w których trzymamy zmienne i obiekty
const liczby = [22, 33, 45];
const slowa = new Array("slowo1", "slowo2");
console.log(liczby[0]);
// 22
// mamy tu zbliżone odliczanie do pythonowego
console.log(liczby[-1]);
// 45
// co ciekawe mimo inicjalizacji jako const możemy zmieniać wartości w tablicy
liczby[1] = 32;
//Ale nie możemy usunąć samej tablicy
Tablice mogą zawierać różne wartości w kolejnych komórkach
let osoba = ["Marian", "Kowalski", 22, ["Burek", "Kulka"]];
Operacje na tablicach
let tablica = [];
// rozmiar tablicy
tablica.length;
// 0
tablica.push(10); //teraz tablica zawiera 10
// 1 - zwraca ona nową długośż naszej tablicy
tablica.unshift(0); //tobi to samo co push, tylko dodaje na początek
tablica.pop(); //a po tej operacji jest znowu pusta
// 10 - zwraca wartość z końca tablicy
tablica.shift(); // zwraca pierwszy element rablicy
const inna = ["pierwszy", "drugi", "trzeci"];
inna.indexOf("pierwszy");
// 0
inna.includes("trzeci");
// true
Iterowanie po tablicach
const lista = ["a", "b", "c"];
for (const [num, elem] of lista.entries()) {
console.log(`Indeks: ${num} Zawartosc: ${elem}`);
}
Rozmontowywanie tablic
// tworzymy trzy zmienne (x, y, z), które bęzą zawierać poszczególne elementy
const [x, y, z] = [1, 2, 3];
const [, dwa, , cztery] = [1, 2, 3, 4, 5]; //nie musimy brać wszystkich elementów z danej tablicy
Możemy też w ten sposób definiować zmienne domyślne (jeśli w tablicy nie ma danego elementu do domyślnie jest Undefined)
const [x = 1, , z = 3] = [0, 1];
Ten mechanizm pozwala na łatwe zwracanie wielu elementów z funkcji, podobnie do krotek z pythona.
Sposoby definiowania
Jest kilka sposobów na definiowanie zmiennych. Służą do tego słowa kluczowe let
, var
i const
.
let - definiowanie zwykłej zmienej wartości, za jego pomocą można też definiować puste zmienne.
let num = 30;
num = 32;
let undef;
var - podobne do let lecz jest już przestarzałe i nie zaleca się jego używania. Var jest dużo bardziej bugogenne
function f() {
for (var let_i = 0; let_i < 5; let_i++) {
console.log("let_i: " + let_i);
}
for (var var_i = 0; var_i < 5; var_i++) {
console.log("let_i: " + let_i); // to będzie działać dobrze
}
}
Na dodatek pozwala ono używać zmiennej przed jej zadeklarowaniem.
const - po prostu stałe zmienne
const PI = 3.14;
// PI = 1; - spowoduje TypeError
// const unknown; - to też nie zadziała (SyntaxError)
Teoretycznie można pracować nawet bez definiowania zmiennych wg wymienionych wyżej sposobów.
imie = "Jan";
console.log(imie);
// Jan
Tylko, że ta zmienna będzie wtedy odpowiednikiem zmiennej globalnej.
Operatory
Typy operatorów:
- Matematyczne:
+
-
*
/
**
(potęgowanie) - Przypisywania:
=
+=
*=
-=
++
--
etc - Boolowskie
- porównania:
>
>=
<
<=
==
!=
===
- logiczne
&&
||
!
- trójargumentowy
? :
Kolejność operatorów jest taka jak w matematyce.
Operator ==
i ===
Istnieją dwa operatory równości:
===
- ścisły - zwraca prawdę tylko i wyłącznie wtedy, gdy obie strony są takie same, nie bawi się w żadne konwersje etc.==
- jest nieco luźniejszy, pozwala sobie na konwersje pomiędzy typami jeśli wartości są różnych typów, poza tym ma inne "pomagające" mechanizmy, które w dłuższej prespektywie mogą powodować więcej błędów. (jeśli się da używaj===
)
19 === "19";
// false
19 == "19";
// true
Podobnie do ===
działa !==
Konwersja
Aby dokonać konwersji trzeba po prostu użyć konstruktora danego typu.
const rokStr = "2001";
Number(rokStr);
//>2001
String(55);
//>"55"
Tutaj w wypadku podania niewłaściwej wartości zamiast wyjątku dla liczby możemy dostać NaN
(Not a Number)
let a = Number("ff2");
a;
//>NaN
typeof a; // Co ciekawe ten typ jest wciąż numerem
("number");
Poza tym warto uważać na sprytną konwersję typów np przy printowaniu.
Na ogół działa to fajnie, ale operator +
może trochę napsuć i być źródłem wielu błędów.
"22" + 10;
// 2210
"22" - 10;
// 12
"22" - "10";
// 12
"22" - "10" + 55;
// 67
"22" - "10" + "55";
// 1255
Konwersja na Boola - wartości, które przy konewrsji na boola dają false
:
- 0 - wartość liczbowa równa zero
- ""
undefined
null
NaN
Pozostałe konwertują się na wartość true
Praca ze stringami
Mamy tutaj standardowe metody do pracy z ciągami znaków, podobne nieco do tych pythonowych.
Mamy:
split(separator=" ")
replace(co,na_co)
,replaceAll(co,na_co)
let slowo = "";
Printowanie
console.log("Wiadomosc1", 323);
//> Wiadomosc1 323
// aby wypisać wiadomośc można klasycznie połączyć stringi
const wiek = 10;
const wiadomosc = "Hej, mam " + wiek + "lat";
console.log(wiadomosc);
// Ale można to zrobić wygodniej warto pamiętać, że robimy to w ``, a nie w ''
const wiadomosc2 = `Hej, mam ${wiek} lat`;
console.log(wiadomosc2);
Ogólnie to obecnie ten drugi sposób definiowania jest wygodniejszy
console.log(
"Wiele\n\
linii w kodzie"
);
//VS
console.log(`Wiele
linii w kodzie`);
IF-y i warunki
Syntax warunków jest zbliżony do C++
const wiek = prompt("podaj wiek"); //pojawi się okno z pytaniem
if (wiek >= 18) {
console.log("Pełnoletni");
} else {
console.log("Niepełnoletni");
}
if (true) console.log("Nie");
else console.log("Tak");
if (true) console.log("True to prawda");
Mamy też tu switcha
switch (key) {
case value:
fun1();
fun2();
break;
case value2:
case value3: // dla tych dwóch wartości będzie się dziać to samo
fun3();
break;
default:
break;
}
Jest też operator trójargumentowy ? :
wiek >= 18 ? console.log("Dorosły") : log.console("Nie Dorosły");
// Albo
console.log(wiek >= 18 ? "Dorosły" : "Nie Dorosły");
Pętle
Nic ciekawego mamy 2 zwykłe typy pętli, for
i while
.
for (let num = 0; num < 10; num++) {
if (num === 5) continue; //ale skipujemy dla 5
console.log(`Printujemy po raz ${num + 1}`);
}
while (true) {
//nigdy nie kończąca się pętla
break; //no chyba, że użyję break
}
Poza tym mamy jeszcze forEach w dwóch wariantach.
const tablica = [0, 11, 22, 33, 44];
for (const i of tablica) {
console.log(i);
}
// Metoda forEach
tablica.forEach(function (i) {
console.log(i);
});
Funkcje
Definiujemy je używając słowa kluczowego function
.
Także są na ogół formatowane jako camelCase
.
Warning
Niestety JS nie wspiera przeciążania funkcji.
Poniżej zwykłe deklaracje funkcji (function declaration).
function printHello() {
console.log("Hello");
}
printHello();
// Hello
function isApple(fruit) {
if (fruit == "apple") return true;
else return false;
}
Argumenty funkcji
Funckcję mającą n
argumentów możemy wywołać używając:
n
argumentów
printHello(23);
// Hello
- więcej niż
n
argumentów - nieoczekiwane argumenty są ignorowane
isApple("apple", 43); //drugi argument jest ignorowany
// true
- mniej niż
n
argumentów - brakujące mają wartośćundefined
isApple(); //pierwszy argument ma wartość "undefined"
// false
- Możemy także przekazać do funkcji listę argumentów, które będą zmapowane na kolejne argumenty
let vector = ["apple", 2, 3, 4];
isApple(...vector);
Do argumentów możemy odwołać się poprzez:
- nazwę
- pseudo-listę
arguments
function greetings() {
for (let i = 0; i < arguments.length; i++) {
console.log("Hi, " + arguments[i]);
}
//Hi, Tom
}
W razie potrzeby możemy wymusić liczbę argumentów
function f(x,y) {if (arguments.length != 2)...
Właśnie przez ten śmietnik nie mamy przeciążania funkcji.
Funkcje anonimowe
Tym określeniem określamy funkcje, które są przechowywane w zmiennych. Określane także jako function expression
.
let myFun = function () {
lonsole.log("HelloFun");
};
myFun();
// HelloFun
Funkcje strzałkowe
Funkcje strzałkowe (arrow functions) to po prostu inny format funkcji przypominający wyrażenia lambda z innych języków.
Pozwala na krótszy zapis funkcji.
const arrowFun = (value) => value + 12;
arrowFun(10);
// 22
const arrowFunMultiline = (imie) => {
console.log(`imie to ${imie}`);
return imie;
};
arrowFunMultiline("Jan");
// imie to Jan
// "Jan"
const linkNames = (imie, nazwisko) => imie + " " + nazwisko;
Mają one dość elastyczny zapis, w wypadku jednego argumentu nie musimy używać nawiasów początkowych, podobnie wygląda sytuacja z klamerkami w któ©ych zawieramy nasze funkcje.
Różnią się też nieco implementacją, nie jest dla nich tworzony this
, ani obiekt arguments
.
const fun1 = function () {
console.log(this); // undefined
};
const fun2 = () => {
console.log(this); // zwróci globalny obiekt window
};
Kolejność (hoisting)
Przy deklarowaniu funkcji nie musimy się martwić kolejnością ich deklaracji, ponieważ ich deklaracje są doczytywane wcześniej (ale na ich użycie musimy poczekać).
const fun1 = function () {
fun2();
};
const fun2 = function () {
console.log("ok");
};
fun1(); //ale ta już musi być po deklaracji
Dostęp do zmiennych w funkcji zagnieżdżonej
Funkcjie zdefiniowane we wnętrzu innuch funkcji mają dostęp do wszystkich zmiennych które są dostępne w wybranym zakresie.
var global = "this is global";
function scopeFunction() {
alsoGlobal = "This is also global!";
var notGlobal = "This is private to scopeFunction!";
function subFunction() {
alert(notGlobal); // We can still access notGlobal in this child function.
stillGlobal = "No var keyword so this is global!";
var isPrivate = "This is private to subFunction!";
}
alert(stillGlobal); // This is an error since we haven't executed subfunction
subFunction(); // execute subfunction
alert(stillGlobal); // This will output 'No var keyword so this is global!'
alert(isPrivate); // This generate an error since isPrivate is private to
// subfunction().
alert(global); // outputs: 'this is global'
}
alert(global); // outputs: 'this is global'
alert(alsoGlobal); // generates an error since we haven't run scopeFunction yet.
scopeFunction();
alert(alsoGlobal); // outputs: 'This is also global!';
alert(notGlobal); // generates an error.
Zmienne proste vs złożone (kopia vs referencja)
Przy pracy ze zmiennymi warto pamiętać o tym, że jedynie proste typy zmiennych są kopiowane przy przekazywaniu dalej (Number, String, Boolean, Undefined, Null, Symbol, BigInt), w pozostałych przypadkach przekazywana jest referencja.
let x = 1;
let y = x;
y = 2;
console.log(x);
// 1
console.log(y);
// 2
let d1 = { x: 1 };
let d2 = d1;
d2.x = 2; //niechcący nadpisaliśmy wartość x obiektu d1
console.log(d1);
//{ x: 2 }
console.log(d2);
//{ x: 2 }
Aby temu zapobiec można użyć operatora Object.assign()
- łączy on ze sobą 2 obiekty, wystarczy połączyć nasz z jakimś pustym.
Tworzy on jednak tylko płytką kopię.
let kopia = Object.assign({}, d1);
Obiekty
Obiekty mogą być tworzone na dwa sposoby.
Tworzenie
Jako wyrażenie (expression) - sposób nieco zbliżony do opisu słownika. Otrzymujemy od razu gotową instancję naszej klasy.
const osoba = {
imie: "Marian",
nazwisko: "Nowak",
ur: 1999,
getAge: function () {
return 2021 - this.ur;
},
};
Poszczególne elementy powinny być oddzielone przecinkami.
Deklaracja - tworzymy deklarację klasy i potem możemy (używając operatora new
tworzyć jej instancje)
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
let instancja = new Rectangle(0, 0);
Dostęp do elementów danej klasy uzyskujemy za pomocą słowa this
.
// Dane można pozyskiwać na 2 sposoby
osoba.imie;
//Marian
//Ten sposób jest nieco bardziej elastyczny i pozwala na nieco łatwiejsze zarządzanie polami
osoba["imie"];
//Marian
// Możemy tutaj też łątwo edytować pola w klasie
osoba.drugie_imie = "Zbigniew";
osoba;
// Object { imie: "Marian", nazwisko: "Nowak", wiek: 25, drugie_imie: "Zbigniew" }
osoba.getAge();
// 22
Prototypy
Prototyp jest deklaracją zawierającą definicje, które są współdzielone między instancjami. Każda instancja ma dostęp do pól i metod prototypu.
Jest to przydatne, gdy chcemy coś zmienić we wszystkich instancjach danego obiektu bez ingerowania w niego.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
let rect = new Rectangle(10, 10);
// dla obiektu prototype dla instancji __proto__
Rectangle.prototype.field = function () {
return this.height * this.width;
};
rect.field(); // to nam zwróci 100
Rectangle.__proto__; //zwróci nam prototyp bazowego obiektu
Gettery i settery
Pozwalają na używanie niektórych metod tak, jakby były polami.
Settery są bardzo przydatne podczas walidacji danych.
const account = {
owner: "Jonas",
movements: [200, 530, 120, 300],
get latest() {
return this.movements.slice(-1).pop();
},
set latest(mov) {
this.movements.push(mov);
},
};
account.latest = 23;
account.latest;
Dziedziczenie
Używając słowa kluczowego extends możemy także tworzeyć hiererchie klas
class Animal {
// nazwa;
constructor(nazwa) {
this.nazwa = nazwa;
}
habla() {}
nazwaAnimal() {
return this.nazwa;
}
}
class Pies extends Animal {
constructor() {
super("Pies");
}
habla() {
return "hau";
}
}
let perro = new Perro();
console.log(perro.nazwaAnimal() + " dice " + perro.habla());
Komunikacja zewnętrzna i funkcje asynchroniczne
Aby zapobiec marnowaniu czasu niektóre funkcje w JS-ie zostały zaimplementowane asynchronicznie.
const img = document.querySelector(".dog");
img.src = "dog.jpg"; // I właśnie to wczytywanie będzie asynchroniczne
//Jak już się wczyta to odpalony zostanie ten event
img.addEventListener("load", function () {
console.log("Wczytano");
});
Warto pamiętać, że eventy z pętli zdarzeń mają miejsce tylko wtedy, gdy w głównym wątku nic nie jest przetwarzane.
Po dokładniejsze wyjaśnienia zajrzyj do Javascript - Inne informacje
AJAX
AJAX - Async JavaScript And XML. Pozwala asynchronicznie komunikować się z zewnętrznymi serwerami (wysyłać żądania etc). Kiedyś opierało się to na użyciu XML-a, ale od iluś lat używa się jednak JSONa (teraz to podstawa REST-a).
Na potrzeby przykładów korzystamy z darmowych API stąd.
Wcześniej używało się do tego XMLHttpRequest.
const request = new XMLHttpRequest();
request.open("GET", "https://restcountries.eu/rest/v2/name/poland");
wynik = request.send();
var polska;
request.addEventListener("load", () => {
console.log(this.responseText);
//po otrzymaniu wyświetli nam się cały surowy tekst JSONa,
//który trzeba przekształcić w jakiś sensowny obiekt
[polska] = JSON.parse(this.responseText);
//json parse zwraca listę obiektów, więc bierzemy tylko pierwszy
});
Jednak obecnie ta metoda jest przestarzała i zamiast tego używa się fetch, które zwraca nam Obietnicę (promise). Jest to tymczasowy obiekt w którym znajdziemy wynik operacji asynchronicznej jak już się wykona.
Dzięki takiemu podejściu nie musimy polegać na callbackach, które mogą być problematyczne.
const promise = fetch("https://restcountries.eu/rest/v2/name/poland");
Taka obietnica po zakończeniu zadania może zmienić swój stan na spełnioną, lub odrzuconą.
Jak już została wykonana to możemy ją skonsumować.
Do tego warto używać metody then
do której przekazujemy co ma zostać zrobione z otrzymanymi danymi.
then(onFulfilled);
then(onFulfilled, onRejected);
then(
(value) => {
/* fulfillment handler */
},
(reason) => {
/* rejection handler */
}
);
W wypadku błędu, lud odrzucenia rządania możemy też wykorzystać catch()
na końcu łańcuszka.
fetch("https://restcountries.eu/rest/v2/name/poland").then(function (response) {
console.log(response); //wypisze nam całą klasę odpowiedzi z kodem statusu etc
const new_promise = response.json(); //zwraca sparsowany obiekt, ale jest też kolejną obietnicą
});
Używając tych mechanizmów można łatwo łączyć wiele żądań w ciągi.
const getCountryData = function (country) {
fetch(`https://restcountries.eu/rest/v2/name/${country}`)
.then((response) => {
response.json();
}) //po otrzymaniu odpowiedzi parsujemy ją asynchronicznie
.then((data) => {
console.log(data);
}); //po sparsowaniu w końcu możemy ją wyświetlić
};
Mamy tu jedno żądanie, które po wykonaniu ma zwrócić kolejną obietnicę, która po spełnieniu ma nam wyświetlić sparsowany wynik.
Wysyłanie żądań z zawartością
Wysyłanie żądania zawierającego JSONa
Stary sposób:
let xhr = new XMLHttpRequest();
let url = "https://httpbin.org/post";
xhr.open("POST", url, true); //true mówi, że ma być asynchronicznie
xhr.setRequestHeader("Content-Type", "application/json"); //kiedy jsona trzeba określić jaki to typ zawartości
xhr.onreadystatechange = function () {
console.log("Jest git");
console.log(this.responseText);
};
var data = JSON.stringify({ imie: "Jan", wiek: 12 });
xhr.send(data);
Nowy sposób:
fetch("https://httpbin.org/post", {
method: "post",
headers: {
Accept: "application/json, text/plain",
"Content-Type": "application/json",
},
body: JSON.stringify({ imie: "Jan", wiek: 12 }),
})
.then((res) => res.json())
.then((res) => console.log(res));
Tutaj drugim argumentam fetcha jest odpowiednik obiektu Request.
Warto zwrócić tu uwagę na pola:
method
- określa typ żądaniaheaders
- mamy tutaj typowe nagłówki http lista, za ich pomocą ustalamy np jaki typ danych wysyłamy (Content-Type
), czy też jakie dane jesteśmy w stanie przyjąć (Accept
).body
- już samo ciało żądania, tutaj warto pamiętać, że najczęściej wysyłamy je w postaci stringa
Runkcje asynchroniczne
Do zdefiniowania funkcji asynchronicznej (zwracającej obietnicę) wykorzystujemy słowo kluczowe async
.
// Variation of program in slide 38...
const fs = require("fs");
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) reject(err + "");
else resolve(data + "");
});
});
}
async function readTwoFiles() {
try {
console.log(await readFilePromise("readfile.js"));
console.log(await readFilePromise("doesntExist.js"));
} catch (err) {
console.error(err + "");
}
}
readTwoFiles();
Włączanie zewnętrznych bibliotek
TODO