Bisher: JavaScript als eine Skriptsprache mit diversen Macken

Typsystem mit fragwürdigen Konvertierungen
When faced with either doing something nonsensical or aborting with an error, it will do something nonsensical. Anything is better than nothing. (Lexy Munroe über PHP, aber es passt hier hervorragend)
Ein “Ökosystem”, welches sich rasend schnell verändert
Look, it’s easy. Code everything in Typescript. All modules that use Fetch compile them to target ES6, transpile them with Babel on a stage-3 preset, and load them with SystemJS. If you don’t have Fetch, polyfill it, or use Bluebird, Request or Axios, and handle all your promises with await. (Jose Aguinaga im Jahr 2016, die Situation hat sich nicht merklich gebessert)

Ab Jetzt: Mehr Schönheit wagen (außerdem Objektorientierung)

In JavaScript steckt ein wunderschöner Kern, den man allerdings mit ein bisschen Sorgfalt freilegen muss
Insbesondere neuere Sprachversionen fügen innovative und sinnvolle Funktionen hinzu

Design patterns are bug reports against your programming language

Peter Norvig (Paraphrasiert und Erläutert, Vortrag)

Der “Spread”-Operator (...)

Ersetzt eine Liste durch eine Sequenz von komma separierten Werten
  • Hilfreich beim Aufruf von Funktionen (statt Function.apply)
  • Hilfreich beim Kombinieren von Arrays
  • Hilfreich beim “Destructuring”
  • javascript_advanced/operator-spread.js
    const n1 = ["Julian", "Richard", "Anne", "George", "Timmy"];
    const n2 = ["Tim", "Karl", "Willi", "Gabi"];
    console.log(["Justus", "Peter", "Bob", ...n1, ...n2]);
    
    const add = (lhs, rhs) => lhs + rhs;
    const numbers = [1, 2];
    
    console.log(add(...numbers));
    console.log(add(...n1));
  • [ 'Justus',
      'Peter',
      'Bob',
      'Julian',
      'Richard',
      'Anne',
      'George',
      'Timmy',
      'Tim',
      'Karl',
      'Willi',
      'Gabi' ]
    3
    JulianRichard
    

Destructuring

Erlaubt das ein oder auspacken von Werten aus Arrays oder Objekten
Vorsicht: Erzeugt implizit globale Variablen wenn die Variable nicht vorher eingeführt wurde
Bei Arrays: Kombination mit dem Spread-Operator möglich
Gespreizter Wert steht dann für die Restliste
Bei Objekten: Klammern um Ausdruck zwingend notwendig
Außerdem Verwendung des Spread-Operators noch nicht standarisiert
  • javascript_advanced/operator-destructuring.js
    let a = 0, b = 1, r = 2;
    [a, b] = [10, 20];
    console.log("1) a =", a);
    console.log("1) b =", b);
    console.log("1) r =", r);
    
    [a, b] = [b, a]
    console.log("2) a =", a);
    console.log("2) b =", b);
    
    [a, b, ...r] = [30, 40, 50, 60, 70]
    console.log("3) a =", a);
    console.log("3) b =", b);
    console.log("3) r =", r);
    
    ({a, b} = { a: 80, c: 90 }); // Round braces required!
    console.log("4) a =", a);
    console.log("4) b =", b);
  • 1) a = 10
    1) b = 20
    1) r = 2
    2) a = 20
    2) b = 10
    3) a = 30
    3) b = 40
    3) r = [ 50, 60, 70 ]
    4) a = 80
    4) b = undefined
    

Closures

Da Funktionen Daten sind: Funktionen können Funktionen berechnen
  • Die innere Funktion hat dabei Zugriff auf die Variablen der äußeren Funktion
  • Die so sichtbaren Daten werden an das neue Funktionsobjekt gebunden und stehen so auch nach dem Verlassen der äußeren Funktion noch zur Verfügung
Im Beispiel: Innere anonyme Funktion (obj => obj[name]) greift auf einen Parameter der äußeren Funktion zu
Der eingeschlossene Wert könnte auch eine lokale Variable sein
  • intro_javascript/closure-intro.js
    // Nutzung von Closure-Variable (name)
    const extractField = function(name) {
      return (obj => obj[name]);
    }
    
    const extractName = extractField("name");
    console.log(extractName({ "name": "Alfred" }));
    
    console.log(extractField("name")({ "name": "Alfred" }));
    
  • Alfred
    Alfred
    

map und filter mit Closures

  • intro_javascript/closure-map.js
    // Nutzung von Closure-Variable (name)
    const extractField = function(name) {
      return (obj => obj[name]);
    }
    
    // Nutzung von Closure-Variable (minLen)
    const minStrLen = function(minLen) {
      return (str => str.length >= minLen);
    }
    
    const complex_data = [
      { name: "Phineas", hair: "red" },
      { name: "Ferb", hair: "green" },
      { name: "Agent P", hair: "green" },
      { name: "Candice", hair: "blonde" },
    ]
    
    const hairLength = 5;
    console.log("Haircolours >= " + hairLength + " chars: ",
      complex_data
        .map(extractField("hair"))
        .filter(minStrLen(hairLength))
    );
    
  • Haircolours >= 5 chars:  [ 'green', 'green', 'blonde' ]
    

Kombination von filter, map und reduce mit Closures

  • intro_javascript/loop-array-combined.js
    const deepCopy = function(obj) {
      // Nicht-so-eleganter-Hack
      return (JSON.parse(JSON.stringify(obj)));
    }
    
    const currentAge = function(person) {
      let currentYear = new Date().getFullYear();
      person.age = currentYear - person.born
      return (person);
    }
    
    // Nutzung von Closure-Variable (fieldName)
    const sumField = function(fieldName) {
      return ((akku, obj) => akku + obj[fieldName]);
    }
    
    // 2x Nutzung von Closure-Variable (fieldName, len)
    const minLength = function(fieldName, len) {
      return (obj => obj[fieldName].length >= len)
    }
    
    const complex_data = [
      { name: "Matt", hair: "brown", born: 1978 },
      { name: "Chris", hair: "brown", born: 1978 },
      { name: "Dominic", hair: "blonde", born: 1977 },
    ]
    
    console.log(
      complex_data
        .map(deepCopy)
        .map(currentAge)
        .filter(minLength("name", 5))
        .filter(person => person.age > 39)
        .reduce(sumField("age"), 0)
    );
    
  • 83
    

