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.