Projekt i implementacja systemów webowych

Wykład 2: języki JavaScript i TypeScript

mgr inż. Maciej Małecki

Historia

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

Good Parts

JavaScript łączy w sobie trzy koncepcje:

  • funkcyjność wzorowaną na języku Scheme,
  • obiektowość wzorowana na języku Smalltalk,
  • składnię wzorowaną na języku Java.

Język JavaScript nie jest wariantem języka Java!!!

Cechy języka

  • Przenośność - identyczne działanie niezależnie od platformy sprzętowej.
  • Dynamiczna typizacja - typ nie jest ściśle przypisany do konkretnej zmiennej lub wyrażenia.
  • Jednowątkowość

Dlaczego JavaScript jest ważny?

  • dostępny wszędzie
  • każdy użytkownik z reguły posiada przeglądarkę
  • automatyczna aktualizacja

Najprostszy "przypadek użycia"

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

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
            
  • Niezainicjowania zmienna przyjmuje wartość undefined.
  • Słowo kluczowe var|let jest opcjonalne, zaleca się jednak je stosować.

Typy danych

  • JavaScript jest językiem dynamicznie typizowanym - typ zmiennej lub wyrażenia nie jest znany podczas kompilacji.
  • Podczas wykonywania kodu typ wartości przechowywanej w zmiennej lub będącej wynikiem wyrażenia jest dobrze określony.

              var a;
              a = 2 + 2;
              console.log(typeof(a)); // -> number
              a = 'foo';
              console.log(typeof(a)); // -> string
            

Typy danych

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'}

Funkcje

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
               

Zmienne globalne

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 !!!
              

Zmienne globalne

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

Zmienne globalne


              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

Zmienne lokalne

Wewnątrz funkcji zawsze deklarujemy zmienne z użyciem słowa var (let, const), gdyż wtedy stają się one zmiennymi lokalnymi.

Hoisting

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
                }
              };
            

Domknięcia


              const nextValue = function () {
                let cntr = 0;
                return function() {
                  ++cntr;
                  return cntr;
                };
              }();
              console.log(nextValue()); // -> 1
              console.log(nextValue()); // -> 2
            
  • Funkcja ma dostęp do parametrów i zmiennych widocznych w miejscu, w którym owa funkcja została zadeklarowana.*
  • Owe parametry i zmienne są dostępne nawet wówczas, gdy funkcja wewnętrzna ma dłuższy czas życia niż funkcja zewnętrzna.
  • Mechanizm ten nazywamy domknięciem (ang. closure).

* Wyjątkiem są zmienne this oraz arguments.

Instrukcje kontrolne

Instrukcja warunkowa


              if(a === 3) {
                 ...
              } else {
                 ...
              }
            
  • Składnia jest identyczna jak w języku C lub Javie.
  • Wyrażenie nie musi być typu boolean.
  • Dla każdego wyrażenia w instrukcji if JavaScript określa czy jest ono truthy czy też falsy.
  • Do porównywania tożsamościowego stosujemy wyłącznie operatory === oraz !==.

Wartości falsy i truthy

Następujące wartości są falsy:

  • false
  • null
  • undefined
  • Pusty napis: ''
  • Liczba 0
  • Liczba NaN

Pozostałe wartości są truthy wliczając w to true, napis 'false' oraz dowolną wartość obiektową.

Instrukcje pętli


              while (expression) {
                 ...
              };
              do {
                 ...
              } while (expression);
              for (let i = 0; i < size; ++i) {
                 ...
              };
            
  • Wyrażenie expression podobnie jak w przypadku instrukcji warunkowej obliczane jest do wartości falsy i truly.
  • Koniecznie należy pamiętać o deklarowaniu zmiennej indeksującej w pętli for.

Obiekty

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]);
              };
            

Metody

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;
              }
            };
          

Zagadka


              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

Bezklasowość

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.

Tworzenie obiektów

Sposób 1: literał obiektowy


              var employee = {
                name: 'John',
                surname: 'Doe',
                age: 30,
                sayHello: function() {
                  console.log('Hello ' + this.name + ' ' + this.surname);
                }
              };
              employee.sayHello();
            

Sposób 2: funkcja fabrykująca


              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();
              

Sposób 3: dziedziczenie prototypiczne


              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
            

Sposób 4: funkcja fabrykująca i pola prywatne

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

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}`);
                });
              

Język TypeScript

Wiemy już, że JavaScript to świetny język, ale...

  • brakuje w nim deklaracji typów
  • typy obiektowe są nieodróżnialne
  • brakuje klas przez co tworzenie wielu instancji podobnych obiektów jest utrudnione

TypeScript

  • Język kompilowany do JavaScript stworzony i rozwijany przez Microsoft.
  • Język ten obecnie jest oficjalnym "poligonem doświadczalnym" dla JavaScriptu.
  • Duża część elementów TS przeszła do JS w ramach (ES2015).
  • Dzięki kompilacji można korzystać z rozszerzeń ECMA na przeglądarkach, które tej wersji JS jeszcze nie wspierają.
https://www.typescriptlang.org/play/index.html

Deklaracja zmiennych i stałych


              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ć

Typy danych

Specyfikacja typów

const price: number = 99; // ok
const price: number = 'foo'; // błąd kompilacji

Podstawowe typy

  • boolean
  • number
  • string
  • number[], string[] - typ tablicowy
  • Array<number>, Array<string> - także typ tablicowy
  • [string, number, number, boolean] - krotka

Enumeracje


              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}
            

Bieda-enumeracje

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
            

Type inference

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
            

Fat arrow

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!

Specyfikacja sygnatury funkcji

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
          

Interfejsy i typy obiektów


            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[]) => ...
          

Klasy

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}]`;
              }
            };
          

Modyfikatory dostępu

Dostępne są następujące modyfikatory dostępu stosowane zarówno do metod jak i pól:

  • public
  • protected
  • private
  • (brak) - przyjmuje się dostęp jak w public

            class Product {
              constructor(private name: string, private price: number) {
              }
              getPriceTag() {
                return `${name} [${price}]`;
              }
            };
          

Dziedziczenie


            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);
          

W praktyce klasy wykorzystywane są rzadko


            interface Person {
              name: string;
              surname: string;
              age: number;
            }

            const johnDoe: Person = {
              name: 'John',
              surname: 'Doe',
              age: 44
            };
          

Unie typów (union)


            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
          

Przecięcia typów (intersection)


            interface Person {
              name: string;
              age: number;
            }
            interface Employee {
              employeeNumber: string;
            }

            const johnDoe: Person & Employee = {
              name: 'John Doe',
              age: 44,
              employeeNumber: '123-234-456'
            };
          

Co jeszcze?

  • Type inference
  • Generics
  • Getters and setters
  • Function overloading
  • Type guards
https://www.typescriptlang.org/docs