Expressives JavaScript: Funktionen höherer Ordnung

Ursprünglicher Autor: Marijn Haverbeke
  • Übersetzung

Inhalt




Tsu-li und Tsu-su rühmten sich der Größe ihrer neuen Programme. "Zweihunderttausend Zeilen", sagte Tsu-li, "ohne die Kommentare zu zählen!" Tsu-su antwortete: "Pf-f, meine sind fast eine Million Zeilen." Meister Yun-Ma sagte: "Mein bestes Programm besteht aus 500 Zeilen." Als Tsu-li und Tsu-su dies hörten, erlebten sie Erleuchtung.

Yun-Ma-Meister, Programmierbuch

Es gibt zwei Möglichkeiten, Programme zu erstellen: Sie müssen so einfach sein, dass es offensichtlich keine Fehler gibt, oder sie müssen so komplex sein, dass es keine offensichtlichen Fehler gibt.

Anthony Hoar, 1980 Turing Lecture


Ein großes Programm ist ein kostspieliges Programm, und zwar nicht nur wegen der Zeit, in der es geschrieben wurde. Große Größen bedeuten normalerweise Komplexität, und Komplexität verwirrt Programmierer. Verwirrte Programmierer machen Fehler in Programmen. Ein großes Programm bedeutet, dass Fehler einen Platz zum Verstecken haben und schwerer zu finden sind.

Kehren wir kurz zu zwei Beispielen aus der Einleitung zurück. Der erste ist autark und nimmt sechs Zeilen.

var total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);


Die zweite basiert auf zwei externen Funktionen und belegt eine Zeile.

console.log(sum(range(1, 10)));


Welcher von ihnen stößt mit größerer Wahrscheinlichkeit auf einen Fehler?

Wenn wir die Summen- und Bereichsdefinitionen addieren, ist auch das zweite Programm groß - größer als das erste. Aber ich behaupte immer noch, dass es wahrscheinlich richtig ist.

Dies liegt daran, dass sich der Ausdruck der Lösung direkt auf das zu lösende Problem bezieht. Die Summierung eines numerischen Intervalls ist nicht Zyklen und Zähler. Dies sind Beträge und Lücken.

Die Definitionen dieses Wörterbuchs (Summen- und Bereichsfunktionen) umfassen Schleifen, Zähler und andere zufällige Details. Aber weil sie einfachere Konzepte ausdrücken als das gesamte Programm, ist es einfacher, sie richtig zu machen.

Abstraktion


In einem programmatischen Kontext werden diese "Wörterbuch" -Definitionen häufig als Abstraktionen bezeichnet. Abstraktionen verbergen Details und geben uns die Möglichkeit, über Aufgaben auf einer höheren oder abstrakteren Ebene zu sprechen.

Vergleichen Sie zwei Rezepte für Erbsensuppe:

Geben Sie eine Tasse getrocknete Erbsen pro Portion in die Schüssel. Fügen Sie Wasser hinzu, damit es die Erbsen bedeckt. Mindestens 12 Stunden so einwirken lassen. Die Erbsen aus dem Wasser nehmen und in die Pfanne geben. Fügen Sie 4 Tassen Wasser pro Portion hinzu. Die Pfanne schließen und die Erbsen zwei Stunden köcheln lassen. Nehmen Sie die Hälfte der Zwiebeln pro Portion. Mit einem Messer in Stücke schneiden, zu den Erbsen geben. Nehmen Sie eine Stange Sellerie pro Portion. Mit einem Messer in Stücke schneiden, zu den Erbsen geben. Nehmen Sie eine Karotte pro Portion. Mit einem Messer in Stücke schneiden, zu den Erbsen geben. Weitere 10 Minuten kochen.

Das zweite Rezept:

Pro Portion: 1 Tasse Erbsen, eine halbe Zwiebel, einen Selleriestiel, Karotten.
Die Erbsen 12 Stunden einweichen. 2 Stunden in 4 Tassen Wasser pro Portion dünsten. Gemüse schneiden und hinzufügen. Weitere 10 Minuten kochen.


Der zweite ist kürzer und einfacher. Aber Sie müssen ein wenig mehr Konzepte im Zusammenhang mit dem Kochen kennen - Einweichen, Schmoren, Schneiden (und Gemüse).

