JavaScript: Wie funktionieren Closures?
Eine Closure ist eine Funktion samt dem Scope, in dem sie entstanden ist. Der Satz stimmt und erklärt nichts, bis man zwei davon dabei beobachtet, wie sie sich weigern, eine Variable zu teilen.
Zwei Zähler, eine Fabrik
function makeCounter() {
let count = 0;
return () => ++count;
}
const a = makeCounter(), b = makeCounter();
a(): 1 2 3
b(): 1
b beginnt bei 1, obwohl a bereits bei 3 angekommen ist. Jeder Aufruf von
makeCounter() hat ein neues count erzeugt, und jede zurückgegebene Funktion
hat ihr eigenes behalten. Gleicher Code, verschiedene Scopes.
Die Variable überlebt den Aufruf
makeCounter has returned, yet: still here
Normalerweise stirbt eine lokale Variable, wenn die Funktion zurückkehrt. Hier nicht, weil die zurückgegebene Funktion sie weiterhin referenziert und sie damit erreichbar bleibt. Das ist der ganze Mechanismus: keine Kopie des Werts, sondern weiterhin Zugriff auf die Bindung.
Gefangen wird die Variable, nicht der Wert
let msg = "first";
const readIt = () => msg;
msg = "second";
readIt() after reassigning: second
Die Closure liest msg beim Aufruf, nicht bei ihrer Entstehung. Dieses Detail
macht den Schleifen-Fehler weiter unten von überraschend zu naheliegend.
Der var-Fehler in der Schleife
for (var i = 0; i < 3; i++) fns.push(() => i);
with var : [ 3, 3, 3 ]
with let : [ 0, 1, 2 ]
var
hat eine Bindung für die gesamte Schleife. Alle drei Funktionen haben
dasselbe i eingefangen, und als sie liefen, war die Schleife durch und hatte
es auf 3 stehen lassen.
let
bekommt pro Durchlauf eine frische Bindung, jede Funktion also ihre eigene.
Über setTimeout
dasselbe, und dort begegnet es einem meistens:
var:3 var:3 var:3 let:0 let:1 let:2
Bevor es let gab, hat man den Scope von Hand erzeugt:
for (var i = 0; i < 3; i++) (function (j) { fns.push(() => j); })(i);
with an IIFE : [ 0, 1, 2 ]
Der Parameter j ist pro Aufruf eine neue Variable — genau das, was let einem
heute abnimmt.
Privater Zustand ohne Klasse
function account(start) {
let balance = start;
return { deposit: n => balance += n, get: () => balance };
}
acc.get() : 150
acc.balance : undefined
balance ist keine Property, also kommt von außen nichts heran. Das war
jahrelang der übliche Weg zu Privatheit in JavaScript und ist immer noch der
kürzeste.
Was eine Closure am Leben hält
function leak() {
const big = new Array(1_000_000).fill("x");
return () => big.length;
}
the closure reports length 1000000 so the 1,000,000-item array cannot be freed
Die zurückgegebene Funktion erwähnt nur big.length — eine Zahl. Das Array
bleibt trotzdem, denn eine Closure behält ihren Scope, nicht den Teil davon, der
benutzt aussieht. Legt man so eine Funktion an einen langlebigen Ort, in einen
Event-Listener oder einen Cache, lebt das Array so lange wie der Listener.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).