JavaScript teilt seine Wert-Welt in zwei Kategorien: sieben Primitives — Number, BigInt, String, Boolean, undefined, null und Symbol — und eine generische Object-Kategorie, die alles abdeckt, was kein Primitive ist (Arrays, Functions, Date, RegExp, Map, Set, Plain Objects). Diese Trennung entscheidet über fundamentale Eigenschaften jedes Wertes: Werden Veränderungen sichtbar, wenn der Wert herumgereicht wird? Sind zwei Werte gleich, wenn sie dasselbe „aussehen"? Was passiert, wenn man eine Methode aufruft? Dieser Artikel zeigt im Detail, wie die beiden Welten funktionieren — und warum die Unterscheidung in praktisch jeder Zeile JavaScript-Code mitspielt.
Zwei Welten: Primitives und Objects
Die ECMAScript-Spezifikation definiert acht „Language Types": die sieben Primitives plus die Object-Kategorie. Primitives sind atomare, immutable Werte — eine Zahl, ein String, ein Boolean. Objects dagegen sind Container für beliebig viele Slots (Properties), die im Lauf der Zeit geändert werden können. Der Unterschied wirkt sich an drei zentralen Stellen aus: in der Identität (was bedeutet „gleich"?), in der Übergabe (was passiert beim Funktionsaufruf?) und in der Lebensdauer (kann sich der Wert ändern, ohne dass ich es merke?).
// Primitive — by value, immutable
let a = 5;
let b = a; // Wert kopiert
b = 99;
console.log(a); // 5 — unverändert
// Object — by reference, mutable
const obj1 = { wert: 5 };
const obj2 = obj1; // Reference kopiert
obj2.wert = 99;
console.log(obj1.wert); // 99 — selber Container5
99Die sieben Primitives im Detail
Jedes Primitive hat einen klar definierten Wertebereich, ein eindeutiges typeof-Ergebnis und — bis auf null und undefined — einen Wrapper-Konstruktor, über den das Auto-Boxing läuft. BigInt kam erst mit ES2020 dazu; davor sprach man von „sechs Primitives".
| Primitive | typeof | Wertebereich | Beispiel |
|---|---|---|---|
Number | 'number' | IEEE 754 64-Bit, ±2^53 sichere Integer | 42, 3.14, NaN |
BigInt | 'bigint' | beliebig große Ganzzahlen | 9007199254740993n |
String | 'string' | UTF-16-Code-Unit-Sequenzen | 'hallo', "text" |
Boolean | 'boolean' | true oder false | true |
undefined | 'undefined' | nur ein einziger Wert: undefined | undefined |
null | 'object' (!) | nur ein einziger Wert: null | null |
Symbol | 'symbol' | jeder Symbol-Aufruf erzeugt einen neuen Wert | Symbol('id') |
null und undefined sind Sonderlinge: sie sind die einzigen Werte, die kein Wrapper-Object haben — und die einzigen, die einen TypeError werfen, wenn man eine Property auf ihnen anspricht.
console.log(typeof 42); // 'number'
console.log(typeof 9007199254740993n); // 'bigint'
console.log(typeof 'hallo'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' (historischer Bug)
console.log(typeof Symbol('id')); // 'symbol'number
bigint
string
boolean
undefined
object
symbolWas bedeutet „primitive"?
Drei Eigenschaften definieren ein Primitive: Immutability, By-Value-Vergleich und By-Value-Übergabe. Ein primitiver Wert kann nicht mutiert werden — alle String-Methoden, alle Number-Operationen erzeugen neue Werte. Zwei Primitives sind gleich, wenn ihr Wert gleich ist; es gibt keinen Begriff von „selbe Identität". Und wenn ein Primitive einer Funktion übergeben wird, bekommt die Funktion eine eigene Kopie.
const s = 'hallo';
s[0] = 'H'; // wirkungslos — Strings sind immutable
console.log(s); // 'hallo'
const upper = s.toUpperCase(); // gibt NEUEN String zurück
console.log(s); // 'hallo' — Original unverändert
console.log(upper); // 'HALLO'
// Number genauso:
let n = 5;
n.eigeneProp = 99; // wirkungslos (silently im strict mode → TypeError)
console.log(n.eigeneProp); // undefinedhallo
hallo
HALLO
undefinedEngines optimieren Primitives intensiv: kleine Integer werden oft direkt im Tagged-Pointer kodiert (Smi — Small Integer in V8), Strings werden geteilt und intern dedupliziert. Diese Optimierungen sind genau deshalb möglich, weil die Sprache garantiert, dass sich der Wert nicht ändert.
Objects und ihre Subtypen
Alles, was kein Primitive ist, ist ein Object — auch wenn es nicht so aussieht. Ein Array ist ein Object mit numerischen Indices als Property-Namen. Eine Function ist ein Object, das zusätzlich aufrufbar ist. Eine Date ist ein Object mit internem Timestamp-Slot. Diese Vielfalt sieht man an typeof: alle liefern 'object' — bis auf Functions, die eigens 'function' zurückgeben.
console.log(typeof {}); // 'object'
console.log(typeof []); // 'object'
console.log(typeof new Date()); // 'object'
console.log(typeof /regex/); // 'object'
console.log(typeof new Map()); // 'object'
console.log(typeof new Set()); // 'object'
console.log(typeof new Error('x')); // 'object'
console.log(typeof function () {}); // 'function' (Sonderfall)
console.log(typeof class C {}); // 'function' (Class ist syntaktisch eine Function)object
object
object
object
object
object
object
function
functionWer feiner unterscheiden möchte, nutzt Array.isArray(), instanceof Date, oder den verlässlichen Trick Object.prototype.toString.call(wert), der '[object Array]', '[object Date]', '[object RegExp]' etc. zurückgibt.
Die typeof-Tabelle vollständig
typeof ist der schnellste Weg, den Typ eines Wertes zur Laufzeit zu erfragen. Das Ergebnis ist immer ein String. Die Tabelle deckt alle Möglichkeiten ab — inklusive des berühmten null-Bugs.
| Wert / Typ | typeof-Ergebnis | Anmerkung |
|---|---|---|
undefined | 'undefined' | auch bei undeklarierten Variablen |
null | 'object' | historischer Bug aus der ersten JS-Engine |
true / false | 'boolean' | |
42, 3.14, NaN, Infinity | 'number' | auch NaN und ±Infinity sind Numbers |
123n | 'bigint' | seit ES2020 |
'text' | 'string' | |
Symbol('id') | 'symbol' | |
{} / [] / new Date() | 'object' | alle nicht-Function-Objects |
function(){} / class {} | 'function' | Sonderfall, eigentlich auch Object |
Der typeof null === 'object'-Effekt geht auf die allererste JavaScript-Implementation von 1995 zurück: Werte wurden als 32-Bit-Tagged-Union kodiert, mit drei Type-Tag-Bits. Object hatte den Tag 0, und null war der NULL-Pointer mit allen Bits auf 0 — fiel also automatisch in den Object-Bucket. Ein Vorschlag, das in ES5.1 zu reparieren, wurde verworfen, weil zu viel Code inzwischen implizit darauf baute.
Auto-Boxing — Methoden auf Primitives
Primitives haben keine eingebauten Methoden — 'hallo'.toUpperCase() müsste eigentlich einen Fehler werfen. Tut es aber nicht, weil JavaScript bei jedem Property-Zugriff auf einem Primitive einen temporären Wrapper-Object erzeugt, die Methode darauf aufruft und das Wrapper-Object danach verwirft. Dieser Mechanismus heißt Auto-Boxing.
const s = 'hallo';
// Hinter den Kulissen:
// tmp = new String(s); // Wrapper-Object
// result = tmp.toUpperCase();
// (tmp wird verworfen)
console.log(s.toUpperCase()); // 'HALLO'
console.log(s.length); // 5
const n = 3.14159;
console.log(n.toFixed(2)); // '3.14' — Number-Wrapper
const b = true;
console.log(b.toString()); // 'true' — Boolean-WrapperHALLO
5
3.14
trueWichtig: das Box-Object wird nicht persistiert. Wer einer Property auf einem Primitive einen Wert zuweist, schreibt in den temporären Wrapper, der direkt danach verschwindet — die Zuweisung ist effektiv wirkungslos.
Daraus folgt eine wichtige Anti-Pattern-Regel: niemals new String('x'), new Number(5) oder new Boolean(true) benutzen. Diese Konstruktoren erzeugen persistent gemachte Wrapper-Objects mit typeof 'object', die sich in fast jeder Hinsicht anders verhalten als das primitive Pendant.
const s1 = 'hallo';
const s2 = new String('hallo');
console.log(typeof s1); // 'string'
console.log(typeof s2); // 'object' — anders!
console.log(s1 === s2); // false — String !== Object
console.log(s1 == s2); // true — Coercion macht == lückenhaft
// Boolean-Wrapper ist besonders heimtückisch:
const b = new Boolean(false);
if (b) {
console.log('wird ausgeführt!'); // weil Object truthy ist
}string
object
false
true
wird ausgeführt!Reference vs. Value bei Funktionsaufrufen
Wenn eine Funktion ein Argument bekommt, hängt das Verhalten am Typ. Primitives werden kopiert — die Funktion arbeitet auf einer eigenen, lokalen Kopie. Objects werden als Reference übergeben — die Funktion sieht denselben Container wie der Aufrufer.
function aendern(p, o) {
p = 999; // wirkt nur lokal — Kopie wird verändert
o.wert = 999; // wirkt nach außen — Container ist geteilt
o = { wert: 0 }; // lokale Re-Bindung — wirkt nicht nach außen
}
const zahl = 42;
const objekt = { wert: 42 };
aendern(zahl, objekt);
console.log(zahl); // 42 — Primitive, unberührt
console.log(objekt.wert); // 999 — Object, mutiert42
999Die Verwirrung ist häufig: JavaScript ist nicht „pass by reference" im klassischen Sinn (wie etwa C++-Referenzen oder Pascal-var-Parameter). Es ist immer „pass by value" — aber bei Objects ist der Wert eben eine Reference. Eine Re-Bindung des Parameters innerhalb der Funktion (o = ...) ändert nichts am Original.
Mutability — Primitives sind eingefroren, Objects nicht
Diese Regel ist ohne Ausnahme: jedes Primitive ist immutable. Strings, Numbers, Symbols — kein Aufruf, keine Methode kann den Wert verändern. Was sich ändert, ist die Variable, die auf einen anderen Wert zeigt. Bei Objects ist das genau umgekehrt: Properties können hinzugefügt, geändert, gelöscht werden, ohne dass die Variable selbst neu gebunden werden muss.
// Primitive: man bekommt einen NEUEN Wert
let s = 'hallo';
s = s + ' welt'; // s zeigt jetzt auf einen NEUEN String
console.log(s); // 'hallo welt'
// Object: man verändert DENSELBEN Container
const arr = [1, 2, 3];
arr.push(4); // arr zeigt weiter auf dasselbe Array
arr[0] = 99;
console.log(arr); // [99, 2, 3, 4]
// Wer ein Object einfrieren möchte:
const fix = Object.freeze({ a: 1 });
fix.a = 99; // im strict mode: TypeError; sonst still ignoriert
console.log(fix.a); // 1hallo welt
[ 99, 2, 3, 4 ]
1Object.freeze ist allerdings flach — innere Objects bleiben mutierbar. Für tiefe Immutability braucht man entweder rekursives Deep-Freeze oder eine Library wie Immer.
Identity bei === — Primitives vs. Objects
=== (Strict Equality) verhält sich an einer entscheidenden Stelle anders, je nachdem ob die Operanden Primitives oder Objects sind. Bei Primitives ist === ein Wert-Vergleich: zwei Strings mit demselben Inhalt sind gleich, zwei Numbers mit demselben Wert sind gleich. Bei Objects ist === ein Identitäts-Vergleich: zwei Objects sind nur dann gleich, wenn sie dieselbe Reference sind — also derselbe Container.
// Primitives — gleich, wenn der Wert gleich ist
console.log('hallo' === 'hallo'); // true
console.log(42 === 42); // true
console.log(true === true); // true
// Objects — gleich nur bei selber Reference
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log(a === b); // false — verschiedene Container
console.log(a === c); // true — selbe Referencetrue
true
true
false
trueEine spezielle Variante ist Object.is. Sie verhält sich wie === mit zwei Ausnahmen: Object.is(NaN, NaN) ist true (während NaN === NaN immer false ist), und Object.is(+0, -0) ist false (während +0 === -0 true ist).
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false
// Beim Object-Vergleich identisch zu ===
const o = {};
console.log(Object.is(o, o)); // true
console.log(Object.is({}, {})); // falsefalse
true
true
false
true
falsePrimitives vs. Objects — die Übersicht
Die zentrale Tabelle zum Mitnehmen: jede Zeile ist ein konkreter Verhaltens-Unterschied zwischen den beiden Welten.
| Aspekt | Primitives | Objects |
|---|---|---|
| Speicherort (Engine) | oft Stack / Tagged-Pointer | Heap |
| Mutability | immutable | mutable (außer Object.freeze) |
===-Vergleich | by value | by reference |
| Funktions-Übergabe | by value (Kopie) | by reference (geteilter Container) |
| Methoden-Aufruf | via Auto-Boxing (temp. Wrapper) | direkt auf Property |
| Property-Zuweisung | wirkungslos / TypeError | persistent |
typeof | spezifisch ('string', 'number'...) | 'object' oder 'function' |
| JSON | direkt serialisierbar | rekursiv serialisierbar |
| Property-Schlüssel | nicht möglich (außer String/Symbol) | als Schlüssel nutzbar |
Symbol als Sonderfall
Symbol ist seit ES2015 das siebte Primitive (technisch das sechste — BigInt kam später). Jeder Symbol()-Aufruf erzeugt einen neuen, eindeutigen Wert, der mit keinem anderen Symbol gleich ist. Das macht Symbols ideal als kollisions-freie Property-Schlüssel — etwa für Library-Code, der nicht mit User-Properties in Konflikt geraten soll.
const s1 = Symbol('id');
const s2 = Symbol('id');
console.log(s1 === s2); // false — jeder Aufruf eigen
console.log(typeof s1); // 'symbol'
// Als Property-Schlüssel
const SECRET = Symbol('secret');
const user = {
name: 'Anna',
[SECRET]: 'verborgen'
};
console.log(user[SECRET]); // 'verborgen'
console.log(Object.keys(user)); // [ 'name' ] — Symbol nicht enumerablefalse
symbol
verborgen
[ 'name' ]Eine Vertiefung mit Well-Known-Symbols, Symbol.iterator und Symbol.for findet sich im dedizierten Symbol-Tutorial: /docs/javascript/symbols-reflect-proxy/symbol/.
Bemerkenswertes & Hintergrund
typeof null gibt 'object' — historischer Bug
In der allerersten JavaScript-Implementation von 1995 wurden Werte als 32-Bit-Tagged-Union mit drei Type-Tag-Bits kodiert. Object hatte den Tag 0, und null war als NULL-Pointer mit allen Bits auf 0 repräsentiert — fiel also automatisch in den Object-Bucket. Ein Fix-Vorschlag wurde später verworfen, weil zu viel Bestandscode implizit darauf baut.
typeof function gibt 'function' — Convenience-Sonderfall
Functions sind eigentlich Objects (haben Properties, einen Prototype, lassen sich erweitern), aber typeof macht für sie eine Ausnahme. Der Grund ist rein praktisch: in den 1990ern war Feature-Detection à la typeof obj.method === 'function' ein häufiges Idiom, und es schien sinnvoll, die Aufrufbarkeit ohne Umweg über instanceof Function abfragen zu können.
BigInt seit ES2020 als 7. Primitive
Vor ES2020 sprach man von sechs Primitives. BigInt wurde nötig, weil Number nur ganze Zahlen bis ±2^53 − 1 exakt repräsentieren kann — alles darüber verliert Präzision. BigInt hat keine Obergrenze, kann aber nicht direkt mit Number in Arithmetik gemischt werden — 1n + 1 wirft TypeError.
new Number(5) erzeugt einen Object-Wrapper
Das ist eine echte Falle: new Number(5) hat typeof 'object', ist nicht === 5, und new Boolean(false) ist sogar truthy in if-Bedingungen. Niemals mit new aufrufen — die Konstruktoren sind nur für Coercion gedacht: Number('5'), String(true), Boolean(0) — alle ohne new liefern Primitives.
Strings sind UTF-16-Code-Unit-Sequenzen, nicht Codepoints
'a'.length === 1, aber '😀'.length === 2 — weil das Emoji als Surrogate-Pair (zwei UTF-16-Code-Units) kodiert ist. Auch '😀'[0] liefert eine ungültige halb-Surrogate. Wer Codepoints zählen möchte, nutzt [...str].length oder Array.from(str).length; beide iterieren über volle Codepoints.
Object.is unterscheidet sich von === bei NaN und ±0
NaN === NaN ist false (per IEEE-754-Spec), Object.is(NaN, NaN) ist true. +0 === -0 ist true, Object.is(+0, -0) ist false. Für Generic-Equality (z. B. in Set/Map-Keys) nutzt JavaScript intern den „SameValueZero"-Algorithmus — eine Mischform: NaN ist gleich NaN, aber +0 und -0 ebenfalls.
Symbol gehört zu den Primitives
Auch wenn Symbols sich oft wie Objects anfühlen (Methoden-Calls, Description-Property), sind sie laut Spec ein Primitive. Sie haben kein Wrapper-Object, das man mit new erzeugen könnte (new Symbol() wirft TypeError) — die einzige Variante ist die Konstruktor-Funktion ohne new: Symbol('id').
structuredClone (ES2022) ersetzt JSON-Hack für Deep-Clone
Vor 2022 war JSON.parse(JSON.stringify(obj)) der Standard-Trick für Deep-Clones — mit den bekannten Schwächen: keine Functions, keine Dates (werden zu Strings), keine zirkulären Referenzen. structuredClone(obj) löst alle drei Probleme: Dates bleiben Dates, Maps und Sets werden korrekt geklont, zyklische Strukturen werden erkannt. Functions und DOM-Nodes werden allerdings weiter abgelehnt.