Bei der Programmierung können wir uns nicht darauf verlassen, dass alle notwendigen Wörter in unserem Wörterbuch enthalten sind. Aus diesem Grund können Sie in das Muster des ersten Rezepts eintauchen: Diktieren Sie dem Computer alle kleinen Schritte nacheinander, ohne die Konzepte einer höheren Ebene zu bemerken, die sie ausdrücken.

Die zweite Natur des Programmierers sollte die Fähigkeit sein, zu bemerken, wenn ein Konzept ein neues Wort dafür zu finden und es in die Abstraktion zu setzen verlangt.

Abstraktes Array-Traversal


Die einfachen Funktionen, die wir zuvor verwendet haben, eignen sich gut zum Erstellen von Abstraktionen. Aber manchmal sind sie nicht genug.

Im vorherigen Kapitel haben wir diesen Zyklus mehrmals gesehen:

var array = [1, 2, 3];
for (var i = 0; i < array.length; i++) {
  var current = array[i];
  console.log(current);
}


Der Code versucht zu sagen: "Drucken Sie es für jedes Element in dem Array auf der Konsole." Es wird jedoch eine Problemumgehung verwendet - mit einer Variablen zum Zählen von i, zum Überprüfen der Länge des Arrays und zum Deklarieren einer zusätzlichen Variablen, current. Er sieht nicht nur nicht gut aus, sondern ist auch die Grundlage für mögliche Fehler. Wir können die Variable i versehentlich wiederverwenden, anstatt die Länge zu schreiben, die Variablen i und current verwechseln usw.

Lassen Sie es uns in eine Funktion abstrahieren. Können Sie sich eine Möglichkeit vorstellen, dies zu tun?

Es ist ganz einfach, eine Funktion zu schreiben, die ein Array durchläuft und jedes Element von console.log aufruft

function logEach(array) {
  for (var i = 0; i < array.length; i++)
    console.log(array[i]);
}


Aber was ist, wenn wir etwas anderes als Anzeigeelemente in der Konsole tun müssen? Da "etwas tun" als Funktion dargestellt werden kann und Funktionen nur Variablen sind, können wir diese Aktion als Argument übergeben:

function forEach(array, action) {
  for (var i = 0; i < array.length; i++)
    action(array[i]);
}
forEach(["Тили", "Мили", "Трямдия"], console.log);
// → Тили
// → Мили
// → Трямдия


Oft kann man keine vordefinierte Funktion an forEach übergeben, sondern direkt vor Ort eine Funktion erstellen.

var numbers = [1, 2, 3, 4, 5], sum = 0;
forEach(numbers, function(number) {
  sum += number;
});
console.log(sum);
// → 15


Es sieht aus wie ein Klassiker für Schleife, mit einem in einem Block geschriebenen Schleifenkörper. Jetzt befindet sich der Body jedoch innerhalb der Funktion und auch innerhalb der forEach-Aufrufklammern. Daher muss es sowohl mit geschweiften als auch mit runden Klammern geschlossen werden.

Mit dieser Vorlage können wir den Variablennamen für das aktuelle Array-Element (Nummer) festlegen, ohne es manuell aus dem Array auswählen zu müssen.

Im Allgemeinen müssen wir nicht einmal für jeden selbst schreiben. Dies ist die Standardmethode für Arrays. Da das Array bereits als Variable übergeben wird, an der wir arbeiten, akzeptiert forEach nur ein Argument - die Funktion, die für jedes Element ausgeführt werden muss.

Um die Zweckmäßigkeit dieses Ansatzes zu demonstrieren, kehren wir zur Funktion aus dem vorherigen Kapitel zurück. Es enthält zwei Zyklen, die Arrays durchlaufen:

function gatherCorrelations(journal) {
  var phis = {};
  for (var entry = 0; entry < journal.length; entry++) {
    var events = journal[entry].events;
    for (var i = 0; i < events.length; i++) {
      var event = events[i];
      if (!(event in phis))
        phis[event] = phi(tableFor(event, journal));
    }
  }
  return phis;
}


Mit forEach zeichnen wir etwas kürzer und viel sauberer auf.

function gatherCorrelations(journal) {
  var phis = {};
  journal.forEach(function(entry) {
    entry.events.forEach(function(event) {
      if (!(event in phis))
        phis[event] = phi(tableFor(event, journal));
    });
  });
  return phis;
}


Funktionen höherer Ordnung


