This chapter follows Symbols in JavaScript.
Paste these into a console when you want a small smile and a smaller crisis of faith. This page is a focused article on that set. Every value is what the language specifies. The expression looks illogical, the result is consistent, and each section names the rule that is enjoying itself.
How these results follow the language
The language is committed. Three rules cover the list, and none of them is a bug report waiting to happen.
- Numbers use one binary format, so some integers and some decimal fractions are stored as a nearby value.
+,-, and==convert their operands before they run.- A
{at the start of a statement is a block. The same characters in an expression are an object.
The sections below apply those rules to the expressions people paste, then stare at. Type coercion and equality states the algorithms in full; Numbers and BigInt covers safe integers and fractions.
NaN is a number
NaN is the value of an operation that has no numeric result, and its type is number. The name says one thing. typeof says another, and it says it calmly: "number", the same answer it gives for Infinity and for 0.
typeof NaN;// "number"
NaN === NaN;// false
Number.isNaN(NaN);// true
Object.is(NaN, NaN);// true=== compares NaN unequal to every number, including itself. It declines to be itself. Number.isNaN returns true for that value and false for a string or a boolean. Object.is treats the two NaN values as the same, which is the reunion === refused.
A typeof check for "number" accepts NaN, because the type is number. The badge checks out. The value still is not a usable number. Type Guards in TypeScript narrows with typeof. After a value has been narrowed to number, Number.isNaN rejects NaN.
Integers past the safe range
A JavaScript number keeps 53 bits of precision. Every integer from -(2^53 - 1) through 2^53 - 1 is exact. Number.MAX_SAFE_INTEGER is that upper bound, 9007199254740991. Past it, the format can store only some integers. A number literal is rounded to the nearest value the format can hold, and the extra nines leave without saying goodbye.
99999999999999999;// 100000000000000000
Number.isSafeInteger(99999999999999999);// false
Number.MAX_SAFE_INTEGER;// 9007199254740991Seventeen nines go in. A round hundred quadrillion comes out. The rounding happens as the literal becomes a number, so the original digits are already gone. BigInt called with that number keeps the rounded value, which is a loyal and useless favor. A bigint literal, or BigInt called with the digits as a string, keeps every digit.
BigInt(99999999999999999);// 100000000000000000n
99999999999999999n;// 99999999999999999n
BigInt("99999999999999999");// 99999999999999999nDecimal values that share a binary format
The same format stores fractions in binary. 0.5 is 1/2, so it is exact, and it behaves. 0.1 is a repeating binary fraction, so the stored value is the closest approximation the format has. Close, in this format, is a technical term.
Adding the exact 0.5 to 0.1 lands on the same bits as the literal 0.6. Both sides of == are numbers here, so the comparison is the same one === would make. This pair agrees, which is how the next pair gets you.
0.5 + 0.1 == 0.6;// true0.1 + 0.2 lands on the next representable value above the literal 0.3. The sum prints as 0.30000000000000004 and waits for you to doubt your arithmetic.
0.1 + 0.2 == 0.3;// false
0.1 + 0.2;// 0.30000000000000004Decimal quantities stay exact when the code counts the smallest unit as an integer. For two decimal places, round to cents and compare those integers. The cents agree. The raw fractions were never going to.
const cents = (value) => Math.round(value * 100);
cents(0.1 + 0.2) === cents(0.3);// trueMath.max and Math.min with no arguments
Ask Math.max for the largest of no numbers and it answers -Infinity. Ask Math.min for the smallest of nothing and it answers Infinity. Empty input, infinite confidence. Those values are the identity elements of the operation: the maximum of a number and -Infinity is the number, and the minimum of a number and Infinity is the number. The functions can then fold a list from an empty start, which is a sensible reason for a startling answer.
Math.max();// -Infinity
Math.min();// InfinitySpreading an empty array calls the function the same way, with zero arguments. The array contributed nothing. The function still had an opinion.
Math.max(...[]);// -Infinityconst scores = [];const best = scores.length === 0 ? undefined : Math.max(...scores);Arrays and objects under plus
+ converts both sides with ToPrimitive, then looks at the results. When either result is a string, + concatenates. Otherwise it converts both sides to numbers and adds them. Addition, in other words, keeps a second career in string joining, and it will take that career at the first excuse.
flowchart TD
start["+"] --> prim["ToPrimitive on both sides"]
prim --> str{"Either value is a string?"}
str -->|yes| concat["Concatenate the strings"]
str -->|no| add["Convert both to numbers and add"]ToPrimitive tries valueOf first. For an array, valueOf returns the array itself, so the conversion continues to toString, which joins the elements with commas. An empty array joins to "". Two empty strings concatenate to "". Two lists, added, become the absence of text.
[] + [];// ""
[1, 2] + [3];// "1,23"[1, 2] + [3] is "1,23", which is what you get when a join and a concatenation share a keyboard. A plain object follows the same path: valueOf returns the object, and toString returns "[object Object]", the phrase an object uses when it has nothing more specific to say. Concatenating that with the empty string from [] produces "[object Object]".
[] + {};// "[object Object]"Arrays in JavaScript covers join, map, and the other array methods. + uses the string toString already returned.
The same steps produce the nine-character string in (!+[]+[]+![]).length. Unary + converts [] with ToNumber. ToNumber("") is 0, and !0 is true. An array is an object, so it is truthy, and ![] is false. The empty box is true. Its negation is false. true + [] concatenates because the array became a string, and "true" + false concatenates because the boolean becomes "false". The result is the word truefalse, length 9, which is a lot of letters to get from a handful of brackets.
+[];// 0
!+[];// true
!+[] + [];// "true"
![];// false
!+[] + [] + ![];// "truefalse"
(!+[] + [] + ![]).length;// 9A brace that starts a statement
When a statement begins with {, those braces are a block. The block in {} + [] is empty. What remains is unary + applied to []. Unary + uses ToNumber, and ToNumber("") is 0. You wrote something that looked like an object. The parser saw a block, stepped around it, and the plus sign turned an array into zero.
Passing the line to eval parses it as a statement, which is the parse a console uses when the line begins with {.
eval("{}+[]");// 0Inside parentheses the same characters are an expression, so {} is an object literal. That object follows the + rule from the previous section and becomes "[object Object]". Parentheses, and the object remembers its name.
({} + []);// "[object Object]"A console that wraps the line in parentheses before evaluating it shows the object string. A console that parses the line as a statement shows 0. The characters match. The grammar position decides which despair you get.
When the value you want is a number or a string, write that conversion yourself.
Number([]);// 0
String({});// "[object Object]"Booleans, strings, and loose equality
+ and - both convert, and they convert toward different results. When neither side of + is a string, a boolean becomes a number: true is 1 and false is 0. Three truths, added, make a three. Subtract a truth from itself and nothing remains.
true + true + true === 3;// true
true - true;// 0- converts both sides to numbers, including a string of digits. + sees a string and switches careers.
91 - "1";// 90
9 + "1";// "91"91 - "1" is the number 90. 9 + "1" is the string "91". Same digits, opposite operators, and the quotes are the whole plot.
== converts until the types match. A boolean becomes a number first, so true == 1 is 1 == 1. Loose equality is happy to make the introduction. === keeps the type and the value, so a boolean and a number stay different, and true === 1 is false.
true == 1;// true
true === 1;// falseAn object compared with == to a number or a string goes through ToPrimitive and the comparison runs again. [] becomes "", and "" == 0 becomes 0 == 0. The empty array equals zero, after a short chain of polite conversions. === ends the chain immediately.
[] == 0;// true
[] === 0;// falseCheck your understanding
Question 1 of 8
0 of 8 checked