My collection => https://gist.github.com/shapkarin/b3fbeaca95ef69df177b
Below is a (quite long!) tour of curious or surprising expressions in JavaScript, along with explanations of why they behave as they do. JavaScript’s dynamic typing, type coercion, loose equality, parsing rules, and handling of edge cases all contribute to these quirks. Many of these examples come directly from how the ECMAScript specification dictates type conversions should happen. While they might appear “weird,” each snippet has a logical (if not always intuitive) explanation once you look under the hood.
[] + {} // "[object Object]"
[]
) converted to a string is ""
(an empty string). An empty object ({}
), when converted to a string, becomes "[object Object]"
. String concatenation of "" + "[object Object]"
yields "[object Object]"
.+
operator, when given an object, will call toString()
(or valueOf()
, depending on the object) to coerce the object into a primitive. For arrays, [].toString()
becomes ""
. For a plain object {}
, {}.toString()
becomes "[object Object]"
. Finally, "" + "[object Object]"
is simply "[object Object]"
.{} + [] // 0
{}
at the beginning of a line can be interpreted as a standalone “block” rather than an object literal if there’s no preceding context. Once that’s parsed as a block, we then have + []
.{}
as an empty block and then sees + []
→ + ""
→ 0
.{}
as an empty statement block. Then +[]
becomes +""
, which is 0
.[] - {} // NaN
[]
becomes ""
when forced into a string, which in numeric conversion is 0
. But {}
as a number is NaN
. Then 0 - NaN = NaN
.-
operator always attempts numeric coercion. An empty object coerces to NaN
, so the result is NaN
.{} - [] // -0
{}
is likely interpreted as an empty block, then we see - []
. That becomes - ""
, which becomes -0
(negative zero is a valid number in JavaScript).( {} - [] )
on one line, it might give you NaN
because then it is truly an object minus array. But in many JS interpreters, the snippet alone is a block followed by -[]
.-[]
is effectively -(0)
, which yields -0
.1 + true // 2
true
coerces to numeric 1
, so 1 + 1 = 2
.1 + [] // "1"
[]
becomes ""
(an empty string) in a string context. Because at least one operand is a string, 1 + ""
is string concatenation → "1"
.+
operator will use numeric addition if both operands can be converted to numbers. But []
coerced to ""
can also be treated as a string. With mixed types, JavaScript tries string concatenation. Hence, "1"
.true + [] // "true"
[]
coerces to ""
, so true + ""
→ "true"
.true
is not automatically coerced to 1
if JavaScript decides to do string concatenation. Because the second operand is a string ([]
→ ""
), the first operand (true
) is converted to a string: "true"
.true + '' // "true"
''
is a string, true
is converted to "true"
, concatenated with ''
→ "true"
.[] == '' // true
[]
and ''
coerce to the empty string: [].toString()
→ ""
, and ""
is ""
, so "" == ""
→ true
.0 == '' // true
''
is an empty string. Under loose equality, ''
is converted to 0
. So 0 == 0
→ true
.0 == [] // true
[]
→ ""
, then ""
→ 0
, so 0 == 0
→ true
.[] == ![] // true
![]
is false
(an array is a truthy value, so ![]
→ false
). Then false
coerces to 0
. []
(for a numeric context) also goes to 0
. Thus 0 == 0
.[] → 0
and false → 0
.0 == '0' // true
'0'
to number is 0
, so 0 == 0
→ true
.[] == '0' // false
[]
→ ""
, and ""
is 0
as a number. '0'
is also 0
. So numerically it might seem 0 == 0
. Actually in older specs, [] == '0'
is false
—but it’s a tricky one:[]
→ ""
(string), comparing string to string '0'
→ false[]
is object → to primitive → ""
; '0'
is a string. "" == '0'
is false. JavaScript does not do ""
→ 0
for string-to-string comparison; it is done for number-to-string comparisons. In the official spec, '0'
remains '0'
, ''
remains ''
, they differ as strings.[]
becomes ""
, which is not equal to '0'
as a string.[] == '' // true
[...Array(2)] == ',' // true
Array(2)
is [empty, empty]
. Spread into [...]
yields [undefined, undefined]
– or basically two empty slots. When stringified, [undefined, undefined]
→ ","
(because each empty or undefined slot is turned into an empty string, and joined with commas). So the array becomes the string ","
. This is loosely equal to the string ","
.undefined
or “empty” become empty strings during .toString()
or .join(',')
, leading to one comma between them → ","
.new Array([], null, undefined) == ',,' // true
new Array([], null, undefined)
→ something like [[], null, undefined]
. When stringified:[].toString()
→ ""
null
→ ""
in array stringificationundefined
→ ""
in array stringification"," + "" + ""
→ ",,"
.",,"
, which is equal (in loose comparison) to the string ",,"
.![] == 1 // false
![]
is false
, because []
is truthy. false == 1
→ false
.![] == 0 // true
![]
→ false
, and false
as a number is 0
. So 0 == 0
→ true
.!![] == 1 // true
!![]
is true
, which as a number is 1
. So 1 == 1
.1 === 1.0 // true
1
and 1.0
are literally the same numeric value.1 === 1.9 // false
1
and 1.9
are different numeric values.1 === 1.00000000000000009 // true
1.00000000000000009
any more precisely than 1.0
, so they become the same value in memory.'a' === 'a' // true
'a' === new String('a') // false
new String('a')
is an object wrapper around the string 'a'
. Strict equality checks object identity, not value, so 'a'
the primitive is not strictly equal to an object with the same contents.'a' === new String('a').toString() // true
new String('a').toString()
becomes the primitive 'a'
. 'a' === 'a'
→ true
.var a = 0.1, b = 0.2, c = a + b c === 0.3 // false
- **What happens**: Floating-point arithmetic can’t exactly represent 0.1 + 0.2 = 0.30000000000000004. So `c === 0.3` is `false`.
- **Why**: This is one of JavaScript’s classic floating-point precision pitfalls.
---
## 7. Compound and Weird Arithmetic
###
```js
var x = 1
x += 1
// 2
x += 1
means x = x + 1
.var x = 1 x-=-!'' // 2
!''
→ !false
(the empty string is falsy) → true
-true
→ -1
x -= -1
→ x = x + 1
→ 2
.var x = 1 x-=-Math.cos([]) // 2
[].toString()
→ ""
, Number("")
→ 0
.Math.cos(0)
→ 1
.-1
→ -1
.x -= -1
→ 2
.Math.max() > Math.min() // false
Math.max()
with no arguments → -Infinity
. Math.min()
with no arguments → Infinity
. -Infinity > Infinity
is false
.Number.MIN_VALUE < 0 // false
Number.MIN_VALUE
is the smallest positive value (~5e-324), but it is still positive, so it’s not less than 0.NaN
typeof NaN // "number"
NaN
is a special numeric value, so typeof NaN
is "number"
.typeof [] // "object"
typeof {} // "object"
typeof null // "object"
null
all return "object"
from typeof
. This is an old, well-known quirk that is left in place for backward compatibility.NaN === NaN // false
NaN
is never equal to anything, not even itself.NaN == NaN // false
NaN
fails.isNaN(NaN) // true
isNaN(null) // false
isNaN(undefined) // true
isNaN([]) // false
isNaN({}) // true
isNaN()
tries to convert the argument to a number.NaN
→ obviously true
.null
→ Number(null)
is 0
, so isNaN(0)
→ false
.undefined
→ Number(undefined)
is NaN
, so true
.[]
→ ""
→ 0
→ not NaN → false
.{}
→ [object Object]
→ NaN
→ true
.[1,2,3] === [1,2,3] // false
[1,2,3] == [1,2,3] // false
JSON.stringify([1,2,3]) === JSON.stringify([1,2,3]) // true
"["1,2,3"]"
), and those strings are equal.[1, 2, 3] + [4, 5, 6] // "1,2,34,5,6"
[1,2,3].toString()
→ "1,2,3"
. Then concatenation with [4,5,6].toString()
→ "4,5,6"
. So the result is "1,2,3" + "4,5,6"
= "1,2,34,5,6"
.toString()
Calls0..toString() // "0"
0 .toString() // "0"
0..toString()
is a hack to let the parser know you mean Number(0)
with a decimal, rather than something else. It’s a trick to allow method calls on a number literal. The result is "0"
.[].toString() // ""
""
.+new Date() // 167...some large timestamp...
+
operator forces a numeric conversion. new Date()
→ numeric value is the milliseconds since the epoch. That’s the same as .getTime()
.+new Date() === Number(new Date) // true
+new Date() === (new Date()).getTime() // true
+[] // 0
[]
→ ""
→ 0
.+[] === 0 + [] // false
+[]
→ 0
. But 0 + []
→ 0 + ""
→ "0"
. "0" === 0
is false with strict equality.0 + [] // "0"
+
expression, concatenation occurs. 0
is converted to "0"
and "0" + ""
→ "0"
.typeof (0 + new Date()) // "string"
new Date()
as a primitive is a string (e.g., "Mon Mar 10 2025 ..."
). So 0 + "Mon Mar..."
→ a string. typeof
that is "string"
.parseInt
vs. parseFloat
parseInt("1.let's see") // NaN
parseInt("1.let's see")
reads "1."
as an integer but sees .
not followed by a digit. It stops parsing right after "1"
. Actually, the presence of 'l'
after the decimal confuses it, so it can’t parse beyond 1
as an integer. The partial parse gives 1
, but then sees an invalid character sequence. Historically, many engines end up with 1
or NaN
. Modern specs: if parseInt
finds a valid integer portion, it returns that integer. But with ".let's"
it’s ambiguous. Some engines might interpret it differently. Often you’ll get 1
. Some older or strict interpretations might yield NaN
. (Exact result can vary, but typically parseInt stops as soon as it can’t parse a valid integer, so the result is 1
. If your environment yields NaN
, it’s possibly a nuance of that engine. Check your browser or Node version.)parseInt
will stop at the first non-valid integer character, but a 'l'
or '
' might break it differently across engines.parseFloat("1.let's see") // 1
parseFloat
can parse a floating-point until it hits a non-digit/non-decimal character. It sees "1."
, stops at the l
, so returns 1
.parseFloat('123.321try') // 123.321
parseFloat('123.try321') // 123
parseFloat
parses until a non-numeric (including decimal) character, ignoring the rest.parseInt(1 / 0, 19) // 18
1 / 0
is Infinity
. Converting that to a string yields "Infinity"
. Then parseInt("Infinity", 19)
: it sees 'I'
as a valid digit? Actually in base 19, digits are 0–9 and a–i
(or A–I
). 'I'
can be interpreted as the digit 18
in base 19. Since it only takes the first character 'I'
, that becomes 18
.parseInt('xyz', 36) // 44027
'x'
, 'y'
, 'z'
are valid digits. 'x'
is 33, 'y'
is 34, 'z'
is 35, so we get a number in base 36 that is 44027 in base 10.parseInt(['a1', [2, [3, [4]]]], 11) // 111
[ 'a1', [2, [3, [4]]] ].toString()
→ "a1,2,3,4"
.parseInt("a1,2,3,4", 11)
.parseInt
stops as soon as it encounters a character that is not valid for base 11. 'a'
in base 11 is 10, '1'
is 1 → so it sees "a1"
as a valid chunk. That is 10*11 + 1 = 111
. Once it hits the comma, parsing stops.parseInt('1') === parseFloat('1') // true
1
.parseInt('a1', 11) === parseFloat('a1', 11) // false
parseInt('a1', 11)
→ 111
(as above). parseFloat('a1', 11)
tries to parse 'a1'
as a float in decimal, fails at 'a'
→ NaN
. 111 === NaN
→ false.Math.PI ^ 0 // 3
Math.PI ^ [] // 3
Math.PI << [] // 3
Math.PI >> [] // 3
Math.PI
(3.1415926...
) then perform integer bitwise operations:3.1415
truncated to integer = 3
.[]
→ 0
.3 ^ 0
= 3
. Same for shifting.~Math.PI // -4
~x
is the bitwise NOT. Math.PI
truncates to 3
. ~3
= -4
(in two’s complement).~~Math.PI // 3
~~x
is a neat trick to truncate a float to an integer. 3.1415...
→ 3
.Math.PI - Math.PI % 1 // 3
x % 1
is the fractional part for positive numbers. 3.1415 % 1
→ 0.1415
. Subtract that from 3.1415
→ 3
.parseInt
on an Array via map
['1', '7', '11'].map(parseInt) // [1, NaN, 3]
.map(parseInt)
calls parseInt(value, index)
. So it calls:parseInt('1', 0)
→ base 0 means base 10 in older browsers → 1
.parseInt('7', 1)
→ base 1 is invalid for digit '7'
→ NaN
.parseInt('11', 2)
→ '11'
in base 2 is 3
..map(x => parseInt(x, 10))
, or better: .map(Number)
, if you want decimal.NaNNaNNaN...
StringsArray(16).join('wat' - 1) + ' Batman!'
'wat' - 1
→ 'wat'
is not numeric, so NaN
.Array(16).join(NaN)
→ 'NaNNaNNaN...NaN'
(15 copies of 'NaN'
joined with no delimiter).' Batman!'
→ "NaNNaNNaN... Batman!"
."fail"
from Strange Indices;(![] + [])[+[]] + (![] + [])[+!+[]] + ([![]] + [][[]])[+!+[] + [+[]]] + (![] + [])[!+[] + !+[]]
"fail"
.![]
→ false
.false + []
→ "false"
.[+[]]
→ [0]
, then +[]
is 0
inside the bracket. So 'false'[0]
is 'f'
.'false'
or 'true'
."banana"
from 'b' + 'a' + + 'a' + 'a'
('b' + 'a' + + 'a' + 'a').toLowerCase(); // "banana"
'b' + 'a'
→ "ba"
.+ 'a'
tries to convert 'a'
to a number, fails → NaN
."ba" + NaN + 'a'
→ "baNaNa"
..toLowerCase()
→ "banana"
.$=_=>`$=${$};$()`;$()
$()
calls itself, returning the string with interpolation of $=${$}
. It’s basically quine-like code in JavaScript."NANO"
and "UNDEFINEDO"
console.log((undefined + undefined + "O").toUpperCase()); // NANO
undefined + undefined
→ "undefinedundefined"
(string concatenation, because the first undefined
in a string context is "undefined"
)."undefinedundefinedO"
, which as a substring is "undefinedundefined" → "NaN"
? Actually carefully:(undefined + undefined)
is NaN
if numeric. But in a string context, it can be "undefinedundefined"
—unless one is forced numeric.undefined
with a +
operator and second undefined
, which might indeed yield NaN
(if it tries numeric first). But often in practice, 'undefined' + 'undefined' = 'undefinedundefined'
."O"
might cause another string conversion → 'undefinedundefinedO'
..toUpperCase()
→ "UNDEFINEDUNDEFINEDO"
."NANO"
might be from a numeric path. (undefined + undefined)
→ NaN
, then NaN + "O"
→ "NaNO"
. .toUpperCase()
→ "NANO"
.NaN
because undefined
in a numeric context is NaN
; NaN + undefined
is also NaN
. Then NaN + "O"
→ "NaNO"
."NaNO"
. Then toUpperCase()
→ "NANO"
.console.log((undefined + "O").toUpperCase()); // UNDEFINEDO
(undefined + "O")
is definitely string concatenation: "undefinedO"
. Then .toUpperCase()
→ "UNDEFINEDO"
.JavaScript’s type system, especially its loose equality rules, automatic type coercion, and corner cases in parsing or arithmetic can produce results that seem bizarre at first glance. However, these quirks usually follow the language specification consistently:
+
operator can trigger numeric or string operations depending on the operand types.==
tries to align types by a maze of conversions (objects → strings
, strings → numbers
, booleans → 0
/1
, etc.).0.1 + 0.2 ≠ 0.3
).parseInt
, parseFloat
read strings in ways that can truncate or interpret unexpected digits or bases.Date
objects default to strings, while ordinary objects default to [object Object]
, or NaN
if forced numerically.Knowing these details helps you understand the surprising results—and avoid bugs in day-to-day coding. When you need reliable results, prefer strict equality over ==
, use explicit type conversions, and treat floating-point with caution. Although these examples can look “weird,” they underscore the importance of reading the specification’s rules for type conversion and how operators behave with various operand types.