Funktionen, die auf andere Funktionen angewendet werden - entweder als Argumente oder als Rückgabe -, werden Funktionen höherer Ordnung genannt . Wenn Sie bereits verstanden haben, dass Funktionen nur Variablen sind, gibt es nichts Besonderes an der Existenz solcher Funktionen. Der Begriff stammt aus der Mathematik, wo die Unterschiede zwischen Funktionen und anderen Bedeutungen strenger wahrgenommen werden.

Funktionen höherer Ordnung ermöglichen es uns, Aktionen zu abstrahieren, nicht nur Werte. Sie sind unterschiedlich. Beispielsweise können Sie eine Funktion erstellen, die neue Funktionen erstellt.

function greaterThan(n) {
  return function(m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true


Sie können eine Funktion erstellen, die andere Funktionen ändert.

function noisy(f) {
  return function(arg) {
    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;
  };
}
noisy(Boolean)(0);
// → calling with 0
// → called with 0 - got false


Sie können sogar Funktionen erstellen, mit denen neue Arten der Programmflusssteuerung erstellt werden.

function unless(test, then) {
  if (!test) then();
}
function repeat(times, body) {
  for (var i = 0; i < times; i++) body(i);
}
repeat(3, function(n) {
  unless(n % 2, function() {
    console.log(n, "is even");
  });
});
// → 0 is even
// → 2 is even


Die in Kapitel 3 erläuterten Regeln für den lexikalischen Geltungsbereich sind in solchen Fällen von Vorteil. Im letzten Beispiel ist die Variable n ein Argument für eine externe Funktion. Da die innere Funktion von der äußeren umgeben ist, kann sie n verwenden. Die Körper solcher internen Funktionen haben Zugriff auf die sie umgebenden Variablen. Sie können die Rolle von Blöcken {} spielen, die in regulären Schleifen und bedingten Ausdrücken verwendet werden. Ein wichtiger Unterschied besteht darin, dass in internen Funktionen deklarierte Variablen nicht in die externe Umgebung fallen. Und normalerweise ist dies nur das Beste.

Argumente weitergeben


Die zuvor deklarierte noisy-Funktion, die ihr Argument an eine andere Funktion weitergibt, ist nicht ganz praktisch.

function noisy(f) {
  return function(arg) {
    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;
  };
}


Wenn f mehr als einen Parameter akzeptiert, erhält es nur den ersten. Es wäre möglich, der internen Funktion eine Reihe von Argumenten hinzuzufügen (arg1, arg2 usw.) und sie alle an f zu übergeben, aber es ist nicht bekannt, wie viel für uns ausreicht. Außerdem konnte die Funktion f mit arguments.length nicht korrekt arbeiten. Da wir immer die gleiche Anzahl von Argumenten übergeben würden, wäre nicht bekannt, wie viele Argumente uns ursprünglich gegeben wurden.

In solchen Fällen verfügen Funktionen in JavaScript über eine Methode apply. Ein Array (oder ein Objekt in Form eines Arrays) von Argumenten wird an ihn übergeben und er ruft eine Funktion mit diesen Argumenten auf.

function transparentWrapping(f) {
  return function() {
    return f.apply(null, arguments);
  };
}


Diese Funktion ist nutzlos, zeigt aber die Vorlage, die uns interessiert - die von ihr zurückgegebene Funktion wird an alle Argumente weitergegeben, die sie erhalten hat, aber nicht mehr. Dies geschieht, indem eigene Argumente, die im arguments-Objekt gespeichert sind, an die apply-Methode übergeben werden. Das erste Argument für die Methode apply, das wir in diesem Fall auf null setzen, kann zum Emulieren eines Methodenaufrufs verwendet werden. Wir werden im nächsten Kapitel auf dieses Thema zurückkommen.

Json


Funktionen höherer Ordnung, die die Funktion auf Array-Elemente anwenden, sind in JavaScript weit verbreitet. Die forEach-Methode ist eine der primitivsten dieser Funktionen. Als Array-Methoden haben wir Zugriff auf viele andere Optionen für Funktionen. Um sie kennenzulernen, spielen wir mit einem anderen Datensatz.

Vor einigen Jahren hat jemand viele Archive untersucht und ein ganzes Buch über die Geschichte meines Familiennamens geschrieben. Ich öffnete es in der Hoffnung, dort Ritter, Piraten und Alchemisten zu finden ... Es stellte sich jedoch heraus, dass es hauptsächlich flämische Bauern gab. Zur Unterhaltung extrahierte ich Informationen über meine unmittelbaren Vorfahren und entwarf sie in einem Format, das zum Lesen durch einen Computer geeignet war.

Die Datei sieht ungefähr so ​​aus:

[
  {"name": "Emma de Milliano", "sex": "f",
   "born": 1876, "died": 1956,
   "father": "Petrus de Milliano",
   "mother": "Sophia van Damme"},
  {"name": "Carolus Haverbeke", "sex": "m",
   "born": 1832, "died": 1905,
   "father": "Carel Haverbeke",
   "mother": "Maria van Brussel"},
  … и так далее
]


Dieses Format heißt JSON und bedeutet JavaScript-Objektnotation. Es ist in der Datenspeicherung und Netzwerkkommunikation weit verbreitet.

JSON ähnelt JavaScript in Bezug auf das Schreiben von Arrays und Objekten - mit einigen Einschränkungen. Alle Eigenschaftsnamen müssen in doppelte Anführungszeichen gesetzt werden, und es sind nur einfache Werte zulässig - keine Funktionsaufrufe, Variablen, nichts, was Berechnungen beinhalten würde. Kommentare sind ebenfalls nicht erlaubt.

JavaScript bietet die Funktionen JSON.stringify und JSON.parse, mit denen Daten aus diesem Format in dieses Format konvertiert werden. Der erste nimmt einen Wert und gibt einen String mit JSON zurück. Der zweite nimmt eine solche Zeile und gibt einen Wert zurück.

var string = JSON.stringify({name: "X", born: 1980});
console.log(string);
// → {"name":"X","born":1980}
console.log(JSON.parse(string).born);
// → 1980


Die Variable ANCESTRY_FILE ist hier verfügbar . Es enthält die JSON-Datei als Zeichenfolge. Dekodieren wir es und zählen die Anzahl der genannten Personen.

var ancestry = JSON.parse(ANCESTRY_FILE);
console.log(ancestry.length);
// → 39


Filtern Sie das Array


Um Leute zu finden, die 1924 jung waren, kann die folgende Funktion nützlich sein. Es filtert die Elemente des Arrays heraus, die den Test nicht bestehen.

function filter(array, test) {
  var passed = [];
  for (var i = 0; i < array.length; i++) {
    if (test(array[i]))
      passed.push(array[i]);
  }
  return passed;
}
console.log(filter(ancestry, function(person) {
  return person.born > 1900 && person.born < 1925;
}));
// → [{name: "Philibert Haverbeke", …}, …]


Es wird ein Argument namens test verwendet - dies ist eine Funktion, die Überprüfungsberechnungen durchführt. Es wird für jedes Element aufgerufen und der von ihm zurückgegebene Wert bestimmt, ob dieses Element in das zurückgegebene Array fällt.

In der Akte befanden sich drei Personen, die 1924 noch jung waren - Großvater, Großmutter und Cousin.

Beachten Sie, dass die Filterfunktion keine Elemente aus einem vorhandenen Array löscht, sondern ein neues Array erstellt, das nur validierte Elemente enthält. Dies ist eine reine Funktion, da sie das übergebene Array nicht beeinträchtigt.

Filter ist wie forEach eine der Standard-Array-Methoden. Im Beispiel haben wir eine solche Funktion beschrieben, nur um zu zeigen, was sie im Inneren bewirkt. Von nun an werden wir es einfach benutzen:

onsole.log(ancestry.filter(function(person) {
  return person.father == "Carel Haverbeke";
}));
// → [{name: "Carolus Haverbeke", …}]


Transformationen mit Karte


Angenommen, wir haben ein Archiv von Objekten, die Personen darstellen. Dieses Archiv wurde durch Filtern eines Arrays von Vorfahren erstellt. Aber wir brauchen eine Reihe von Namen, die leichter zu lesen wären.

Die map-Methode transformiert ein Array, indem sie eine Funktion auf alle ihre Elemente anwendet und aus den zurückgegebenen Werten ein neues Array erstellt. Das neue Array hat dieselbe Länge wie die Eingabe, sein Inhalt wird jedoch in das neue Format konvertiert.

function map(array, transform) {
  var mapped = [];
  for (var i = 0; i < array.length; i++)
    mapped.push(transform(array[i]));
  return mapped;
}
var overNinety = ancestry.filter(function(person) {
  return person.died - person.born > 90;
});
console.log(map(overNinety, function(person) {
  return person.name;
}));
// → ["Clara Aernoudts", "Emile Haverbeke",
//    "Maria Haverbeke"]


Interessanterweise sind die Menschen, die mindestens 90 Jahre alt sind, die gleichen, die wir zuvor gesehen haben und die in den 1920er Jahren noch jung waren. Dies ist nur die neueste Generation in meinen Unterlagen. Anscheinend hat sich die Medizin erheblich verbessert.

Wie forEach und filter ist map auch eine Standardmethode für Arrays.

Summation mit reduzieren


Ein weiteres beliebtes Beispiel für die Arbeit mit Arrays ist das Abrufen eines einzelnen Werts basierend auf den Daten im Array. Ein Beispiel ist die bekannte Summierung einer Liste von Zahlen. Die andere ist die Suche nach der Person, die vor allen anderen geboren wurde.

Eine Operation höherer Ordnung dieses Typs wird als Reduzieren (Reduzieren; oder manchmal Falten, Falten) bezeichnet. Sie können sich vorstellen, ein Array einzeln zu falten. Bei der Summierung der Zahlen haben wir bei Null begonnen und für jedes Element mit der aktuellen Summe durch Addition kombiniert.

Die Parameter der Reduktionsfunktion sind neben dem Array eine Kombinationsfunktion und ein Anfangswert. Diese Funktion ist etwas weniger klar als Filter oder Map, achten Sie also genau darauf.

function reduce(array, combine, start) {
  var current = start;
  for (var i = 0; i < array.length; i++)
    current = combine(current, array[i]);
  return current;
}
console.log(reduce([1, 2, 3, 4], function(a, b) {
  return a + b;
}, 0));
// → 10


Noch praktischer ist die standardmäßige Methode zum Reduzieren von Arrays, die natürlich auf die gleiche Weise funktioniert. Wenn das Array mindestens ein Element enthält, können Sie das Argument start weglassen. Die Methode nimmt das erste Element des Arrays als Startwert und beginnt mit der Arbeit ab dem zweiten.

Um den ältesten meiner bekannten Vorfahren mit redu zu finden, können wir etwas schreiben wie:

console.log(ancestry.reduce(function(min, cur) {
  if (cur.born < min.born) return cur;
  else return min;
}));
// → {name: "Pauwels van Haverbeke", born: 1535, …}


Zusammensetzbarkeit


Wie können wir das vorige Beispiel (Suche nach einer Person mit dem frühesten Geburtsdatum) ohne Funktionen höherer Ordnung schreiben? Eigentlich ist der Code nicht so schlimm:

var min = ancestry[0];
for (var i = 1; i < ancestry.length; i++) {
  var cur = ancestry[i];
  if (cur.born < min.born)
    min = cur;
}
console.log(min);
// → {name: "Pauwels van Haverbeke", born: 1535, …}


Etwas mehr Variablen, zwei Zeilen länger - aber bisher einigermaßen klarer Code.

Funktionen höherer Ordnung eröffnen sich wirklich, wenn Sie Funktionen kombinieren müssen. Zum Beispiel schreiben wir einen Code, der das Durchschnittsalter von Männern und Frauen in einer Gruppe ermittelt.

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}
function age(p) { return p.died - p.born; }
function male(p) { return p.sex == "m"; }
function female(p) { return p.sex == "f"; }
console.log(average(ancestry.filter(male).map(age)));
// → 61.67
console.log(average(ancestry.filter(female).map(age)));
// → 54.56


(Es ist albern, dass wir Addition als Plus-Funktion definieren müssen, aber die Operatoren in JavaScript sind keine Werte, sodass Sie sie nicht als Argumente übergeben).

Anstatt den Algorithmus in einen großen Zyklus einzubetten, wird alles nach den für uns interessanten Konzepten verteilt - Bestimmung des Geschlechts, Zählung des Alters und Mittelung der Zahlen. Wir wenden sie der Reihe nach an, um das Ergebnis zu erhalten.

Für verständlichen Code ist dies eine fantastische Gelegenheit. Klarheit ist natürlich nicht kostenlos.

Preis


Im fröhlichen Land des eleganten Codes und der schönen Regenbogen gibt es ein kleines Monster namens Inefficiency.

Das Programm, das das Array verarbeitet, wird am schönsten als eine Folge klar getrennter Schritte dargestellt, von denen jeder etwas mit dem Array tut und ein neues Array zurückgibt. Das Schichten all dieser Zwischenanordnungen ist jedoch teuer.

Ebenso ist es praktisch und leicht zu verstehen, eine Funktion an forEach zu übergeben, damit sie das Array durchläuft. Das Aufrufen von Funktionen in JavaScript ist jedoch teurer als Schleifen.

Dies ist auch bei vielen Techniken der Fall, die die Lesbarkeit des Programms verbessern. Abstraktionen fügen Schichten zwischen der sauberen Arbeit eines Computers und den Konzepten, mit denen wir arbeiten, hinzu - und infolgedessen leistet der Computer mehr Arbeit. Dies ist keine ironclad-Regel - es gibt Sprachen, mit denen Sie Abstraktionen hinzufügen können, ohne Abstriche bei der Effizienz zu machen, und selbst in JavaScript kann ein erfahrener Programmierer Wege finden, abstrakten und schnellen Code zu schreiben. Dieses Problem ist jedoch weit verbreitet.

Glücklicherweise sind die meisten Computer wahnsinnig schnell. Wenn Ihr Datensatz nicht zu groß ist oder die Laufzeit aus Sicht einer Person gerade schnell genug sein sollte (um beispielsweise jedes Mal etwas zu tun, wenn ein Benutzer auf eine Schaltfläche klickt), spielt es keine Rolle, ob Sie eine schöne Lösung geschrieben haben, die funktioniert eine halbe oder sehr optimierte Millisekunde, die eine Zehntel-Millisekunde dauert.

Es ist praktisch, grob zu berechnen, wie oft dieser Code aufgerufen wird. Wenn Sie eine Schleife in einer Schleife haben (direkt oder durch einen Aufruf in einer Schleife einer Funktion, die intern auch mit einer Schleife arbeitet), wird der Code N * M-mal ausgeführt, wobei N die Anzahl der Wiederholungen der äußeren Schleife und M die innere ist. Wenn es in der inneren Schleife einen weiteren Zyklus gibt, der P-mal wiederholt, erhalten wir bereits N * M * P - und so weiter. Dies kann zu großen Zahlen führen, und wenn das Programm langsamer wird, kann das Problem häufig auf ein kleines Stück Code innerhalb der innersten Schleife reduziert werden.

Großartig, großartig, großartig, großartig ...


In der Akte wird mein Großvater Philibert Haverbeke erwähnt. Ausgehend von ihm kann ich meine Familie auf der Suche nach dem ältesten meiner Vorfahren, Powels van Haverbeck, meinem direkten Vorfahren, verfolgen. Jetzt möchte ich berechnen, wie viel Prozent der DNA ich (theoretisch) von ihm habe.

Um vom Namen des Vorfahren zu dem Objekt zu gelangen, das ihn repräsentiert, erstellen wir ein Objekt, das Namen und Personen entspricht.

var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});
console.log(byName["Philibert Haverbeke"]);
// → {name: "Philibert Haverbeke", …}


