Webtechnologien Wintersemester 2024

 

  • JavaScript, aka Mocha, aka LiveScript, aka JScript, aka ECMAScript ist eine der weltweit populärsten Programmiersprachen. Nahezu jeder PC hat mindestens einen JavaScript-Interpreter installiert und unter Verwendung.
  • Es ist die Skriptsprache des Webs, aber mitterweile auch an anderen Orten zu Hause: Stark auf dem Server, mit React Native in nativen Apps, zunehmend bedeutend im IoT-Bereich, aber auch auf Arduino oder Apple OSX usw.

kleines Beispiel-Programm

// Vieles ist wie in anderen Sprachen …
let names = ['Alice', 'Bob', 'Carol', 'Dave'];

for (let name of names) {
   if (name.length > 3)
      console.log(`${name} ist ein kurzer Name.`);
   else
      console.log(`${name} ist ein langer Name.`);
}

// … mit kleinen Eigenheiten
let sizeOfAllNames = names.reduce((sum, name) => sum + name.length, 0);
console.log(`Alle Namen sind zusammen ${sizeOfAllNames} Zeichen lang.`);
  • C-Syntax
  • Variablen, Schlüsselwörter, Kontrollstrukturen
  • Literale für Arrays und Objekte
  • vieles simpel und aus anderen Sprachen bekannt, ein paar Sachen deutlich anders und gewöhnungsbedürftig
  • darum schauen wir uns die Geschichte an …

Geschichte

NotJavaScript

  • Von Brendan Eich für Netscape entwickelt
  • Aus Marketinggründen Java im Name
  • interpretiert, schwach typisiert, dynamisch, funktional und objektorientiert
  • Prototypische Vererbung (statt klassischer)
  • Läuft überall, aber einzige Sprache im Browser (in einer Sandbox)
    1. JavaScript entstand 1994 aus der Notwendigkeit heraus, Web-Sites dynamisch zu machen. Doch statt wie andere Sprachen Jahre der Reifung zu haben, legte der mit der Erstellung beauftragte Brendan Eich nach nur knapp zwei Wochen einen Entwurf von Mocha – später LiveScript – samt Beispielimplementation vor, der von den unter Druck stehenden Netscape-Managern als gut genug befunden und ausgeliefert wurde. Aus Marketing-Gründen (Java-Fame) wurde die Sprache später zu JavaScript umbenannt. Sie ist das Kind dreier Eltern: Java, Scheme und Self – sie hat Javas Syntax, Schemes Funktions-Modell und die prototypischen Objekte aus Self. Sie ist zu einer der weitest verbreiteten Sprachen der Welt geworden. Nahezu alle PCs, Tablets und Smartphones enthalten einen JavaScript-Interpreter.
    1. Der große Vorteil von JavaScript ist nicht Prototypische Vererbung, sondern klassenlose Objekte.
    1. JavaScript wird vom Browser in einer Sandbox ausgeführt, wodurch es keinen Zugriff auf andere Tabs, Fenster oder das Dateisystem hat und keine Einstellungen ändern oder Software installieren kann. Wird ein Tab oder Fenster geschlossen, werden auch alle darin laufenden Scripte beendet. Werden in JavaScript große Mengen an Daten verarbeitet, wird gleichsam riskiert, dass das Interface nicht mehr reagiert, da JavaScript grundlegend single-threaded ist. Es kann also jeweils nur eine Sache abgearbeitet werden. (Um das zu umgehen, gibt es WebWorker, in denen Scripts in ihrem eigenen Prozess, im Hintergrund und getrennt von der Website ausgeführt werden können.)

Geschichte – Trademark

JavaScript is a Trademark of ORACLE

  • Auf der Suche nach einem Partner, um JavaScript zu standardisieren, stieß Netscape auf ECMA, die European Computer Manufacturers Association. Da das Java in JavaScript jedoch von Sun lizensiert war, konnte der Standard nicht so benannt werden und letztlich wurde der interne Arbeitstitel übernommen: ECMAScript. Der Standard wird mittlerweile jährlich aktualisiert.
  • Hat nichts mit Java zu tun, aber eine syntaktische Nähe. So wie Java zu C.
  • Aber es ist besser in dem Bereich, für den Java mal gedacht war (Web, Interoperabilität)
  • ECMAScript (Standard)
  • JScript (Microsoft)
  • Amerikanisches Recht: Trademarks müssen verteidigt werden, sonst verfallen sie.

  • JS war nie so gedacht, wie es heute eingesetzt wird …

