Cover Photo by Tony Tran on Unsplash
Source:
The few examples to follow look pretty unhinged, fair warning.
The factor most at fault is automatic type conversion due to loose typing, but operator overload is also a problem.
The plus +
operator is overloaded
The + operator in JavaScript has two different functions. As you might expect, it can add numbers together, but it can also concatenate strings.
💡 Concatenation is when two (or more) strings are taped together to form a new, combined string.
For example, concatenating
"hello"
and"world"
results in"helloworld"
.
This is done with the+
operator in JavaScript, but other languages might use other methods, such as aconcat()
function or a different operator.
When an operator can do multiple different things like this, it is referred to as an overloaded operator.
By contrast, if you look at the minus -
operator, it only does math, and doesn't have a defined operation for strings at all. Minus is not overloaded.
Type coercion
JavaScript has two distinct equality operators, one that compares content but not type (==
), and one that compares content and type (===
).
const a = "2"; // string
const b = 2; // number
a == b; // "2" == "2"
a === b; // "2" === 2
So how does the loose equality operator (==
) determine if two variables have the "same" content? It converts one or both values.
Taking a look at "2" == 2
, let's go over the process step-by-step.
- Check if both values are of the same type. In this case, they aren't, and need to be converted.
- Determine the type(s) to convert to. JavaScript's loose equality algorithm determines which values to convert, and to what. In this case, the string is converted to a number.
- Compare the converted values. In this case,
2
(converted from string) and2
(original) are equal, so the return value is true.
Another resource to check for this is the JavaScript Equality Table.
💡 Advanced note on equality for arrays and objects
So far, we've looked at strings and numbers for
==
and===
, but it functions differently for more complicated constructs like arrays and objects. In these cases, both operators check that the operands have the same reference. For more information, see MDN.
Empty array + empty array
Let's take a look at the first (and easiest to explain), empty array plus empty array.
[]+[]; // evaluates to ""
Here's what's happening:
- JavaScript sees the
+
operator and thinks about what types are valid for this: strings and numbers. Additionally, the two operands should match each other.- Two strings will be concatenated (
"a" + "b"
=>"ab"
) - Two numbers will be added numerically (
1 + 2
=>3
).
- Two strings will be concatenated (
- JavaScript looks at what the actual variables used in the operation, and sees if any need to be converted. In this case, both arrays need to be converted, since they aren't strings or numbers. For
+
, both arguments must be converted to "primitive" values (either a string, number, or boolean). - JavaScript uses a specific set of logic to cast the arguments to the type it wants
- Arrays are converted to a predetermined primitive type, which is a string 📝. An empty array becomes an empty string,
""
.
- Arrays are converted to a predetermined primitive type, which is a string 📝. An empty array becomes an empty string,
- Two empty strings concatenated together make an empty string - and there's your result!
📝 How does an array become a string?
In JavaScript, you can test this out by using
String([...])
or([...]).toString()
to observe how different arrays are transformed into strings.The elements in the array are transformed into strings, and those stringified elements are joined together with a comma.
String(["a", "b"]); // "a,b" String(["a", 2, ["b", 3]]); // "a,2,b,3"
This is much different from the result of
JSON.stringify
!
Empty array + empty object
This one works very similarly to the above. JavaScript determines that both operands need to be strings in order to continue, and converts them both. As we have seen previously, []
becomes ""
.
The only difference is that an empty object {}
, when cast to a string, becomes "[object Object]"
.
💡 Why
"[object Object]"
?From https://stackoverflow.com/a/25419538
As others have noted, this is the default serialisation of an object. But why is it [object Object] and not just [object]?
That is because there are different types of objects in Javascript!
Empty object + empty array
Despite the title of this section, the {}
is actually not interpreted as an object.
When used at the start of an expression, and there's no other indication that it ought to be an object, JS decides it's a block statement instead.
Here's what a non-empty block statement looks like:
{
let foo = 'bar';
console.log(foo);
}
You've seen these before, though they're usually used with constructs like if-statements, loops, and the like:
if (true) {
let foo = 'bar';
console.log(foo);
}
In the case of this particular example, it's interpreted as an empty block statement, which is functionally equivalent to typing nothing.
The reason the previous example was interpreted as an empty object instead of an empty block was that it was being used as a right-hand operand for +
. JavaScript supports +
being used to cast to number type when used without a left-hand operand:
// left and right
1 + "1"; // "11"
// casting with right-side only
+"1"; // 1
+1; // 1
+true; // 1
Thus, we can reduce the problem to +[]
.
When JS converts an array to a number, it is effectively the same as casting that same array to a string (as seen in the two previous examples), then casting that string to a number. (Note that casting the array to a string converts values such as null
and undefined
to empty string as well.)
[].toString(); // ""
Number(""); // 0
["1"].toString(); // "1"
Number("1"); // 1
["1", "2"].toString(); // "1,2"
Number("1,2"); // NaN, can't parse this
["a"].toString(); // "a"
Number("a"); // NaN, can't parse this
So now we can see where the 0
comes from - it's the numeric value of an empty string.
Type coercion with different operators
So far we've worked only with the +
operator, which is an overloaded operator: it can be used for more than one thing, depending on the operands. However, the mathematical opposite, -
, is only used for mathematical subtraction, and can't be used to manipulate strings at all.
The examples used in the reddit post:
9 + "1"; // "91"
91 - "1"; // 90
Since one of the operands of 9 + "1"
is a string, even if it's a numeric string, the +
operation results in a string concatenation.
However, since -
cannot be used for string concatenation, the "1" is interpreted as numeric.
Here are some more examples of this principle in action:
// two numeric operands is always math
1 + 2; // 3
1 - 2; // -1
// numeric strings are concatenated for addition, and converted for subtraction
1 + "2"; // "12"
"1" + 2; // "12"
"1" + "2"; // "12"
1 - "2"; // -1
"1" - 2; // -1
"1" - "2"; // -1
// non-numeric strings are concatenated for addition, and result in a math error for subtraction
1 + "hello"; // "1hello"
"hello" + 1; // "hello1";
1 - "hello"; // NaN
"hello" - 1; // NaN
When is a true
a 1
?
true + true + true === 3; // true
true - true; // 0
As in the section dealing with []+[]
, we know that JavaScript will convert operands if they aren't of the appropriate type. In this case, the boolean value true
is cast to a numeric 1
, and as we all know, 1 + 1 + 1
is in fact three.
Similarly, 1 - 1
we know to be zero.
Note that these can be cast to the strings "true" and "false", for example when a string is an operand:
1 + "hello" + true + false; // "1hellotruefalse"
When is a true
not a 1
?
true == 1; // true
true === 1; // false
The numeric value of true
is 1
, so the loose equality operator sees them as having the same value.
The reddit post lists the following statements:
true == 1; // yes, because true is cast to 1
true === 1; // no, because true and 1 are not the same type
[] == 0; // yes, because empty array is cast to 0 (see empty object + empty array example)
Abusing type coercion for fun and profit
The last example from reddit is basically a gigantic exercise in learning automatic type conversion, so let's break it down.
(!+[]+[]+![]).length; // 9
The first thing we need to ask here is: The length of what, exactly? Let's take a look at what's in those parentheses.
!+[]+[]+![]; // "truefalse"
The .length
might have led to you believe that it was an array of length 9, but here we have a string of length 9. Now, let's figure out why we get that string out of that expression.
Let's separate it out a little bit, so it's more readable:
(!+[]) + ([]) + (![]);
That first set of parentheses contains two explicit conversions, which are worked from the "inside out" - the closest conversion to the value goes first, then it works its way outwards. So first, []
is our value, which is converted to a number by +[]
, which results in 0
. Next, the !
converts the number to a boolean value and inverts it - 0
converts to a boolean false
, which when inverted results in true
.
Let's leave that empty array in the middle for a moment. It isn't being explicitly cast.
On the right, we have ![]
- at first glance, it seems like this also ought to be true, since !+[]
is true. However, the extra conversion to number makes all the difference! When converted directly to a boolean value, an empty array results in true
, which when inverted is false
.
Number([]); // 0
Boolean([]); // true
Boolean(0); // false
So now, we have the simplified form of our expression from earlier:
true + [] + false
Now on to that +
operand. The choice with +
is either numeric plus, or string concatenation, and JavaScript's internal ruleset says that when you are using +
with a boolean and an array, both are cast automatically to strings and concatenated. From left to right, we can work this expression out:
// true + [] + false
true + []; // "true"
"true" + false; // "truefalse"
And there you have it.
Wait, so... why?
Why is JavaScript loosely typed, and why does it do automatic type conversion?
Brandon Eich designed the language in 1995 in under two weeks. I don't really think there's a "why" to it, to be honest. Maybe we can blame a lack of sleep on his part.
Why does X convert to Y?
There is an internal logic to all the automatic type conversion JavaScript does - it is, after all, run by a computer, and computers don't just spontaneously decide to do something a little different to mess with you.
You might notice that arrays and objects are tricky, being cast to many different possible primitive values:
// empty arrays
Number([]); // 0
String([]); // ""
Boolean([]); // true
// arrays with stuff in them
Number(["a"]); // NaN
Number([1]); // 1
Number([2]); // 2
Number([1, 2]); // NaN
Number([null]); // 0
Number([true]); // NaN
String(["a"]); // "a"
String(["a", 1]); // "a,1"
String([null]); // ""
Boolean([false]); // true
// empty objects
Number({}); // NaN
String({}); // "[object Object]"
Boolean({}); // true
Most JavaScript-specific WTFery actually depends, deep down, on these conversions.
Final notes
Much of this isn't JavaScript-specific. The many examples of JavaScript shenanigans are often simple floating-point errors, which can be found in any programming language that uses floating point numbers. (Please stop using these as examples of why JavaScript is bad. There's enough wrong about JavaScript to be mad about without bringing in floating point arithmetic.)
The true WTF is not necessarily that JavaScript can do automatic type conversion, but the absolute bonkers scale of how much it will convert, and to what.
What do I even do about this?
If you're coming from a strongly-typed language and/or felt like crying several times during this article, I suggest TypeScript.
If you can't or don't want to use TypeScript, make sure to follow these two rules:
- Always use strict equality (
===
) - Always explicitly convert types, rather than allowing JavaScript to implicitly convert them for you
JavaScript can't break your stuff with automatic type conversion if you've made sure things are the right type to begin with.
You made it to the end! Congrats for wading knee-deep into JavaScript's type system. Have a picture of a squirrel eating at a tiny table for your trouble. (via reddit)
Top comments (1)
developer.mozilla.org/en-US/docs/W...