[] + {} = wat.

July 2016

In den beiden großartigen Talks Wat und The Birth & Death of JavaScript werden ein paar witzige Eigenheiten von JavaScript gezeigt. Die meisten davon ergeben aber - zumindest ein wenig - Sinn.

Array + Array = String

› [] + []
‹ ''

Um zu verstehen, wieso Array + Array einen leeren String ergibt, sollte bekannt sein, dass JavaScript nur wenige Primitives besitzt - darunter Number und String. Laut Specification werden beide Seiten um den Addition-Operator + herum an ToPrimitive() übergeben. Ist mindestens eine der Seiten kein Primitive sondern ein Object, wird versucht sie in eins umzuwandeln.

In ES6 ist ToPrimitive() grundsätzlich recht simpel definiert: ToPrimitive (input [, PreferredType]). Bei einem + ist kein PreferredType gegeben und es wird von 'default''number' ausgegangen (der einzige weitere verfügbare Wert ist 'string').

Nun werden, je nach PreferredType, die beiden Funktionen toString und valueOf ausgeführt. Bei 'string' in dieser, bei 'number' in umgedrehter Reihenfolge. In unserem Fall also zuerst [].valueOf() und dann [].toString(). Ist eines der beiden kein Objekt - also ein Primitive - wird es als Ergebnis zurückgegeben. Wenn keine der Methoden funktioniert, wird ein TypeError geworfen.

› [].valueOf()
‹ [] // type: object
› [].toString() // == [].join()
‹ '' // type: string

TL;DR: [] als Primitive gecastet ist ein leerer String: ''. [] + [] ist also gleich '' + '' und damit gleich ''.

Array + Object != Object + Array

Mit Wissen über ToPrimitive() sollte es nicht verwundern, dass [] + {} gleich '[object Object]' ist. Im Endeffekt steht hier ([].toString() = '') + ({}.toString() = '[object Object]').

Interessanter wird es mit {} + [] = 0, das ist an sich tatsächlich falsch. {} wird hier nicht als Object, sondern als leerer Code Block interpretiert. Was also tatsächlich ausgeführt wird, ist + []. Und ein + ohne linken Teil wird zu einem Unary Plus-Operator. Die Funktionsweise ist im Grunde ähnlich dem Addition-Operator gefolgt von einem Cast zu Number. So wird aus + [] also + '', Number('') und damit 0.

Das erklärt auch, wieso {} + {} gleich NaN ist: + {}Number('[object Object]')NaN.

Soll nun ein Object an erster Stelle nicht als Code Block interpretiert werden, kann es in () gelegt werden:

› {}.valueOf()
‹ Uncaught SyntaxError: Unexpected token .
› ({}.valueOf())
‹ Object {}

Das passiert in den Konsolen der Browser automatisch. In Chromes History (⇧) z.B. wird es angezeigt.

› {} + [] == [] + {}
‹ false
› ({} + [] == [] + {})
‹ true

Und nun?

Was lässt sich damit anstellen? Bspw. seinen Sicherheits-Mechanismus mit sehr unleserlichem Code generieren. Ein nun verständliches Beispiel, das im Talk genannt wurde und eigentlich nur die Anwendung oben genannter Regeln in einer facy Umgebung ist:

› y = {}
› y[[]] = 1
› Object.keys(y)
‹ ['']

Ebenso kann alternativ zu x[2] auch mit x[!+[] + !+[]] auf die dritte Value in x zugegriffen werden.

Ganz was anderes: Array.map(parseInt)

… ist immer eine schlechte Idee. Und hat nichts mehr mit Unary Plus zutun.

› ['10', '10', '10'].map(parseInt)
‹ [10, NaN, 2]

Das zu erklären ist simpel: map() gibt dem Callback nicht nur die Value, sondern als zweites Argument den Index (und den ganzen Array als drittes). parseInt() akzeptiert zwei Argumente: Den zu castenden Wert und die Base (definiert für ≥2 und ≤62). Hier wird es also wie folgt aufgerufen:

› parseInt('10', 0, ["10", "10", "10"])
‹ 10 // 10 base 0, 0 < 2
› parseInt('10', 1, ["10", "10", "10"])
‹ NaN // 10 base 1, 1 < 2
› parseInt('10', 2, ["10", "10", "10"])
‹ 2 // 10 base 2

Irgendwie wollte ich das mal aufschreiben.