Die Aufgabe besteht nicht nur darin, in jedem Datensatz einen Vater zu finden und zu berechnen, wie viele Schritte es dauert, bis Powels erreicht ist. In der Familiengeschichte gab es mehrere Ehen zwischen Cousins ​​(na ja, kleine Dörfer usw.). In dieser Hinsicht sind die Zweige des Stammbaums an einigen Stellen mit anderen verbunden, sodass ich mehr Gene als 1/2 G erhalte (G ist die Anzahl der Generationen zwischen Powels und mir). Diese Formel basiert auf der Annahme, dass jede Generation den genetischen Fundus in zwei Teile aufteilt.

Es wäre vernünftig, eine Analogie mit "Reduzieren" zu ziehen, bei der das Array durch sequentielles Kombinieren von Daten von links nach rechts auf einen einzelnen Wert reduziert wird. Hier müssen wir auch einen Singular bekommen, aber wir müssen den Linien der Vererbung folgen. Und sie bilden keine einfache Liste, sondern einen Baum.

Wir betrachten diesen Wert für eine bestimmte Person und kombinieren diese Werte seiner Vorfahren. Dies kann rekursiv erfolgen. Wenn wir jemanden brauchen, müssen wir den notwendigen Wert für seine Eltern berechnen, was wiederum eine Berechnung für ihre Vorfahren usw. erfordert. Theoretisch müssen wir eine unendliche Anzahl von Baumknoten umgehen, aber da unser Datensatz endlich ist, müssen wir irgendwo anhalten. Wir legen einfach einen Standardwert für alle Personen fest, die nicht auf unserer Liste stehen. Es wäre logisch, ihnen einen Wert von Null zuzuweisen - Leute, die nicht auf der Liste stehen, tragen nicht die DNA des Vorfahren, den wir brauchen.