Geschichte – Absicht

The by-design purpose of JavaScript, was to make the monkey dance when you moused over it.
Scripts were often a single line. We considered ten line scripts to be pretty normal, hundred line scripts to be huge, and thousand line scripts were unheard of. The language was absolutely not designed for programming in the large, and our implementation decisions, performance targets, and so on, were based on that assumption.

Eric Lippert

  • Sprachen werden mit Absicht entwickelt und in JS wurde viel weggelassen, was in großen Anwendungen Kosten erzeugt, in kleinen aber die ganze Zeremonie verkürzt
  • designed für Programme, die ein Mensch überblicken kann
  • darum (lange) kein Modularisierungs-System, (keine klassische Vererbung), keine Interfaces, keine Verkapselung, keine Annotations etc
  • leises Fehlersystem, um Benutzer zu schonen (und weil Fehlerkosten als gering eingeschätzt wurden)

Syntax

Kontrollstrukturen

// Kommentar
/* Kommentar */

if (1 >= 2) {
   console.log("Nope");
}
else if (1 == 1) {
   console.log("Yep");
}
else {
   console.log("Nope");
}
  • schneller Ritt durch die Syntax
  • Leerzeichen haben keine Bedeutung
  • Aufrufe sollten mit Semikolon enden (müssen aber nicht unbedingt)
  • gibt auch switch, case

Operatoren

// logische Operatoren
if (username && password) do_stuff();

let name = user && user.getName();
let name = user.name || "default";

// Unäre Operatoren
!!user.name // true / false
+"300" // 300
i++

// Ternäre Operatoren
bedingung ? anweisung() : anweisung2();
numerus = people.length == 1 ? "Person" : "Personen";
  • gibt neuerdings auch: ??

Schleifen

while (bedingung) { anweisungen; }

do { anweisungen; } while (bedingung);

for (let i = 0; i < 100; i++) { anweisungen; }
  • gibt neuerdings auch for of und for in;

Variablen

  • mitterweile gibt es 3 Arten, Werte zwischenzuspeichern
var eins = 1; // ⚠️ function scope

zwei = 2; // ⚠️ automatisch global

let drei = 3; // block scope, veränderbar
drei = 4;

const vier = 4; // block scope, unveränderbar
vier = 5; // 🛑 Zonk
  • ok, kommen wir zu variablen, jetzt wird es schwieriger
  • schwach typisiert
  • case sensitive
  • sollten immer deklariert werden, vor allem wenn nicht global gemeint -> leicht, Fehler zu machen

Typen

  • Number
  • String
  • Boolean
  • Function
  • Object
  • Object
    • Function
    • Array
    • Date
    • RegExp
  • null
  • undefined
  • False: undefined, null, 0, false, NaN, ''
  • String: sind auch Objekte und haben auch Funktionen
  • Boolean: false, 0, "", NaN, null, undefined
  • Kontrollstrukturen: if () {} else {}
  • Funktionen: Bürger erster Klasse, function scope (Menschenrechte / Bürgerrechte), returned undefined
  • Objekte / Arrays: array.pop() etc
    • → Da so leicht zu benutzen (leichter als z.B. in Java), ist die Hürde geringer
  • neue Typen, für uns aber irrelevant: Symbol, Map, Set, WeakMap, WeakSet, TypedArrays wie Int8Array u.ä.

Numbers

