JavaScript is a wonderful language to work with. It's something that's easy to start using, but challenging to master due to its forgiving nature when compiling...
If you come from a more strict language like C++, some of the things that JavaScript does with variables might make you scratch your head at first, but thank it after you understand what's happening.
In JS we have the following variable types available:
boolean
- true/false valuesnumber
- integers and fractional valuesstring
- characters, words, and sentencesundefined
- value for uninitialized variablesobject
- set of values of any type
What's cool about JavaScript is that variables are dynamically typed, meaning that they can change type based on the values assigned to them. For example:
var x; // type: undefined
x = true; // type: boolean
x = 5; // type: number
x = "Hello world!"; // type: string
Variables also type dynamically when operated on. Often times this catches people off guard, and can result in unexpected behavior that is difficult to debug. If we were to add a string to a number in C++ it would spit out an error when compiling, but JS would go along with it. This behavior is called coercion.
Coercion: Arithmetic Operations
With arithmetic operations you'll primarily be working with strings and numbers. You might already be aware that when operating on strings with the + operator, the strings are concatenated(combined). An expression like 'Hello ' + 'World'
would return the value 'Hello World'
. What if one of the values in the expression isn't a string?
var x = 7; // type: number
var y = '7'; // type: string
console.log( x + y ); //> 77 // string
console.log( y + x ); //> 77 // string
In this example we declare var x = 7
and var y = '7'
such that x
has the type number and y
has the type string. We then add x + y
and output the result to the console. Despite one of the values not being a string, the +
operator still returns a concatenated string.
When encountering something that is not of the expected type, JS will attempt to convert its type, in this case resulting in '7' + '7'
. Since combining strings with the + operator concatenates the values, we end up with '77'
.
This automatic type conversion can be helpful in some situations, but can also result in difficult to find bugs in your code. Coercion is the reason form validation is so important, especially when you are operating on user input.
Say for example we needed to perform other operations on this value(multiplication, division, etc), it would work, but be very far from the expected result.
var x = 3;
var y = '10';
console.log(x * y); //> 30 // number
console.log(y * x); //> 30 // number
With arithmetic operations other than +
, the string is the value that is coerced and the parser attempts to return a number.
It's good to keep in mind: all numbers have a string representation, but only some strings have a number representation.
var x = 7; // type: number
var y = "seven"; // type: string
console.log(x + y); //> 7seven
If we started performing other arithmetic operations on this string(multiplication, division, etc), since the string can't be coerced to a number, we would result in something weird, a value type that I did not mention above.
This is the NaN
value type, or not a number. This is produced when a number is expected, but the computation cannot be made.
var x = 7; // type: number
var y = "seven"; // type: string
var notAnumber = x * y;
console.log(notAnumber); //> NaN
What makes NaN
particularly challenging to handle? It's the only value in JS that does not equal itself...
This transitions us into the realm of logic, arguably the most important topic when discussing coercion.
Coercion: Logical Operations
So we didn't account for someone being dumb enough to spell out numbers when using our calculator app... Whoops... Well at least this is easy enough to catch. What if we have some sort of event that is supposed to occur when two value are equal?
var x = 7; // type: number
var y = 'seven'; // type: string
if( (y * 7) == (y * x) ) {
console.log('True!');
} else {
console.log('False!');
}
//> False!
Since NaN
is not equal to itself, the if condition is false, so the else runs despite the values, from a human perspective, appearing to be the same.
Without validating the values being operated on, it's very easy to miss something like this. Unlike other languages, JavaScript isn't going to complain when you hand it unlike values, and because of this 'silent failure', debugging a more sophisticated app can be a torturous process.
Before we continue, let's go over binary logical operators:
... && ... // and (both true)
... || ... // or (one is true)
... > ... // greater than
... >= ... // greater than or equal to
... < ... // less than
... <= ... // less than or equal to
... == ... // equals
... != ... // not equals
There are others, but let's keep things simple for now.
Logical operators, like arithmetic operators, work left to right, meaning that the left is compared to the right and the result is a boolean type value, true or false, usually(we'll get to that). There is a great reference guide available on the Mozilla Development Network that covers this and much more about operators in JS for those who are curious.
Consider console.log( 4 < 2 < 1 )
. From our perspective, this should be false, but JavaScript doesn't agree...
When parsing 4 < 2 < 1
, it's operated on like (4 < 2) < 1
, and since 4 < 2
results in false
, the next step of the operation is false < 1
.
How does that work? The boolean value is coerced to a number. When coercing booleans to numbers, true becomes 1
and false
becomes 0
, so we end up with 0 < 1
, which is true
.
So now we know a little bit about binary operations in JS and how booleans coerce to numbers... How do numbers(and other values for that matter) coerce to booleans?
Well here is where things get a little funky.
Here is a little example:
var num;
var str;
var arr = ['','a','b'];
for(var i = 0; i < 3; i++) {
num = i;
str += arr[i];
if(num) {
console.log('number: ' + num + ' coerces to true');
} else {
console.log('number: ' + num + ' coerces to false');
}
if(str) {
console.log('string: ' + str + ' coerces to true');
} else {
console.log('string: ' + str + ' coerces to false');
}
}
//> number: 0 coerces to false
//> string: coerces to false
//> number: 1 coerces to true
//> string: a coerces to true
//> number: 2 coerces to true
//> string: ab coerces to true
Put simply, the number zero and empty string coerce to false
, while everything else coerces to true(even for negative numbers)... But not always... The way the values are considered in the above example is by existence, meaning that if the value exists and isn't zero, then the expression evaluates to true
.
When actually compared with boolean values, the previous inference only holds true for the number zero, the number one, and the empty string. So... if we had the expression true == 5
or true == 'hello'
, the result would be false
.
Even more oddities begin bubbling up when working with null, undefined, and empty object values. So how do we work around this?
There are a number of methods and operators to safely handle coercion that are built into JS, which I will discuss later, but in many cases, coercion can be a powerful tool. This brings us to our next talking point.
Leveraging Coercion
I know that I've spent the better part of this post reminding you how dangerous coercion can be, but now I'm going to tell you how wonderful it can be.
Existence
The example loop above touches on the first, and arguably most common way in which developers leverage JavaScript coercion, the existence conditional. In fact, it's so common that you've probably already done it.
Say for example you had a block of code in a function that needed to run if some variable had a value. Simply use the variable as a conditional:
var person = {}; // type: object
for(var i = 0; i < 10; i++) {
if(person) { // runs if person has data
console.log('person exists on loop ' + i);
break;
}
if(i) {
person.name = 'John Doe';
}
}
//> person exists on loop 2
I'll admit that this isn't a particularly practical bit of code, but it gets the idea across. The concept itself is incredibly handy and has a wide variety of practical uses.
One way to coerce values in a similar way is with the !
operator, which works by inverting the truth value. However, if you do this on a conditional, make sure you understand what is happening.
This:
if(item) {
console.log('the item exists');
} else {
console.log('the item does not exist');
}
becomes this:
if(!item) {
console.log('the item does not exist');
} else {
console.log('the item exists');
}
If you don't want the value inverted but need it coerced, just do !!item
. It is somewhat a choice of style, but if you are logging to the console or assigning the existence of a one variable to another, this is an arguably the best approach.
Short Circuit Logic
This is my personal favorite when it comes to the side effects of coercion. If you thought the last one had some nifty uses, prepare to be amazed. This one is a little less common, but no less powerful.
We have already learned that logical operators go left to right, now it's time to see that in action. Let's start by discussing &&
, the and operator.
The &&
operator evaluates to true
when both values given to it (left value and right value are true
), but how does that work?
First, JS assesses the left value, if that value is true
, it moves on to assess the value on the right. Straightforward so far. Since the &&
operator requires both values to be true
in order to evaluate to true
, if the left value is false
it returns that value, be it an empty object, empty string, or zero.
The parser won't even look at the value on the right if the left can be coerced to false
. On the other hand, if the value on the left is true
, then the second value is returned, whatever it may be.
Let's see how this can be utilized:
var isTrue = 1;
var isFalse = 0;
var trueEvent = function() {
console.log('first value is true');
return 'cat';
}
if(isTrue && trueEvent()) {
console.log('first if: both true');
console.log(isTrue && trueEvent());
}
if(isFalse && trueEvent()) {
console.log('second if: both true');
}
//> first value is true
//> first if: both true
//> cat
In a way, the conditional expression is acting like its own if statement. We passed a value and a function to both if
statements. The first of which, we passed a value that coerces to true
, so the function trueEvent() was called. Our function performed an action and returned a value that coerced to true
, so the code in the first is ran.
This can be incredibly useful for doing things like operating on a value if it exists before satisfying the conditional. I won't go deep into usage cases here, but you should take some time to think of creative ways in which these behaviors can improve your code.
The ||
operator is essentially the opposite. If the left value can coerce to true
, it returns that value without considering the value on the right. If the left value is false
, the value on the right is returned. Like so:
var uploadMG = {};
var userIMG= uploadIMG || '/user/default.jpg';
console.log(userIMG); //> /user/default.jpg
This is great when assigning default values when none exist. Again, take a few minutes to think of some creative ways to leverage this.
Safety
What if you don't want your variables to coerce without you knowing? Thankfully there are a couple methods to accomplish this. The first of which being the unary typeof operator, which outputs a string of a given value's type.
Let's see that in use:
var x = 5;
if(typeof x === 'number') {
console.log( x + 'is a number');
}
//> 5 is a number
There is something in that example that may throw some people off, especially if you are coming from a language like C++. The ===
operator, also known as strict equality. The inverse of it is !==
, strict inequality. These differ from the equality operator in that they also check the types of the values they compare:
var x = 5;
var y = '5';
if(x==y) {
console.log('equal');
} else {
console.log('strict not equal');
)
if(x === y) {
console.log('strict equal');
} else {
console.log('strict not equal');
)
//> equal
//> strict not equal
Since the values compared are not the same type, the strict equality returns false
. This is useful when the type of the value matters in your code.
When checking or comparing the types of values, there are a few pitfalls, most notably, arrays. In JavaScript, an array is a type of object, so if we were to check it's type, it wouldn't be helpful:
var val = [1,2,3,4,5];
if(typeof arr === 'val') {
console.log('this is an object');
}
//> this is an object
Fortunately, the object constructor Array
has a method isArray()
that we can use to differentiate them from regular objects. It looks something like this:
var val = [1,2,3,4,5];
if(Array.isArray(val)) {
console.log('this is an array');
}
//> this is an array
Now you can safely identify and compare object types when coercion is an issue, and thanks to your understanding of how coercion works, write more eloquent code.