Wenn Sie eine Person akzeptieren, eine Funktion zum Kombinieren von Werten aus zwei Vorfahren und einen Standardwert, komprimiert die Funktion reductAncestors einen Wert aus einem Stammbaum.

function reduceAncestors(person, f, defaultValue) {
  function valueFor(person) {
    if (person == null)
      return defaultValue;
    else
      return f(person, valueFor(byName[person.mother]),
                       valueFor(byName[person.father]));
  }
  return valueFor(person);
}


Die interne Funktion valueFor arbeitet mit einer Person. Dank rekursiver Magie kann sie sich selbst nennen, um den Vater und die Mutter dieser Person zu verarbeiten. Die Ergebnisse werden zusammen mit dem Personenobjekt an f übergeben, das den gewünschten Wert für diese Person berechnet.

Jetzt können wir diesen Wert verwenden, um den Prozentsatz der DNA zu berechnen, den mein Großvater mit Powels Baths Haverbeke geteilt hat, und ihn in vier Teile aufzuteilen.

function sharedDNA(person, fromMother, fromFather) {
  if (person.name == "Pauwels van Haverbeke")
    return 1;
  else
    return (fromMother + fromFather) / 2;
}
var ph = byName["Philibert Haverbeke"];
console.log(reduceAncestors(ph, sharedDNA, 0) / 4);
// → 0.00049


