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, ...).