Das Funktions-Objekt

Bereits bekannt: Funktionsdefinitionen lassen sich einer Variablen zuweisen
Typ der Variablen ist dann function, aufruf “wie gewöhnlich” mit myVar()
Neu: Aufruf der Funktion auch über Funktionen des Funktions-Objekts möglich
Auf diesem Weg kann der this-Kontext gesetzt werden
Alle Funktionen haben einen this-Kontext, der unterschiedlichste Werte annehmen kann
Funktioniert daher nicht wie das von der Programmiersprache verwaltete this in anderen Sprachen
Wert für this kann für jeden Aufruf aufs neue gesetzt werden
this fungiert somit als weiterer Parameter außerhalb der Parameterliste

Aufruf mit Function.call

Parameter: call(thisArg, arg1, arg2, ...)
Erster Parameter ist der this-Kontext, dann folgen die “normalen” Parameter
Unterschiedliche Verhaltensweisen für function- und Arrow-Funktionen
Arrow-Funktionen lassen keine Veränderungen des this-Kontextes zu
  • javascript_advanced/function-call.js
    const greetFunction = function(job) {
      console.log(`Hallo ${this.name}, du bist ${job}!`);
    };
    
    const greetArrow = (job) => {
      console.log(`Hallo ${this.name}, du bist ${job}!`);
    };
    
    const person = {
      "name" : "Justus"
    };
    
    greetFunction.call(person, "Detektiv");
    greetArrow.call(person, "Detektiv");
    
  • Hallo Justus, du bist Detektiv!
    Hallo undefined, du bist Detektiv!
    

Aufruf mit Function.apply

Parameter: apply(thisArg, [argArray])
Erster Parameter ist der this-Kontext, die Parameter werden als ein Array übergeben
  • javascript_advanced/function-apply.js
    const greetFunction = function(job, car) {
      console.log(`Hallo ${this.name}, du bist ${job} und fährst ${car}!`);
    };
    
    const greetArrow = (job, car) => {
      console.log(`Hallo ${this.name}, du bist ${job} und fährst ${car}!`);
    };
    
    const person = {
      "name" : "Peter"
    };
    
    greetFunction.apply(person, ["Detektiv", "MG"]);
    greetArrow.apply(person, ["Detektiv", "MG"]);
    
  • Hallo Peter, du bist Detektiv und fährst MG!
    Hallo undefined, du bist Detektiv und fährst MG!
    

Aufrufe verzögern mit Function.bind

Parameter: bind(thisArg, arg1, arg2, ...)
Verarbeitung der Parameter exakt wie bei call
Rückgabe: Ein neues Funktionsobjekt mit (auch teilweise) belegten Parametern
Zurückgegebene Funktion muss noch aufgerufen werden
  • javascript_advanced/function-bind.js
    const greetFunction = function(job, car) {
      console.log(`Hallo ${this.name}, du bist ${job} und fährst ${car}!`);
    };
    
    const person = {
      "name" : "Bob"
    };
    
    const partialGreet = greetFunction.bind(person, "Detektiv");
    partialGreet();
    partialGreet("VW Käfer");
    
  • Hallo Bob, du bist Detektiv und fährst undefined!
    Hallo Bob, du bist Detektiv und fährst VW Käfer!
    

Objekte mit Funktionen

Bei Objekten mit Funktionen: this wird an das entsprechende Objekt gebunden
Konsistent mit “freien” Funktionen: Diese sind an das globale Objekt gebunden
Arrow-Funktionen an dieser Stelle unsinnig
Merkregel: function für “Methoden im Sinne einer Klassen” nutzen, Arrow-Funktionen für Funktionen
  • javascript_advanced/function-of-object.js
    const person = {
      name : "Titus",
      job : "Schrotthändler",
      greetFunc : function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      },
      greetArrow : () => {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      }
    }
    
    person.greetFunc();
    person.greetArrow();
    
  • Hallo Titus, du bist Schrotthändler!
    Hallo undefined, du bist undefined!
    

Schachtelung von Objekten mit Funktionen

this-Kontext wird transitiv auch für Kind-Objekte gesetzt
this in Funktionen die Teil von person.address sind zeigen auch auf person.address
  • javascript_advanced/function-of-object-nested.js
    const person = {
      name : "Titus",
      job : "Schrotthändler",
      greet : function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      },
      address : {
        city: "Rocky Beach",
        print : function() {
          console.log(`Adresse: ${this.city}`);
        }
      }
    }
    
    person.greet();
    person.address.print();
    
  • Hallo Titus, du bist Schrotthändler!
    Adresse: Rocky Beach
    

Vorsicht: Objekte mit Funktionen sind keine Klassen

  • javascript_advanced/function-of-object-not-class.js
    const titus = {
      name : "Titus",
      job : "Schrotthändler",
      greet : function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      }
    };
    
    const john = {
      name : "John William Melvin Roger",
      job : "Journalist",
      greet : function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      }
    };
    
    titus.greet();
    john.greet();
    
    
  • Hallo Titus, du bist Schrotthändler!
    Hallo John William Melvin Roger, du bist Journalist!
    
Frage:

Welche Eigenschaft von Klassen fehlen den hier vorgestellten Objekten mit Funktionen?

Antwort:

Keine automatische Instanzierung gleichartiger Objekte möglich, die Methoden müssten für jedes konkrete Objekt wiederholt werden. Daran würde auch ein auslagern der greet-Methode nichts ändern!