Eine Person namens Pauwels vann Haverbeke teilt offensichtlich 100% der DNA mit Pauwels vann Haverbeke (die Datenliste enthält keine vollständigen Namensvetter), daher gibt die Funktion für ihn 1 zurück. Alle anderen teilen den durchschnittlichen Prozentsatz ihrer Eltern.

Statistisch gesehen habe ich ungefähr 0,05% der DNA, die meinem Vorfahren aus dem 16. Jahrhundert entspricht. Dies ist natürlich eine ungefähre Zahl. Das ist ziemlich klein, aber da unser genetisches Material ungefähr 3 Milliarden Basenpaare umfasst, habe ich etwas von meinem Vorfahren.

Es wäre möglich, diese Zahl zu berechnen, ohne reduAncestors zu verwenden. Die Trennung des allgemeinen Ansatzes (Umgehen des Baums) und des speziellen Falles (DNA-Zählung) ermöglicht es uns jedoch, verständlicheren Code zu schreiben und Teile des Codes erneut für andere Aufgaben zu verwenden. Der folgende Code ermittelt beispielsweise den Prozentsatz der bekannten Vorfahren einer bestimmten Person, die 70 Jahre alt war.

function countAncestors(person, test) {
  function combine(person, fromMother, fromFather) {
    var thisOneCounts = test(person);
    return fromMother + fromFather + (thisOneCounts ? 1 : 0);
  }
  return reduceAncestors(person, combine, 0);
}
function longLivingPercentage(person) {
  var all = countAncestors(person, function(person) {
    return true;
  });
  var longLiving = countAncestors(person, function(person) {
    return (person.died - person.born) >= 70;
  });
  return longLiving / all;
}
console.log(longLivingPercentage(byName["Emile Haverbeke"]));
// → 0.145