3     // 3
3.14  // 3.14
9 / 2 // 4.5
2 * (3 + 4) // 14
0.5 + 0.5 // 1
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
  • besondere Zahlen: Infinity, -Infinity, NaN, Number.MAX_SAFE_INTEGER, Number.MAX_VALUE
  • neu: BigInt
  • Number: es gibt nur einen typen für zahlen und der ist immer immer float (double, 64-bit)
  • es gibt keine Integer
  • der dezimalpunkt wird nur nicht angezeigt, wenn danach nichts folgt
  • I’ve got 99.000004 problems, and Integers are one.0.1 und 0.2 lassen sich nicht endlich darstellen und müssen abgeschnitten werden

String

let vorname = "Alice";
let nachname = 'Bob';
let full = vorname + ' ' + nachname;
let satz = `Der ganze Name ist ${vorname} ${nachname}`;
  • kein unterschied zwischen ' und " - für Java-Entwickler anfänglich schwierig, aber ' wird tendenziell bevorzugt;
  • Template Literals (können alle Browser außer IE)
  • + = Addition und String Concatination in einem
  • was auch Nachteile mit sich bringt …

Type Coersion

1 + 2 + "3" // "123"
1 - 2 - "3" // -4
1 * 2 * "3" // 6

1 == "1" // true
1 === "1" // false

"a" < "b" // true
  • schwach und dynamisch typisiert
  • Vergleich: == / === (type coercion)

Funktionen

function hallo (name = 'unbekannt') {
   alert(`Hallo ${name}!`);
}

let plus = function (a, b) { return a + b; }
plus(1, 2); // 3

let plus = (a, b) => a + b; // arrow function

'abcdefg'.charAt(2).toUpperCase(); // 'C'
'abcdefg'['charAt'](2)['toUpperCase'](); // 'C'

Literale für Arrays und Objekte

let namen = ['Alice', 'Bob'];

let person = {
   name: 'Alice',
   alter: 42
}

Verschachtelte Objekte und Arrays

let brot = { // Object
   name: "Matze", // Property
   zutaten: [ // Array
      { "name": "Mehl", "menge": 2 },
      { name: "Wasser", menge: 1 }
   ],
   getRezept: function () { // Method
      return this.zutaten[0].name +
         " + " + this["zutaten"][1]["name"] +
         " = " + this.name;
   }
};

// member operator
brot.name === brot["name"];
  • für alle die JSON kennen kein ungewöhnliches Bild, außer mit der enthaltenen Funktion

Objekte – Konstruktor-Funktion

function Person(name, alter) {
   this.name = name;
   this.alter = alter;
}
function introduce(person) {
   return "Hallo, ich heiße " + person.name +
          " und bin " + person.alter + " Jahre alt.";
}

let alice = new Person('alice', 42);
alice.alter == 42;
introduce(alice);
  • jetzt wird es seltsam – es gibt keine klassischen klassen, sondern prototypen
  • new und this gehören eng zusammen
  • new erzeugt neues, leeres Objekt und ruft die Funktion darauf auf
  • Funktionen, die mit new aufgerufen werden, heißen Konstruktor-Funktionen

Objekte – Konstruktor-Funktion

function Person(name, alter) {
   this.name = name;
   this.alter = alter;

   this.introduce = function() {
      return "Hallo, ich heiße " + this.name + " und bin " + this.alter + " Jahre alt.";
   }
}

let alice = new Person('Alice', 42);
alice.introduce();

Objekte – Konstruktor-Funktion

function Person(name, alter) {
   this.name = name;
   this.alter = alter;
}

Person.prototype.introduce = function() {
   return "Hallo, ich heiße " + this.name + " und bin " + this.alter + " Jahre alt.";
}

let alice = new Person('Alice', 42);
alice.introduce();
  • lookup chain oder hier prototype chain
  • Prototypen können jederzeit verändert (und damit erweitert) werden
  • Beispiel: toString oder
String.prototype.reversed = function() {
   let r = '';
   for (let i = this.length - 1; i >= 0; i--) {
      r += this[i];
   }
   return r;
};
// oder in funktionaler Eleganz:
String.prototype.reversed = function() {
   return this.split('').reverse().join('');
};

Klassen

class Person {
  constructor(name, alter) {
   this.name = name;
   this.alter = alter;
  }

  introduce() {
     return `Hallo, ich heiße ${this.name} und bin ${this.alter} Jahre alt.`;
  }
}