Erster Ansatz: Instanziierung über Konstruktorfunktionen

  • javascript_advanced/function-of-object-instanciate.js
    const makePerson = (name, job) => {
      return ({
        "name" : name,
        "job" : job,
        "greet" : function() {
          console.log(`Hallo ${this.name}, du bist ${this.job}!`);
        }
      });
    }
    
    const titus = makePerson('Titus', 'Schrotthändler');
    const john = makePerson('John William Melvin Roger', 'Journalist');
    
    titus.greet();
    john.greet();
    
    
  • Hallo Titus, du bist Schrotthändler!
    Hallo John William Melvin Roger, du bist Journalist!
    
Frage:

Welche Eigenschaft von Klassen fehlen den hier vorgestellten Objekten mit Konstruktorfunktionen?

Antwort:

Vererbung

Klassen mit Funktionsobjekten

Grundidee: Funktionsobjekt als Instanz von Klassen verwenden
Funktionen haben einen this-Kontext, dem man Werte zuweisen kann
Allerdings: Funktionsobjekte werden nicht für Aufrufe, sondern Funktionsdefinitionen angelegt
Benötigt: Mechanismums zum Erzeugen von neuen Objekten
  • javascript_advanced/oop-function-object-idea.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    
      this.greet = function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      };
    
      // Dangerously wrong: Always points to the same function object
      return (this);
    };
    
    const p1 = Person("Alfred", "Erzähler");
    const p2 = Person("Skinny", "Ratte");
    
    p1.greet();
    p2.greet();
    
  • Hallo Skinny, du bist Ratte!
    Hallo Skinny, du bist Ratte!
    

Der new-Operator

Lösung: Erstellen von neuen (Funktions)-Objekten, also Instanzen, mit dem new-Operator
Aufruf einer Konstruktorfunktion, kein return notwendig oder sinnvoll
  • javascript_advanced/oop-function-object.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    
      this.greet = function() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      };
    };
    
    const p1 = new Person("Alfred", "Erzähler");
    const p2 = new Person("Skinny", "Ratte");
    
    p1.greet();
    p2.greet();
    
  • Hallo Alfred, du bist Erzähler!
    Hallo Skinny, du bist Ratte!
    

Definition von Klassen mit Prototypen

Jedes Objekt verfügt über eine prototype-Eigenschaft, dessen Eigenschaften implizit auf dem Objekt verfügbar sind
Dient zur Definition von Klasseneigenschaften
  • Definition von Methoden
  • “Statische” Variablen
Wenn auf dem Objekt eine Eigenschaft nachgefragt wird, erfolgt eine mehrstufige Auflösung
  1. Verfügt das Objekt selbst über die Eigenschaft?
  2. Verfügt das prototype-Objekt über die Eigenschaft?
  3. undefined
  • javascript_advanced/oop-function-object-prototype.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    };
    
    Person.prototype.greet = function() {
      console.log(`Hallo ${this.name}, du bist ${this.job}!`);
    };
    
    const p1 = new Person("Alfred", "Erzähler");
    const p2 = new Person("Skinny", "Ratte");
    
    p1.greet();
    p2.greet();
    
  • Hallo Alfred, du bist Erzähler!
    Hallo Skinny, du bist Ratte!
    

Definition von Klassenvariablen mit Prototypen (static)

Von der Konstruktorfunktion existiert per Definition nur exakt eine Instanz
Auf dieser Instanz können daher global verfügbare Werte gespeichert werden
  • javascript_advanced/oop-function-object-prototype-static.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    
      Person.count++;
    };
    
    Person.count = 0;
    
    Person.prototype.greet = function() {
      console.log(`Hallo ${this.name}, du bist ${this.job}!`);
    };
    
    const p1 = new Person("Alfred", "Erzähler");
    const p2 = new Person("Skinny", "Ratte");
    
    p1.greet();
    p2.greet();
    
    console.log(`Es gibt ${Person.count} Personen`);
    
  • Hallo Alfred, du bist Erzähler!
    Hallo Skinny, du bist Ratte!
    Es gibt 2 Personen
    

Polymorphie und Vererbung

Bisher fehlender Aspekt von Objektorientierung: Polymorphie
Unterschiedliche Verhaltensweisen für Objekte auf Basis ihrer Spezialisierung
Implementierung über eine prototype-Kette
  • Alle Eigenschaften des prototype-Objektes stehen implizit jeder Instanz zur Verfügung
  • prototype-Objekte können ihrerseits wieder eine prototype-Eigenschaft haben
Aufruf von Methoden der Oberklasse über das Funktionsobjekt
Typischerweise call, erster Parameter ist this
  • javascript_advanced/oop-function-object-inheritance.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    };
    
    Person.prototype.greet = function() {
      console.log(`Hallo ${this.name}, du bist ${this.job}!`);
    };
    
    const Detective = function(name, number) {
      Person.call(this, name, "Detektiv");
      this.number = number;
    }
    
    Detective.prototype = Object.create(Person.prototype);
    
    Detective.prototype.greet = function() {
      console.log(`Du bist ${this.name}, der ${this.number}. Detektiv`);
    }
    
    const p1 = new Person("Henry", "Filmtrickspezialist");
    const p2 = new Detective("Peter", 2);
    
    p1.greet();
    p2.greet();
    
  • Hallo Henry, du bist Filmtrickspezialist!
    Du bist Peter, der 2. Detektiv
    

Die class-Syntax

Alternative Syntax für Konstruktorfunktionen und prototype
Annäherung an Sprachen wie Java oder C#, Funktionsumfang bleibt unverändert
Automatische Transformation in prototype-Objekte
Spätestens der Debugger bricht also mit dieser Abstraktionsebene
  • javascript_advanced/oop-class.js
    class Person {
      constructor(name, job) {
        this.name = name;
        this.job = job;
      }
    
      greet() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      }
    };
    
    const p1 = new Person("Alfred", "Erzähler");
    const p2 = new Person("Skinny", "Ratte");
    
    p1.greet();
    p2.greet();
    
  • Hallo Alfred, du bist Erzähler!
    Hallo Skinny, du bist Ratte!
    

Vererbung mit extends / Zugriff auf Basisklasse mit super