Keine Notwendigkeit, solche Berechnungen zu ernst zu nehmen, da unser Set eine willkürliche Stichprobe von Personen enthält. Der Code zeigt jedoch, dass reductAncestors ein nützlicher Bestandteil eines allgemeinen Vokabulars für die Arbeit mit einer Datenstruktur wie einem Stammbaum ist.

Einband


Die Bind-Methode, über die alle Funktionen verfügen, erstellt eine neue Funktion, die das Original mit einigen festen Argumenten aufruft.

Das folgende Beispiel zeigt, wie das funktioniert. Darin definieren wir die Funktion isInSet, die angibt, ob sich in einer bestimmten Menge ein Personenname befindet. Zum Aufrufen des Filters können wir entweder einen Ausdruck mit einer Funktion schreiben, die isInSet aufruft, und ihm eine Reihe von Zeichenfolgen als erstes Argument übergeben, oder teilweise die isInSet-Funktion verwenden.

var theSet = ["Carel Haverbeke", "Maria van Brussel",
              "Donald Duck"];
function isInSet(set, person) {
  return set.indexOf(person.name) > -1;
}
console.log(ancestry.filter(function(person) {
  return isInSet(theSet, person);
}));
// → [{name: "Maria van Brussel", …},
//    {name: "Carel Haverbeke", …}]
console.log(ancestry.filter(isInSet.bind(null, theSet)));
// → … same result


