Wykład 2: języki JavaScript i TypeScript
mgr inż. Maciej Małecki
JavaScript to język programowania zaprojektowany przez firmę Netscape Communications dla przeglądarki internetowej.
Twórcą języka jest Brendan Eich: "To defend the idea of JavaScript against competing proposals, the company needed a prototype. Eich wrote one in 10 days, in May 1995."
JavaScript: The Good Parts, Douglas Crockford
Język JavaScript nie jest wariantem języka Java!!!
Osadzenie w stronie HTML
<html>
<body>
<script>
console.log('Hello world');
</script>
</body>
</html>
Użycie osobnego pliku źródłowego
index.html
<html>
<body>
<script src="script.js"/>
</body>
</html>
script.js
console.log('Hello world');
Zmienne deklarujemy przy użyciu słowa kluczowego var
:
var a = 10, b; // a -> 10, b -> undefined
var c = null; // c -> null
lub let
:
let a = 10, b; // a -> 10, b -> undefined
let c = null; // c -> null
undefined
.var|let
jest opcjonalne, zaleca się jednak je stosować.
var a;
a = 2 + 2;
console.log(typeof(a)); // -> number
a = 'foo';
console.log(typeof(a)); // -> string
typ | przykłady |
---|---|
number |
0, 1, 0.5, 2E-10 |
boolean |
true, false |
string |
'', 'foo' |
function |
function(a) { return a + 1; } |
array |
[], ['apple', 'orange'] |
object |
{}, {id: 5, value: 'foo'} |
Funkcja jest wartością utworzoną przy użyciu literału funkcyjnego:
function add(left, right) {
return left + right;
};
add(2, 2); // -> 4
function add() {
var sum = 0;
for (i = 0; i < arguments.length; ++i) {
sum += arguments[i];
};
return sum;
};
add(1, 2, 3, 4); // -> 10
a co jeśli...
function add(left, right) {
return
left + right;
};
add(2, 2); // ?
function add(left, right) {
left + right;
};
add(2, 2); // ?
funkcja to też typ danych...
function map(array, fn) {
var result = [];
for(i = 0; i < array.length; ++i) {
result[i] = fn ? fn(array[i]) : array[i];
}
return result;
};
const array = [1,2,3,4,5];
const res1 = map(array);
const incrementFn = function(a) { return a + 1; };
map(array, incrementFn);
map(array, function(a) { return a * 2; });
map(array, incrementFn()); // ?
wartością funkcji może być funkcja
function createMultiplier(amount) {
return function(a) { return a * amount; };
};
createMultiplier(5)(2); // -> 10
function map(array, fn) {
var result = [];
for(i = 0; i < array.length; ++i) {
result[i] = fn ? fn(array[i]) : array[i];
}
return result;
};
Na szczęście funkcja map jest standardowo zdefiniowana dla typu tablicowego:
[1,2,3,4,5].map(createMultiplier(2));
// -> [2,4,5,8,10];
W języku JavaScript funkcję można zadeklarować (stworzyć) na kilka sposobów...
var add = function(left, right) {
return left + right;
};
function sub(left, right) {
return left - right;
};
const mul = (left, right) => left * right;
// w konsekwencji...
add(2, 2); // -> 4
function add(left, right) {
return 0;
}
add(2, 2); // -> 0
add = null;
add(2, 2); // -> Uncaught TypeError: add is not a function
Użycie zmiennej wewnątrz ciała funkcji bez wcześniejszej deklaracji z użyciem słowa var spowoduje użycie lub stworzenie zmiennej globalnej.
Jest to fatalny błąd projektowy, który popełnili twórcy języka JavaScript!
var a = 10;
var f = function() {
console.log(a); // -> 10 !
a = 99;
};
f();
console.log(a); // -> 99 !!!
var f = function() {
a = 99;
};
f();
console.log(a); // -> 99 !!!
Szczególnie groźne jest błędne użycie zmiennej indeksującej w pętli for:
function processRow(row) {
for (i = 0; i < row.length; ++i) {
console.log(row[i]);
}
}
function processMatrix(matrix) {
for (i = 0; i < matrix.length; ++i) {
processRow(matrix[i]);
}
};
processMatrix([[1,2,3], [4,5,6], [7,8,9]]);
-> 1 2 3
function processRow(row) {
for (var i = 0; i < row.length; ++i) {
console.log(row[i]);
}
}
function processMatrix(matrix) {
for (var i = 0; i < matrix.length; ++i) {
processRow(matrix[i]);
}
};
processMatrix([[1,2,3], [4,5,6], [7,8,9]]);
-> 1 2 3 4 5 6 7 8 9
Wewnątrz funkcji zawsze deklarujemy zmienne z użyciem słowa var (let, const), gdyż wtedy stają się one zmiennymi lokalnymi.
Zasięg zmiennych w JavaScripcie jest funkcyjny a nie blokowy, chociaż blokowa składnia sugeruje coś innego.
function f1() {
console.log(a); // -> Uncaught ReferenceError: a is not defined
};
function f2() {
console.log(a); // -> undefined
var a = 10;
console.log(a); // -> 10
};
function f3() {
console.log(a); // -> undefined
{ // <- to nie ma znaczenia dla widzialności
var a = 10;
console.log(a); // -> 10
}
};
const nextValue = function () {
let cntr = 0;
return function() {
++cntr;
return cntr;
};
}();
console.log(nextValue()); // -> 1
console.log(nextValue()); // -> 2
* Wyjątkiem są zmienne this oraz arguments.
if(a === 3) {
...
} else {
...
}
===
oraz !==
.
Następujące wartości są falsy:
false
null
undefined
''
0
NaN
Pozostałe wartości są truthy wliczając w to true
, napis 'false'
oraz dowolną wartość obiektową.
while (expression) {
...
};
do {
...
} while (expression);
for (let i = 0; i < size; ++i) {
...
};
expression
podobnie jak w przypadku instrukcji
warunkowej obliczane jest do wartości falsy i truly.
JavaScript oferuje bardzo wygodną formę deklarowania obiektów przy użyciu tzw literału obiektowego:
const person = {
name: 'John',
surname: 'Doe',
age: 30
};
person.name; // -> 'John'
person['age']; // -> 30
person.email; // -> undefined
person.email || '' // -> ''
person.address.street // -> TypeError
person.address && person.address.street // -> undefined
Obiekty są strukturami dynamicznymi, można dodawać i usuwać właściwości "w locie":
const item = {}; // obiekt "pusty"
item.name = 'ATmega1284';
item.price = 199.90;
console.log(item); // {name: "ATmega1284", price: 199.9}
delete item.name;
console.log(item); // {price: 199.9}
Możliwe jest wyliczenie wszystkich właściwości obiektu przy użyciu specjalnej składni for
in
.
const obj = {
name: 'John',
age: 30,
sayHello: function() { console.log('Hello ' + this.name); }
};
for(let property in obj) {
console.log(property + ':' + typeof obj[property]);
};
Obiekty mogą posiadać właściwości typu funkcyjnego. Tak zdefiniowane funkcje nazywamy metodami.
const account = {
balance: 0,
debit: function(amount) {
if (amount < 0) {
throw new Error('Illegal amount');
}
if (this.balance < amount) {
throw new Error('Insufficient account balance');
}
this.balance -= amount;
return this.balance;
},
credit: function(amount) {
if (amount < 0) {
throw new Error('Illegal amount');
}
this.balance += amount;
return this.balance;
}
};
const account = {
balance: 199,
printBalance: function() {
console.log(this.balance);
}
};
-> 199
const account = {
balance: 199,
printBalance: function() {
function inner() {
console.log(this.balance);
};
inner();
}
};
-> undefined
JavaScript do wersji ES5 włącznie był językiem obiektowym, w którym nie istniało pojęcie klasy ani konstruktora.
Ponieważ istnieje tylko jeden typ obiektowy (wszystkie obiekty są tego samego typu), pojęcie klasy nie ma sensu.
Klasy są jednak także użyteczne jako wzorce do tworzenia obiektów, JavaScript oferuje inne mechanizmy wzorców.
var employee = {
name: 'John',
surname: 'Doe',
age: 30,
sayHello: function() {
console.log('Hello ' + this.name + ' ' + this.surname);
}
};
employee.sayHello();
function makeEmployee(name, surname, age) {
return {
name: name,
surname: surname,
age: age,
sayHello: function() {
console.log(`Hello ${this.name} ${this.surname}`);
}
};
};
makeEmployee('John', 'Doe', 30).sayHello();
...lub bardziej elegancko przy użyciu obiektu specyfikującego
function makeEmployee(spec) {
return {
name: spec.name,
surname: spec.surname,
age: spec.age,
sayHello: function() {
console.log(`Hello ${this.name} ${this.surname}`);
}
};
};
makeEmployee({name: 'John', surname: 'Doe', age: 30}).sayHello();
const employee = {
name: '',
surname: '',
sayHello: function() {
console.log(`Hello ${this.name} ${this.surname}`);
}
};
const john = Object.create(employee);
john.name = 'John';
john.surname = 'Doe';
john.sayHello(); // -> Hello John Doe
Każda zmiana w prototypie jest natychmiast widoczna w obiektach dziedziczących.
employee.sayHello = function() {
console.log('Hello ' + this.name);
};
john.sayHello(); // -> Hello John
W obiektach JavaScriptowych nie ma pól (a więc i metod) prywatnych, ale można ukryć pola stosując technikę domknięć (closure).
function makeEmployee(spec) {
const name = spec.name;
const surname = spec.surname;
return {
sayHello: function() {
console.log(`Hello ${name} ${surname}`);
}
};
};
var john = makeEmployee({name: 'John', surname: 'Doe'});
john.sayHello(); // -> Hello John Doe
john.name; // undefined
john.surname; // undefined
Tablice w języku JavaScript są obiektami o pewnej specjalnej charakterystyce:
const t = [];
t[0] = 123; // --> [123]
t[5] = 3; // --> [123, 4 empty slots, 5]
t[6] = 'foo'; // --> [123, 4 empty slots, 5, 'foo bar']
t.foo = 'bar'; // --> [123, 4 empty slots, 5, 'foo bar']
console.log(t.foo); // --> bar
const fibb = [1,2,3,5,8,13];
const bag = [1,5,'foo',{}, {name: 'Doe'}, [1,2,3]];
Iterowanie klasyczne:
const fibb = [1,2,3,5,8,13];
for(var i = 0; i < fibb.length; ++i) {
console.log(`${i}: ${fibb[i]}`);
};
Iterowanie funkcyjne:
const fibb = [1,2,3,5,8,13];
fibb.forEach(function(val, i) {
console.log(`${i}: ${val}`);
});
let price = 100.99;
podobnie jak var
, ale zasięg blokowy zamiast funkcyjnego
const multiplier = 3;
podobnie jak let
, lecz raz przypisanej wartości nie można zmienić
const price: number = 99; // ok
const price: number = 'foo'; // błąd kompilacji
boolean
number
string
number[]
, string[]
- typ tablicowyArray<number>
, Array<string>
- także typ tablicowy[string, number, number, boolean]
- krotka
enum Color {Red, Green, Blue};
let color: Color = Color.Green;
console.log(Color[1]); // -> "Green"
console.log(Color["Green"]); // -> 1
Typ enumeracyjny jest kompilowany do tablicy zawierającej mapowanie liczb na nazwy i na odwrót.
enum Color {Red, Green, Blue};
console.log(Color); // -> {"0": "Red", "1": "Green", "2": "Blue", Red: 0, Green: 1, Blue: 2}
Typescript pozwala na deklarowania typów jako unii wartości
type Color = 'Red' | 'Green' | 'Blue';
let color: Color;
color = 'Red'; // ok
color = 'foo'; // transpilation error
W wielu miejscach stosowanie specyfikatorów typów nie jest konieczne. TypeScript zawiera mechanizm type inference.
let price = 0; // wnioskowanie -> number
price = 'foo'; // błąd kompilacji
Funkcje można obecnie specyfikować zwięźlej stosująć notację "strzałkową" (fat arrow).
const add = function(left, right) {
return left + right;
};
const add2 = (left, right) => {
return left + right;
};
// lub zwięźlej
const add3 = (left, right) => left + right;
Znacząco zwiększa to czytelność kodu funkcyjnego:
[1,2,3,4,5].map((value) => 2 * value); // [2,4,6,8,10]
[1,2,3,4,5].filter((value) => value < 4); // [1,2,3]
Notacja fat arrow zmienia sposób wyliczania zmiennej this
!
Parametry oraz rezultat mogą być typizowane.
const add = function(left: number, right: number): number {
return left + right;
};
const add2 = (left: number, right: number): number => {
return left + right;
};
// lub zwięźlej
const add3 = (left: number, right: number) => left + right;
Zmienne typu funkcyjnego mogą być dokładnie wyspecyfikowane:
const addFn: (left: number, right: number) => number;
addFn = (left: number, right: number) => left + right; // OK
addFn = () => "Foo"; // błąd kompilacji
const totalPrice = (items: {price: number}[]) => {
let sum = 0;
items.forEach((item) => sum += item.price);
return sum;
};
totalPrice([{price: 4.99, name: 'Bread'},
{price: 12.99, name: 'Butter'}]); // -> 17.98
totalPrice([{name: 'Apple'}, {name: 'Orange'}]); // -> błąd kompilacji
Interfejs m.in. pozwala unikać redundantnych deklaracji:
interface Priceable {
price: number; // pole obowiązkowe
name?: string; // pole opcjonalne
};
const totalPrice = (items: Priceable[]) => ...
Brak klas w JavaScript jest uciążliwy. ECMAScript 6 i TypeScript wprowadzają pojęcie klasy.
class Product {
constructor(public name: string, public price: number) {
}
getPriceTag() {
return `${name} [${price}]`;
}
};
Dostępne są następujące modyfikatory dostępu stosowane zarówno do metod jak i pól:
public
protected
private
public
class Product {
constructor(private name: string, private price: number) {
}
getPriceTag() {
return `${name} [${price}]`;
}
};
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
const sam = new Snake("Sammy the Python");
const tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
interface Person {
name: string;
surname: string;
age: number;
}
const johnDoe: Person = {
name: 'John',
surname: 'Doe',
age: 44
};
let x: 'foo' | 'bar';
x = 'foo';
x = 'some other value'; // <-- błąd kompilacji
let y: string | number;
y = 'foo';
y = 123;
y = true; // <-- błąd kompilacji
interface Person {
name: string;
age: number;
}
interface Employee {
employeeNumber: string;
}
const johnDoe: Person & Employee = {
name: 'John Doe',
age: 44,
employeeNumber: '123-234-456'
};