Alternative Syntax zur Erstellung einer Prototypenkette
Alle Eigenschaften der Basisklasse in der abgeleiteten Klasse verfügbar
Zugriff auf die Basisklasse mit super
Keine redundante Nennung der konkreten Klasse nötig
Vorsicht: super liefert die tatsächliche Basisklasse, nicht die aufrufende Methode
  • Um die Methode greet der Oberklasse aufzurufen, wäre die Syntax also super.greet()
  • Zur Erinnerung: Der Konstruktur ist eine Funktion, beim Verketten von Konstruktoren ist daher der direkte Aufruf von super() möglich
  • javascript_advanced/oop-class-extend.js
    class Person {
      constructor(name, job) {
        this.name = name;
        this.job = job;
      }
    
      greet() {
        console.log(`Hallo ${this.name}, du bist ${this.job}!`);
      }
    };
    
    class Detective extends Person {
      constructor(name, number) {
        super(name, "Detektiv");
        this.number = number;
      }
    
      greet() {
        console.log(`Du bist ${this.name}, der ${this.number}. Detektiv`);
      }
    };
    
    const p1 = new Person("Henry", "Filmtrickspezialist");
    const p2 = new Detective("Peter", 2);
    
    p1.greet();
    p2.greet();
    
  • Hallo Henry, du bist Filmtrickspezialist!
    Du bist Peter, der 2. Detektiv
    

Sinn von Arrow-Funktionen im Klassenkontext

Potenziell problematisch: Callback-Aufrufe über addEventListener
addEventListener setzt this auf das geklickte Element
  • javascript_advanced/oop-class-arrow.js
    class ClickCounterArrow {
      constructor(elem) {
        this.elem = elem;
        this.numClicks = 0;
    
        this.elem.addEventListener('click', () => {
          this.numClicks++;
          this.elem.textContent = `Arrow: Clicked ${this.numClicks} times`;
          console.log(this);
        });
      }
    }
    
    class ClickCounterFunction {
      constructor(elem) {
        this.elem = elem;
        this.numClicks = 0;
    
        const self = this;
    
        this.elem.addEventListener('click', function() {
          self.numClicks++;
          self.elem.textContent = `Func: Clicked ${this.numClicks} times`;
          console.log(this);
        });
      }
    }
    
    new ClickCounterArrow(document.querySelector('#arrow'));
    new ClickCounterFunction(document.querySelector('#func'));
    

Eigenschaften von Objekten

Grundidee: get- und set-Methoden ohne Syntax für Funktionsaufrufe
Stattdessen “normale” Syntax für Zuweisung oder Abruf von Variablen
Nebeneffekt: Implementierung von “read only” oder “write only” Eigenschaften möglich
Durch Auslassung der lesenden oder schreibenden Methoden

Eigenschaften mit Object.defineProperty

Definition von drei Eigenschaften:
  • name soll unmittelbar gelesen und geschrieben werden können
  • birthyear soll alle Geburtsdaten minimal auf den Wert 1900 festlegen
  • age soll aus birthyear das Alter in Jahren berechnen, aber keine Schreibvorgänge zulassen
Achtung: Definition sollte für jede Instanz aufs neue Erfolgen
Bindung an Prototypen nur in Ausnahmefällen möglich
  • javascript_advanced/oop-define-property.js
    const Person = function(name, birthyear) {
      Object.defineProperty(this, 'name', {
        value: name
      });
    
      Object.defineProperty(this, 'birthyear', {
        get: function() {
          return (this._birthyear);
        },
        set: function(value) {
          this._birthyear = Math.max(+value, 1900);
        },
      });
      
      this.birthyear = birthyear;
    
      Object.defineProperty(this, 'age', {
        get: function() {
          return (new Date().getFullYear() - this._birthyear);
        }
      });
    
    };
    
    const people = [new Person("robert", 1909),
                    new Person("alfred", 1899)];
    people.forEach(p => {
      console.log("Not 12:", [p.name, p.birthyear, p.age].join(','));
    
      p.age = 12;
    
      console.log("12?   :", [p.name, p.birthyear, p.age].join(','));
    });
    
  • Not 12: robert,1909,110
    12?   : robert,1909,110
    Not 12: alfred,1900,119
    12?   : alfred,1900,119
    

Eigenschaften mit get und set

  • javascript_advanced/oop-getter-setter-property.js
    class Person {
    
      constructor(name, birthyear) {
        this.name = name;
        this.birthyear = birthyear;
      }
    
      get name()      { return (this._name); }
      set name(value) { this._name = value;  }
    
      get birthyear() { return (this._birthyear); }
      set birthyear(value) {
        this._birthyear = Math.max(+value, 1900);
      }
      
      get age() {
        return (new Date().getFullYear() - this._birthyear);
      }
    };
    
    const people = [new Person("robert", 1909),
                    new Person("alfred", 1899)];
    people.forEach(p => {
      console.log("Not 12:", [p.name, p.birthyear, p.age].join(','));
    
      p.age = 12;
    
      console.log("12?   :", [p.name, p.birthyear, p.age].join(','));
    });
    
  • Not 12: robert,1909,110
    12?   : robert,1909,110
    Not 12: alfred,1900,119
    12?   : alfred,1900,119
    

“Monkey-Patching” oder “Polyfills”

Eingebaute Klassen wie string, Array, … verfügen ebenfalls über die prototype-Schnittstelle
Entsprechende Funktionen dennoch nicht notwendigerweise sichtbar, da möglicherweise Teil der Laufzeitumgebung
Hinzufügen neuer Funktionen zu Klassen der Standardbibliothek daher möglich
Gefährlicher Eingriff: Kommende Versionen des Standards können Konflikte auslösen
Typischer Einsatzzweck: Ergänzung von standarisierten, aber nicht implementierten Funktionen
Am besten über eine Bibliothek wie core.js

Beispiel: Implementierung der ES6 String-Methode endsWith

