Antwort an „Rolf B“ verfassen

Hallo effel,

Ich komme aus der Zeit als man noch die Programme in Machinensprache schrieb

Meine ersten Programme entstanden auf einem TI-59 und einem 6502-Minicomputer mit 8 Stellen 7-Segmentanzeige und ca 20 Tasten (16 davon 0-F). Dann kamen BASIC, PASCAL, 8086 Assemler, COBOL, PL/1, IBM/370 Assembler, dBase, C, C++ (nur die Basics), Smalltalk, C#, PHP und JavaScript. Soll nur heißen: Wenn man vom Assembler her kommt, sind OOP und funktionale Basics nicht außer Reichweite. Aber ganz schön weit weg. Bei rein funktionalen Sprachen wie Elixier, womit Christian Kruse das Forum gemacht hat, gehen bei mir allerdings die Klappen schlagartig zu.

Heap

Ist ein Speicherbereich der JavaScript-Runtime, wo alles liegt, was nicht auf den Stack passt. Und, ja, das ist so gut wie alles. Arrays, Objekte, vorcompilierter JavaScript-Code, und interne Objekte wie Funktions- oder Modul-Scopes. Der Heap wird dynamisch verwaltet und kann von JS im Hintergrund reorganisiert werden (Garbage Collection).

Adressen

Zeigen auf die auf dem Heap gespeicherten Dinge, ABER: wenn der Garbage Collector ein Objekt an eine andere Adresse verschiebt, müssen alle Stellen, die die Adresse dieses Objekts enthalten, korrigiert werden. Dazu muss er entweder exakt nachhalten, wo solche Stellen sind, oder es darf nur eine einzige Stelle geben: eine Tabelle aller vorhandenen Objekte.

Lösung 1 ist speicheraufwändiger, hat aber nur Zeitaufwand beim Speichern von Adressen. Lösung 2 ist einfacher, aber jeder Lesezugriff muss über die Objekttabelle gehen und dauert deshalb länger. Ich weiß nicht, wie es die JS Engines machen.

Überlauf

Stack und Heap werden von JS verwaltet. Reicht ihr Platz nicht, kann JS weiteren Platz vom Betriebssystem anfordern. Das ist auf modernen Prozessoren ziemlich einfach, weil die Prozesse keinen direkten Speicher bekommen, sondern einen virtuellen Adressraum, der per Hardware im Prozessor auf den realen Speicher abgebildet wird. Ich kann also für jeden Prozess einfach mal 2GB oder mehr virtuellen Speicher anlegen, den Stack oben unter die Decke hängen und den Heap von unten wachsen lassen. Realer Speicher wird nur soviel belegt, wie auch genutzt wird.

Was Du meinst, sind Zugriffe über die Grenze von Speicherbereichen hinaus, z.B. Zugriff auf Index 1000 eines Arrays mit 100 Elementen. Das geht in JS nicht, weil diese Zugriffe von der Engine überwacht werden.

const und Zeichenketten

Nein. Mit const definierst Du, dass eine Variable nur einmal befüllt und dann nicht mehr verändert werden kann. Das ist ziemlich oft sinnvoll, und der Just-in-Time Compiler (JIT) von JavaScript kann const-Variablen besser optimieren. Allerdings würde ich vermuten, dass moderne JITs den const-Zustand einer Variablen auch selbstständig erkennen können. const/let ist vor allem für den Leser des Codes von Bedeutung.

Zeichenketten hingegen sind in JavaScript und vielen anderen Sprachen ohnehin immutable. D.h. hier

let a = (new Date()).toString();
a = "Otto";
console.log(a);   // Otto
a[3] = "i";       // Keine Fehlermeldung, aber:
console.log(a);   // Nicht Otto, sondern immer noch Otto

wird der Heapspeicherbereich, wo die Stringdarstellung des Datums steht, nicht durch "Otto" überschrieben. Statt dessen wird die Verbindung von a auf diesen Bereich aufgehoben. Irgendwann läuft der Garbage Collector und räumt den alten String mit dem Datum ab.

Die Variable a verweist nun auf den String "Otto". Dieser String ist unveränderlich, das liegt aber nicht daran, dass er ein String-Literal ist, das gilt für jeden String.

function-Name

Der Name ist ein Name im Programmcode. Und er landet auch in der name Eigenschaft des Objekts, das die Funktion repräsentiert. Dieser Name bleibt unverändert, auch wenn das Objekt irgendwohin übergeben wird.

function hugo1() { return "Hier ist hugo1/" + hugo1.name; }

const hugo2 = function() { return "Hier ist hugo2/" + hugo2.name; }

function otto(func) {
   console.log("Ergebnis von " + func.name + ": " + func());
}