let alice = new Person('Alice', 42);
alice.introduce();
  • wiederverwendbare Funktionalität zu bündeln war für Anwender kompliziert, darum wurden doch Klassen eingeführt, was aber nur Syntax-Zucker ist

Zeit

  • kein Thread.sleep wie in Java
  • nur Callbacks, die nach gewisser Zeit aufgerufen werden:
    • setTimeout(funktion, verzögerung, params) → ID
    • setInterval(funktion, verzögerung, params) → ID
function sayTime() { console.log(`Es ist ${new Date().toLocaleTimeString()}`); }
sayTime();
// setzen
const idT = setTimeout(sayTime, 1000); // einmal nach ~1s
const idI = setInterval(sayTime, 5000); // alle ~5s
// löschen
clearTimeout(idT);
clearInterval(idI);
  • so, sanfter Abchluss
  • Zeit wird anders behandelt als z.B. Java, da wegen Single-Process kein sleep möglich
  • Themen wie Zeit, Synhronität und Asynchronität groß und wichtig
  • heutzutage: async functions, mit denen fluss kontrolliert werden kann
  • im Browser auch requestAnimationFrame und requestIdleCallback
  • in Node auch setImmediate

Modernes JavaScript

  • Seit ES6 / ECMAScript 2015 gibt es jährliche Updates zu JS
  • neue Features können über ein mehrstufiges Verfahren zum Standard hinzukommen
  • React braucht keine besonderen Features, sie machen die Entwicklung aber deutlich leichter
  • darum schauen wir sie uns an, dieser Foliensatz dient aber auch, um später noch einmal nachschlagen zu können

Array methods

[1, 2, 3].includes(2); // true
[1, 2, 3].find(value => value === 2); // 2
[1, 2, 3].some(value => value === 2); // true
[1, 2, 3].every(value => value === 2); // false
[1, 2, 3].filter(value => value !== 2); // [1, 3]
[1, 2, 3].map(value => value * 2); // [2, 4, 6]
[1, 2, 3].reduce((sum, value) => sum + value, 0); // 6
  • statt for loops und if/else

destrukturierende Zuweisung

const anschrift = {
   strasse: 'Schwarzstraße 13-15',
   plz: '5020',
}

const strasse = anschrift.strasse; // alt
const { plz } = anschrift; // neu

const adresse = ['Schwarzstraße 13-15', '5020', 'Salzburg', 'Österreich'];

const [strasse, plz, stadt] = adresse; // auch mit arrays

console.log(`Post bitte an ${strasse} in ${plz} ${stadt}`);
  • Die destrukturierende Zuweisung ermöglicht es, Daten aus Arrays oder Objekten zu extrahieren, und zwar mit Hilfe einer Syntax, die der Konstruktion von Array- und Objekt-Literalen nachempfunden ist.

Standard Funktionsparameter

const add = (a, b) => {
   a = a === undefined ? 0 : a; // meh
   b = b === undefined ? 0 : b;
   return a + b;
};

const add = (a = 0, b = 0) => a + b; // yeah

add() === 0
add(1) === 1
add(undefined, 2) === 2

function greetUser ({ name = 'unbekannt' }) {} // auch für Objekte
  • nur für undefined, nicht für null
  • Standard Funktionsparameter erlaubt es, formale Parameter mit vorgegebenen Werten zu initialisieren, wenn beim Funktionsaufruf kein Wert oder undefined übergeben wird.

Shorthand property names

let vorname = 'Alice';
let alterInJahren = 42;

const lang = { vorname: vorname, alterInJahren: alterInJahren }; // 😕

const kurz = { vorname, alterInJahren }; // 🙂

Rest ...

const adresse = ['Schwarzstraße 13-15', '5020', 'Salzburg', 'Österreich'];

const [strasse, ...rest] = adresse;

console.log(rest); // ['5020', 'Salzburg', 'Österreich']

const charMap = { a: 1, b: 2, c: 3, d: 4 };

const { b, d , ...rest} = charMap;

console.log(rest); // { a: 1, c: 3 }
  • Mit der Rest Parameter Syntax kann man beliebig viele Parameter als Array empfangen.