Prüfung, ob ein String mit einer bestimmten Zeichenfolge endet
Implementierung entstammt dem MDN
  • javascript_advanced/polyfill-string-endswith.js
    const hasEndsWith = !!String.prototype.endsWith;
    
    if (!String.prototype.endsWith) {
      String.prototype.endsWith = function(searchString, position) {
        var subjectString = this.toString();
        if (typeof position !== 'number'
            || !isFinite(position) 
            || Math.floor(position) !== position
            || position > subjectString.length) {
          position = subjectString.length;
        }
        position -= searchString.length;
        var lastIndex = subjectString.lastIndexOf(searchString, position);
        return lastIndex !== -1 && lastIndex === position;
      };
      console.log("Patched String.endsWith");
    } else {
      console.log("No patching required");
    }
    
  • javascript_advanced/polyfill-string-endswith.html | Validieren
    <!-- Only here for the link -->
    
  • No patching required
    

Beispiel: Überschreiben von console.log

Überschreibt console.log mit einer HTML-Ausgabe
Browser-Implementierung wird dennoch aufgerufen
  • javascript_advanced/polyfill-console-log.js
    (function () {
      const prev = console.log;
      const logElem = document.querySelector('#log');
      
      console.log = function () {
        prev.apply(console, arguments);
        Array.from(arguments).forEach(arg => {
          if (typeof arg === "string") {
            logElem.innerText += arg;
          } else {
            logElem.innerText += JSON.stringify(arg);
          }
          logElem.innerText += ' ';
        });
    
        logElem.innerText += '\n';
      }
    })();
    
  • javascript_advanced/polyfill-console-log-run.js
    console.log("Foo", 1, [1,2,3]);
    console.log({ foo: "bar" });
    

Iteratoren

Grundkonzept eines Iterators: Wiederholter Aufruf einer next-Methode
In JavaScript: Gibt ein Objekt mit zwei Werten zurück und verändert den internen Zustand
  • value ist ein irgendwie geartetes “nächstes” Element
  • done kann auf true gesetzt werden um das Ende des Iterators zu signalisieren
  • javascript_advanced/iterators-intro.js
    class CountIterator {
      constructor() {
        this._current = 0;
      }
    
      next() {
        this._current += 1;
        return ({
          value : this._current,
          done : false
        });
      }
    }
    
    console.log("CountIterator");
    const count = new CountIterator();
    for (let i = 0; i < 15; ++i) {
      console.log(count.next().value);
    }
    
    class WrappingCountIterator {
      constructor(length) {
        this._length = length;
        this._current = 0;
      }
    
      next() {
        this._current = (this._current + 1) % this._length;
        return ({
          value : this._current,
          done : false
        });
      }
    }
    
    console.log("WrappingCountIterator");
    const countWrap = new WrappingCountIterator(3);
    for (let i = 0; i < 15; ++i) {
      console.log(countWrap.next().value);
    }
    
  • CountIterator
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    WrappingCountIterator
    1
    2
    0
    1
    2
    0
    1
    2
    0
    1
    2
    0
    1
    2
    0
    

Generatoren

Syntaxzucker für Iteratoren, bei denen der Zustand implizit gespeichert wird
Implementierung erfolgt über Funktionen mit einem return-artigen Schlüsselwort
Syntax: yield an Stelle einer return-Anweisung springt aus der Funktion
Beim nächsten Aufruf der Funktion wird die Ausführung beim zuletzt ausgeführten yield fortgesetzt
Syntax: function* als Schlüsselwort, danach Definition der Funktion wie bisher
Mit yield* können in Generator-Funktionen weitere Generatorfunktionen aufgerufen werden
  • javascript_advanced/generators-intro.js
    const countGenerator = function*() {
      for(let i = 0; true; ++i) {
        yield i;
      }
    }
    
    const count = countGenerator()
    
    console.log(count.next().value);
    console.log(count.next().value);
    console.log(count.next().value);
    
    const wrappingCountGenerator = function* (length) {
      for(let i = 0; true; ++i) {
        yield i % length;
      }
    }
    
    const countWrap = wrappingCountGenerator(2);
    console.log(countWrap.next().value);
    console.log(countWrap.next().value);
    console.log(countWrap.next().value);
    console.log(countWrap.next().value);
    
  • 0
    1
    2
    0
    1
    0
    1
    

Fibonacci-Generator

  • javascript_advanced/generators-fibonacci.js
    function* fibonacci() {
      let [prev, curr] = [0, 1];
      while (true) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
      }
    }
    
    for (let n of fibonacci()) {
      console.log(n);
      if (n >= 10) {
        break;
      }
    }
  • 1
    2
    3
    5
    8
    13
    

Iteration mit for .. in ..

Einsatzzweck: Iteration über Schlüssel von Objekten
Trotz ähnlichem Namen sehr unterscheidlich zu einer foreach-Iteration über Werte
Möglicherweise unerwünscht: for .. in .. bezieht Funktionen und die Vererbunghierarchie mit ein
Sollte daher nicht für Iteration über reine Daten-Objekte verwendet werden
Object.prototype.hasOwnProperty kann zwischen eigenen und ererbten Eigenschaften unterscheiden
Müsste daher bei der Iteration über JSON-Objekte mit for .. in eigentlich immer genutzt werden
  • javascript_advanced/for-in.js
    const Person = function(name, job) {
      this.name = name;
      this.job = job;
    };
    
    Person.prototype.greet = function() {
      console.log(`Hallo ${this.name}, du bist ${this.job}!`);
    };
    
    const Detective = function(name, number) {
      Person.call(this, name, "Detektiv");
      this.number = number;
    }
    
    Detective.prototype = Object.create(Person.prototype);
    
    Detective.prototype.greet = function() {
      console.log(`Du bist ${this.name}, der ${this.number}. Detektiv`);
    }
    
    const p1 = new Detective("Peter", 2);
    
    for (let property in p1) {
      console.log(property);
    }
    
  • name
    job
    number
    greet
    

Iteration mit for .. of ..