otto(hugo1);
// Ergebnis von hugo1: Hier ist hugo1/hugo1
otto(hugo2);
// Ergebnis von hugo2: Hier ist hugo2/hugo2

Es sieht so aus, als wäre function hugo1() {...} und const hugo2 = function() {...} so ziemlich das Gleiche. Ja, so ziemlich. Exakt gleich aber nicht. Das Wiki diskutiert das ausführlich.

im Module direct1 functionsaufruf m kann diese Referenz an die "Argument"variable add weiter gegeben werden...

Ja, so wie jeder JavaScript-Wert. Wo nun der genau Ort ist, an dem die Weitergabe erfolgt, kann man diskutieren. Vom Prinzip her legt der Aufrufer alle Parameter und die Rückkehradresse auf den Stack (so wie auch im Assembler) und der Aufgerufene weiß, dass mit dem Namen "add" der so-und-so-vielte Parameter auf dem Stack gemeint ist.

...aber kann dort nicht mehr überschrieben werden.

Naja, wenn Du an add etwas zuweist, wird der Wert auf dem Stack schon überschrieben. Aber eben nicht die Stelle, wo der Stackwert hergekommen ist.

Ein Scope ist ein Gültigkeitsbereich, z.B. für Variable.

Zur Compile-Zeit: Ja. Zur Laufzeit wird bei einem Funktionsaufruf zu jedem Scope ein Aufrufkontext erzeugt, der die Variablen dieses Scopes aufnimmt. Dieser Aufrufkontext kannst Du Dir wie ein Objekt auf dem Heap vorstellen, dessen Eigenschaften die lokalen Variablen sind. Jede Funktion, die in diesem Aufrufkontext definiert wird, bekommt einen internen Verweis auf diesen Kontext. Deswegen schrieb ich vorhin auch, dass der Aufrufer "vom Prinzip her" die Parameter auf den Stack legt. Tatsächlich sind die Funktionsparameter Teil des Aufrufkontextes. Das müssen sie sein, damit Closures funktionieren.

function getAdder(summand) {
   return function(x) { return x + summand; }
}

const add7 = getAdder(7);

console.log(add7(3));   // gibt 10 aus

getAdder gibt eine Funktion zurück, die den Wert addiert, der an getAdder übergeben wurde. Im Beispiel wird diese Funktion in add7 gespeichert. Wenn add7 aufgerufen wird, ist getAdder schon zu Ende, d.h. der Stackframe des getAdder-Aufrufs ist nicht mehr da. Trotzdem kann die von getAdder zurückgegebene Funktion noch auf den summand-Parameter zugreifen. Warum geht das?

  1. Es geht erstmal grundsätzlich deshalb, weil summand zum Elternscope der erzeugten Funktion gehört und Funktionen auf Variablen in ihren Elternscopes zugreifen dürfen
  2. Es geht nach dem Ende von getAdder deshalb immer noch, weil der Aufrufkontext von getAdder, also die Laufzeitdarstellung des Scopes, an der erzeugten Funktion dranhängt.

Eigentlich gibt es im compilierten Bereich keine Variablennamen mehr, sondern nur noch Adressen, auf die der Prozessor zugreift.

Im Wesentlichen hat Du recht. Das ist genau so wie bei Symbolen, die Du im Assembler-Sourcecode definierst. Es gibt aber Fälle, wo der Name zur Laufzeit auf anderem Wege noch verfügbar ist:

  • Als function statement deklarierte Funktionen (z.B. oben hugo1 oder getAdder) und mit var deklarierte Variablen im globalen Scope sind gleichzeitig Eigenschaften des globalen Objekts (window im Browser, global in Node)
  • Funktionen haben eine name-Eigenschaft (die bei anonymen Funktionen (z.B. die von getAdder erzeugte Funktion) aber auch leer sein kann. Oben bei hugo2 war der Name ausnahmsweise nicht leer, da hat JS die Zuweisung an hugo2 erkannt und das als name der Funktion zugewiesen)
  • Objekteigenschaften sind keine Variablennamen. Eigenschaftsnamen sind auch zur Laufzeit verfügbar.

So kämpfe ich mit den neuen Begriffen...

Ja, das ist einfach viel Stoff. Und an machen Stellen muss man echt aufpassen, dass man sich das gedanklich nicht zu sehr vereinfacht und in Fallen tappt.

Rolf

--
sumpsi - posui - obstruxi
freiwillig, öffentlich sichtbar
freiwillig, öffentlich sichtbar
freiwillig, öffentlich sichtbar

Ihre Identität in einem Cookie zu speichern erlaubt es Ihnen, Ihre Beiträge zu editieren. Außerdem müssen Sie dann bei neuen Beiträgen nicht mehr die Felder Name, E-Mail und Homepage ausfüllen.

abbrechen