Spread ...

Math.max(3, 2, 5, 4) // 5

const values = [1, 5, 3, 9, 4];

Math.max(...values) // 9

const newArray = [1, 2, 3, ...values] // [1, 2, 3, 1, 5, 3, 9, 4]

const defaultValues = { name: 'unbekannt', anschrift: 'unbekannt' };
const person = { name: 'Alice' }
const merged = { ...defaultValues, ...person };
console.log(merged); // { name: 'Alice', anschrift: 'unbekannt' }
  • Die Spread-Syntax erlaubt es, einen einzelnen Array-Ausdruck oder String an Stellen zu expandieren, an denen Null oder mehr Argumente (für Funktionsaufrufe) oder Elemente (für Array-Literale) erwartet werden, oder einen Objekt-Ausdruck an Stellen zu expandieren, an denen Null oder mehr Schlüssel-Wert-Paare (für Objektliterale) erwartet werden.

ESModules export / import

utils.js

const add = (a, b) => a + b;
export add;
export default add;
export const apiURL = 'https://example.com/api';

app.js

import add from './utils';
import { add, apiURL } from './utils';
import('./utils').then(utils => { /* do something */ });
  • Das export-Statement wird verwendet, um Funktionen und Objekte aus einer gegebenen Datei (oder Modul) zu exportieren.
  • Das import Statement wird verwendet um Funktionen, Objekte und Primitives zu importieren die von einem externen Modul, einem anderen Script, etc. exportiert wurden.

Nullish coalescing operator ??

function showAmount (value) {
   const amount = value || 'unbekannt';
   console.log(amount);
}

showAmount(null) // 'unbekannt'
showAmount(1000) // 1000
showAmount(0) // 'unbekannt' 😱

function showAmount (value) {
   const amount = value ?? 'unbekannt'; // nur bei `null` und `undefined`
   console.log(amount);
}

showAmount(0) // 0 🙂

Optionale Verkettung ?.

function (user) { // user === null
   console.log(user.account.iban)
   // -> TypeError: Cannot read property 'account' of undefined
   
   if (user && user.account && user.account.iban) {
      console.log(user.account.iban) // 😕
   }

   console.log(user?.account?.iban) // 🙂   

   user?.accounts?.[0].iban?.format?.() // 😶
}
  • Der Optionale-Verkettungs-Operator (Optional Chaining) ?. ermöglicht es, einen Wert aus einer Eigenschaft tief innerhalb einer Verkettung von Objekt-Eigenschaften auszulesen, ohne dabei explizit überprüfen zu müssen, ob jede Referenz in der Kette valide ist.
  • gibt im Fehlerfall (null oder undefined) undefined zurück
  • häufige Verwendung Zeichen für mangelndes Vertrauen in den Code / die API und Indiz für größeres Problem

Promises mit async / await

async function getUser (id) {
   if (!userCache[id]) {
      const response = await fetch(`${apiURL}/users/${id}`)
      const json = await response.json();
      userCache[id] = json.data;
   }

   return userCache[id];
}

const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));

console.log('1');
await sleep(1000); // sleep 1 second
console.log('2');

  • Wir haben Promises schon kennen gelernt, aber intensivieren es jetzt
  • Promises sind in den letzten Jahren fester Bestandteil geworden und alle neuen (asynchronen) APIs bauen auf Promises auf
    • viele neue APIs sind auch asynchron, um den Main-Thread zu entlasten – vor allem wichtig, um das Web auf schwächeren Phones relevant zu halten
  • Callbacks trotzdem noch relevant
    • Promises können nur 1 mal resolven, also für 1 Operation
    • Callbacks können mehrmals aufgerufen werden, also für wiederkehrende Ergebnisse (e.g. Event-Handler, click, watchPosition etc)
  • Die async function-Deklaration definiert eine asynchrone Funktion, die immer ein Promise zurück gibt (also mit await aufgerufen oder mit .then() genutzt werden muss)
  • Der await Operator wird genutzt, um auf einen Promise zu warten. Er kann nur in einer async Funktion benutzt werden.