Einsatzzweck: Iteration über Werte von iterierbaren “Dingen”
  • Verallgemeinerung der Schleife unabhängig vom darunterliegenden Datentyp
  • Funktioniert für Array, Map, Set, String, Generatorfunktionen, arguments, NodeList aus dem DOM, …
  • Funktioniert nicht für Object, allerdings kann über Object.entries() iteriert werden, was wiederrum eine Funktion aus ECMA Script 2017 ist, die noch nicht in allen Browsern verfügbar ist.
  • javascript_advanced/for-of.js
    function* foo(){ 
      yield 1; 
      yield 2; 
      yield 3; 
    };
    
    for (const val of foo()) {
      console.log(val);
    }
    
    for (const val of [1,2,3]) {
      console.log(val);
    }
    
    for (const val of "123") {
      console.log(val);
    }
    
    for (const val of {1: 'a', 2: 'b', 3: 'c'}) {
      console.log(val);
    }
    
  • 1
    2
    3
    1
    2
    3
    1
    2
    3
    
    [stdin]:19
    for (const val of {1: 'a', 2: 'b', 3: 'c'}) {
                      ^
    
    TypeError: {(intermediate value)(intermediate value)(intermediate value)} is not iterable
        at [stdin]:19:19

Bäume traversieren mit Generatoren

“Mehrfacher” Aufruf von yield mit yield*
Ruft den Iterator auf der rechten Seite auf anstelle Ihn als Ergebnis zurückzugeben
  • javascript_advanced/generators-tree.js
    function* iterateDomTree(curr, depth) {
      if (depth === undefined) {
        depth = 0;
      }
      
      yield ({
        "elem" : curr,
        "depth" : depth
      });
    
      for (const child of curr.children) {
        yield* iterateDomTree(child, depth + 1);
      }
    }
    
    for (const node of iterateDomTree(document.body)) {
      console.log(node);
    }
    
    

Promises

Potenzielles Problem: Abhängige Daten führen zu einer tiefen Verschachtelung von Callbacks (“Callback Hell”)
Problematisch wenn eine Folgefunktion mit den Daten der vorigen Stufe aufgerufen werden muss
Die “Muster”-Lösung mit ES6: Promise
Keine JavaScript-Eigenart: Konzept findet sich in vielen Programmiersprachen
Grundidee: Stellvertretend für den tatsächlichen Wert erhält der Aufrufer ein Promise-Objekt
Fungiert als Versprechen einen Wert zu berechnen und “irgendwann” eine Antwort zu liefern
Mögliche Zustände eines Promise-Objektes
  • fulfilled, in diesem wird onFulfilled “so früh wie möglich” aufgerufen
  • rejected, in diesem wird onRejected “so früh wie möglich” aufgerufen
  • pending, wenn weder fulfilled noch rejected gilt

Einsatzzwecke

Ausführung von Berechnungen mit langen Wartezeiten
  • Netzwerkzugriffe (AJAX)
  • Datenbankabfragen
  • Dateisystemoperationen
  • Transparentes Caching
  • Benutzerinteraktionen
Für Spieleentwickler: Tolle Methode zur KI-Abstraktion
“Zug” eines Spielers (egal ob KI oder menschlich) über Promise abstrahieren
Für UI-Entwickler: Tolle Methode um Wartezeit-Dialoge zu kapseln
Dezidierte Handhabung von “Abbrechen” Operationen

Promise-API

Die then-Methoden eines Promise-Objektes: Hinterlegen von beliebig vielen Callbacks zur Verarbeitung der berechneten Ergebnisse
Einfach mehrfacher Aufruf von then mit unterschiedlichen Funktionen
Aufrufparameter der registrierten then-Funktion
  1. Parameter (onFulfilled) wird im Erfolgsfall aufgerufen
  2. Parameter (onRejected) ist optional und wird im Fehlerfall aufgerufen
Versprechen machen mit dem Promise-Konstruktor: Übergabe einer Berechnungsfunktion
Funktion erhält die Funktionsobjekte resolve und reject als Parameter um das Ende der Berechnung signalisieren zu können
Nebeneffekt: Nur noch ein Kanal zur Kommunikation von Fehlern
Macht Callback mit err-Parameter überflüssig

Beispiel: Promise für AJAX-Anfragen

  • javascript_advanced/promise-xhr.js
    const doAjaxRequest = url => {
      return (new Promise((resolve, reject) => {
        var request = new XMLHttpRequest();
        request.open('GET', url);
    
        request.onload = () => {
          if (request.status === 200) {
            resolve(request.responseText);
          } else {
            reject(Error(`Code ${request.status}: ${request.statusText}`));
          }
        };
        request.onerror = () => {
          reject(Error('Network Error'));
        };
    
        request.send();
      }));
    }
    
  • javascript_advanced/promise-xhr-run.js
    const promise = doAjaxRequest('/interactive/api/random/pokemon');
    promise.then(
      (res) => document.body.innerHTML = res,
      (err) => console.log(err)
    );
    

Optionale oder geschachtelte Versprechungen

Rekursives Auflösen von Versprechen mit Promise.resolve
  • Nimmt ein beliebiges Objekt entgegen und wertet es so lange aus, bis zum ersten Mal ein konkreter Wert berechnet wird
  • Garantie: Parameter des onFulfilled-Callbacks ist kein Promise

Transparentes Caching

Konkrete HTTP-Endpunkt liefert zufällige Ergebnisse (unabhängig vom Schlüssel)
Ausschließlich zu Demonstrationszwecken
Zu einem numerischen Schlüssel soll daher für die Dauer einer Sitzung ein gleichbleibender Wert gespeichert werden
Berechnung (in diesem Fall AJAX-Anfrage) darf also nur exakt einmalig vorgenommen werden
Mögliche Erweiterungen des folgenden Minimalbeispiels
  • Berechnungsoperation sollte als Callback übergeben werden
  • Speicherung über die Dauer einer Sitzung hinaus

Beispiel: Transparentes Caching

Wert in this.cache[key] ist mal ein Promise und mal nicht
  • Wenn der Wert noch berechnet werden muss: Promise
  • Nach der Berechnung: String
Effekt: Jede Cache-Berechnung wird nur einmalig “angestoßen”
Weitere Anfragen nutzen das existierende Promise-Objekt
  • javascript_advanced/promise-nested.js
    class Cache {
      constructor() {
        this.cache = {};
      }
      
      getValue(key) {
        if (!this.cache[key]) {
          this.cache[key] = doAjaxRequest(`/interactive/api/cache/${key}`);
          this.cache[key].then(result => this.cache[key] = result);
        }
        return (this.cache[key]);
      }
    }
    
    const c = new Cache();
    Promise.resolve(c.getValue(1)).then(v => console.log(v));
    Promise.resolve(c.getValue(1)).then(v => console.log(v));
    

Warten auf viele Versprechungen

Manchmal notwendig: Warten auf sehr viele Versprechen
  • Promise.all(iterable): Liefert eine neue Promise-Instanz die erfüllt wird, wenn alle Versprechen des Iterationsobjektes erfüllt sind
  • Promise.race(iterable): Liefert eine neue Promise-Instanz die erfüllt wird, wenn das erste Versprechen des Iterationsobjektes erfüllt ist
  • javascript_advanced/promise-multiple.js
    const grabRandomValues = num => {
      const toReturn = [];
      for (let i = 0; i < num; ++i) {
        const req = doAjaxRequest(`/interactive/api/cache/${i + 1}`);
        req.then(v => console.log(`Anfrage ${i} fertig`));
        toReturn.push(req);
      }
    
      return (toReturn);
    };
    
    const many = grabRandomValues(10);
    Promise.all(many).then(v => console.log("Alles fertig"));
    Promise.race(many).then(v => console.log("Erster fertig"));
    

async / await

Zwei neue Schlüsselwörter als Syntaxzucker rund um die Verwendung von Promise
Hebt das “Promise Pattern” auf Sprachebene
async: Markiert eine Funktion als asynchron
  • Voraussetzung für Nutzung des await-Schlüsselwortes
  • Typ einer asynchronen Funktion ist AsyncFunction (nicht das “normale” Function)
await: Wartet auf das Ergebnis einer Promise-Berechnung
  • Nur innerhalb einer async-Funktion
  • then-Ergebnis ist Ergebnis des Ausdrucks
  • reject-Ergebnis wird als Ausnahme geworfen

Beispiel zur Syntax

setTimeout ruft die übergebene Funktion nach einer Zeitspanne in ms auf
Dieses Beispiel ist faktisch eine Simulation von sleep
  • javascript_advanced/async-intro.js
    console.log("program: enter");
    
    function timeout(ms) {
      console.log("timeout: enter");
      const toReturn = new Promise(resolve => {
        setTimeout(_ => {
          console.log("timeout: resolve");
          resolve(ms)
        }, ms)
      });
      console.log("timeout: leave");
      return (toReturn);
    }
    
    async function asyncCall() {
      console.log("asyncCall: enter");
      const result = await timeout(1000);
      console.log("asyncCall: leave");
    }
    
    asyncCall();
    
    console.log("program: leave");
  • program: enter
    asyncCall: enter
    timeout: enter
    timeout: leave
    program: leave
    timeout: resolve
    asyncCall: leave
    

AJAX-Beispiel mit async / await

  • javascript_advanced/promise-xhr.js
    const doAjaxRequest = url => {
      return (new Promise((resolve, reject) => {
        var request = new XMLHttpRequest();
        request.open('GET', url);
    
        request.onload = () => {
          if (request.status === 200) {
            resolve(request.responseText);
          } else {
            reject(Error(`Code ${request.status}: ${request.statusText}`));
          }
        };
        request.onerror = () => {
          reject(Error('Network Error'));
        };
    
        request.send();
      }));
    }
    
  • javascript_advanced/async-xhr.js
    async function getRandomPokemon() {
      return await doAjaxRequest('/interactive/api/random/pokemon');
    }
    
    document.body.innerHTML = getRandomPokemon();

Längere async / await -Folge

  • javascript_advanced/async-combined.js
    function timeout(ms) {
      return new Promise(resolve => setTimeout(_ => resolve(ms), ms))
    }
    
    async function longProcess() {
      await timeout(2000);
      document.body.innerHTML = await doAjaxRequest('/interactive/api/random/pokemon');
      await timeout(2000);
      document.body.innerHTML = await doAjaxRequest('/interactive/api/random/harry-potter');
      await timeout(2000);
    }
    
    longProcess();

Observables

Grundlegendes Problem von Promise: Berechnet nicht mehr als einen Wert
In vielen Client-Anwendungen aktualisieren sich Werte aber permanent selbstständig
Weiterentwicklung: “Reactive Programming”
  • Fasst jede Variable als einen Datenfluss auf, der zu bestimmten Zeitpunkten bestimmte Werte hat
  • Programmiermodell von Tabellenkalkulationen wie z.B. Microsoft Excel
Hilfreich für Entwicklung von Anwendungen mit grafischer Oberfläche
  • Vom Benutzer veränderliche Werte werden als veränderlicher Datenstrom aufgefasst
  • Variablen melden “automatisch” wenn sie verändert worden sind
Grundsätzliche Benutzung sehr ähnlich wie Ereignisbehandlung
Angabe einer Callbackfunktion die bei veränderten Werten ausgelöst wird
Sehr gut dokumentierte Implementierung im Rahmen der ReactiveX Bibliotheken
Umsetzung für eine Vielzahl von Sprachen, natürlich auch für JavaScript
Komplexere Interaktionen auf Basis von bereitgestellten Primitiven möglich
Unter anderem die bekannten Funktionen map oder filter

Reaktion auf Ereignisse mit Observable

Wesentliche Methode: Observable.subscribe
  • Kann als Erweiterung von Promise.then aufgefasst werden
  • Erster Parameter heißt typischerweise onNext und wird möglicherweise mehr als einmal aufgerufen

Tastaturereignisse und map

  • javascript_advanced/observable-event-map.js
    const textarea = document.querySelector('#subscribe');
    const target = document.querySelector('#target');
    
    Rx.Observable.fromEvent(textarea, 'keyup')
      .map(event => event.target.value)
      .subscribe(v => target.innerHTML = v);
    

Endliche Datenströme

Zu Testzwecken: Erzeugen von endlichen Intervallen mit Observable.range(start, length)
Gibt die spezifierte Anzahl an Werten aus und ist dann fertig
Weiterer Callback: onCompleted, signalisiert das Ende des Datenstroms
Wird nur aufgerufen wenn nie wieder mit neuen Daten zu rechnen ist, nicht bei möglicherweise langen Pausen
  • javascript_advanced/observable-range.js
    const source = Rx.Observable.range(1, 10);
    
    source.subscribe(
      x => console.log(`Value: ${x}`),
      err => console.log(`Error: ${err}`),
      () => console.log("Finished")
    );
    

Filtern von Werten

Zu Testzwecken: Erzeugen von unendlichen Intervallen mit Observable.interval(ms)
Erhöht einen Zähler nach ms Millisekunden
Kombination zweier Filter
  • take begrenzt die Anzahl der Werte, die der Quelle entnommen werden
  • filter lässt keine Werte durch, die nicht dem Filterkriterium entsprechen
  • javascript_advanced/observable-filter.js
    const source = Rx.Observable.interval(500);
    
    source
      .filter(x => x % 2 == 0)
      .take(5)
      .subscribe(
        x => console.log(`Even value: ${x}`),
        err => console.log(`Even error: ${err}`),
        () => console.log("Even finished")
      );
    
    source
      .take(10)
      .subscribe(
        x => console.log(`Value: ${x}`),
        err => console.log(`Error: ${err}`),
        () => console.log("Finished")
      );
    

Beispiel: Selektiv aktualisieren

Zweck: Nicht jede minimale Änderung sofort mit großem Aufwand anzeigen
  • javascript_advanced/observable-debounce.js
    const textarea = document.querySelector('#subscribe');
    const target = document.querySelector('#target');
    
    Rx.Observable.fromEvent(textarea, 'keyup')
      .map(event => event.target.value)
      .debounceTime(500)
      .distinctUntilChanged()
      .subscribe(v => target.innerHTML = v);
    

Seit RxJS 5.5: “pipeable” oder “lettable” Operationen

Problem: RxJS-Operatoren wurden über umfangreiches Monkey-Patching bereitgestellt
Limitiert Möglichkeiten zur automatischen Code-Reduktion (dazu später mehr)
Lösung: RxJS-Operatoren müssen über explizite import- oder require-Anweisungen referenziert werden
Erlaubt einfachen Ausschluss von “totem” Code
Neues Problem: Die dazu erforderliche Syntax ist wirklich häßlich
Operatoren müssen in einem pipe()-Aufruf eingewickelt werden

Auslieferung von JavaScript-Code

Allgemein guter Stil: Aufteilung des Quelltextes auf unterschiedliche Module
Jedes Modul enthält inhaltlich zusammenhängenden Quelltext
Für HTTP/1 gilt: Jeder Zugriff auf eine neue Ressource ist vergleichsweise teuer
TCP-Verbindung muss ggfs. auf und abgebaut werden
Daher: Download von einer großen JavaScript-Datei effizienter als mehrere kleine Downloads
Gilt nicht mehr für HTTP/2, lesenswerter Artikel von Ashley Rich: Performance Best Practices in the HTTP/2 Era
Interaktiver Vergleich der Ladezeiten von HTTP/1 und HTTP/2
1s Verzögerung kommt in Mobilfunknetzwerken durchaus häufiger vor!
Vorgehen: Zusammenfügen von allen JavaScript-Dateien zu einer einzigen Datei
  • Nachteil: Überträgt möglicherweise auch Code, der für die aktuelle Seite irrelevant ist
  • Vorteil: Browser kann den gesamten Code perfect im Cache vorhalten
Darüber hinaus: Kompression des Quelltextes durch kompaktere, aber funktional identische Notation
Erfolgt im Rahmen eines weiteren Kompilierungsschrittes

Beispiel für minimierte JavaScript-Dateien

  • javascript_advanced/minify-1.js
    // Working with objects
    const homes = {
      "phineas": "Maple Drive 2308",
      "ferb": "Maple Drive 2308",
      "doofenschmirtz": "Doofenshmirtz Evil Inc."
    }
    
    homes.isabella = "across from the Flynn-Fletcher house";
    homes["Major Monogram"] = "O.W.C.A. Secret Headquarters";
    
    const keys = Object.keys(homes);
    for (let index = 0; index < keys.length; ++index) {
      const key = keys[index];
      console.log(key, "=>", homes[key]);
    }
    
  • javascript_advanced/minify-2.js
    // Working with arrays
    const names = ["Phineas", "Ferb"];
    names[3] = "Candice";
    
    for (let index = 0; index < names.length; ++index) {
      console.log("Name #" + index + ": " + names[index]);
    }
    

Auslieferung anderer Resourcen

Vorgehen für CSS-Dateien analog zu JavaScript-Code
Können konkateniert und im Quelltext komprimiert werden
Für Bilddateien: Verwendung von Spritesheets
Platzierung mehrerer Bilder auf einem Bild mit anschließendem “ausschneiden” via CSS
Für Icons: Verwendung von Icon-Fonts (wie z.B. Font Awesome)
Übertragung vieler Icons in einer einzigen Vektor-Datei

Tools zur Optimierung für die Auslieferung

Vielzahl von Tools zur Quelltext-Kompression von JavaScript-Code
  • Uglify JS, existiert in verschiedenen Varianten je nach JavaScript-Version
  • Google Closure Compiler, optimiert den Quelltext wesentlich aggressiver und löscht z.B. auch toten Code
All-in-one Lösung mit umfangreichen Kompilierungsoptionen: Webpack
Erfordert umfangreiche Konfiguration, erzielt aber sehr gute Ergebnisse

Typescript

Grundidee: JavaScript mit statischer Typisierung
Endlich ein Compiler, der eine Vielzahl von trivialen Fehlern auffangen kann
Hilfreich: Jedes gültige JavaScript-Programm ist ein gültiges TypeScript-Programm
Wissen über JavaScript lässt sich hervorragend mitnehmen
Im Rahmen dieser Vorlesung: Keine formale Einführung, aber ein Hinweis auf den TypeScript-Spielplatz
Sofern weitere Beispiele TypeScript-Code erfordern, wird dieser auch mit JavaScript-Wissen gut interpretierbar sein