Der Bind-Aufruf gibt eine Funktion zurück, die isInSet mit dem ersten Argument für theSet aufruft, und nachfolgende Argumente, die den an bind übergebenen entsprechen.

Das erste Argument, das jetzt auf null gesetzt ist, wird für Methodenaufrufe verwendet - genau wie in apply. Wir werden später darüber sprechen.

Zusammenfassung


Die Möglichkeit, einen Funktionsaufruf an andere Funktionen zu übergeben, ist nicht nur ein Spielzeug, sondern eine sehr nützliche JavaScript-Eigenschaft. Wir können Ausdrücke mit Leerzeichen schreiben, die dann mit den von den Funktionen zurückgegebenen Werten ausgefüllt werden.

Arrays verfügen über mehrere nützliche Methoden höherer Ordnung - um mit jedem Element etwas zu tun, Filter - um ein neues Array zu erstellen, in dem einige Werte gefiltert werden, Map - um ein neues Array zu erstellen, von dem jedes Element durch eine Funktion geleitet wird, Reduce - für die Kombination alle Elemente des Arrays in einen Wert.

Funktionen verfügen über eine Methode apply, mit der Argumente als Array übergeben werden können. Sie verfügen auch über eine Bindemethode zum Erstellen einer Kopie einer Funktion mit teilweise angegebenen Argumenten.

Übungen


Faltung

Verwenden Sie die Methode "Reduzieren" in Kombination mit "Concat", um ein Array von Arrays in ein einziges Array zu reduzieren, das alle Elemente der Eingabearrays enthält.

var arrays = [[1, 2, 3], [4, 5], [6]];
// Ваш код тут
// → [1, 2, 3, 4, 5, 6]


Altersunterschied zwischen Müttern und ihren Kindern

Berechnen Sie anhand des Datensatzes aus dem Beispiel den durchschnittlichen Altersunterschied zwischen Müttern und ihren Kindern (dies ist das Alter der Mutter zum Zeitpunkt der Geburt des Babys). Sie können die Durchschnittsfunktion im Kapitel verwenden.

Bitte beachten Sie, dass nicht alle im Kit genannten Mütter anwesend sind. Das byName-Objekt kann nützlich sein, um die Suche nach einer Person anhand des Namens zu vereinfachen.

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}
var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});
// Ваш код тут
// → 31.2


Historische Lebenserwartung

Wir glaubten, dass nur die letzte Generation von Menschen 90 Jahre alt war. Schauen wir uns dieses Phänomen genauer an. Berechnen Sie das Durchschnittsalter der Menschen für jedes der Jahrhunderte. Weisen Sie einem Jahrhundert von Menschen zu, indem Sie das Todesjahr durch 100 teilen und runden: Math.ceil (person.died / 100).

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}
// Тут ваш код
// → 16: 43.5
//   17: 51.2
//   18: 52.8
//   19: 54.8
//   20: 84.7
//   21: 94


Schreiben Sie als Bonusspiel eine groupBy-Funktion, die die Gruppierungsoperation abstrahiert. Es sollte ein Array und eine Funktion enthalten, die die Gruppe für die Elemente des Arrays zählt, und ein Objekt zurückgeben, das die Namen der Gruppen den Arrays der Mitglieder dieser Gruppen zuordnet.

Jeder und einige

Arrays haben alle und einige Standardmethoden. Als Argument nehmen sie eine bestimmte Funktion, die beim Aufruf mit einem Array-Element als Argument true oder false zurückgibt. So wie && nur dann true zurückgibt, wenn die Ausdrücke auf beiden Seiten des Operators true zurückgeben, gibt die Methode every true zurück, wenn die Funktion für alle Elemente des Arrays true zurückgibt. Dementsprechend gibt einige true zurück, wenn die angegebene Funktion true zurückgibt, wenn mit einem der Elemente des Arrays gearbeitet wird. Sie verarbeiten nicht mehr Elemente als erforderlich. Wenn beispielsweise für das erste Element einige zutreffen, werden die verbleibenden nicht verarbeitet.

Schreiben Sie alle Funktionen, die genau wie diese Methoden funktionieren, und akzeptieren Sie nur ein Array als Argument.

// Ваш код тут
console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false

Jetzt auch beliebt: