Since its humble beginnings at NetScape in 1995, JavaScript (JS) has evolved far beyond its original purpose of making web pages dynamic. ECMAScript (ES), the standard for JS, has seen numerous updates. Yet, many peculiarities from its early days persist and have become defining features of the language.

Recently, during a talk at Conf & Coffee 2018, I was reminded of some of the quirky aspects of JS that I’ve fallen prey to in the past. For newcomers, these oddities can be frustrating, especially given the language itself is preventing you from solving the problem at hand. Coming from a C/C++ background, truthy / falsy values, logical operators, scope and objects were especially perplexing.

Truthy and Falsy Values

A truthy value is any value considered true in a Boolean context (such as within an if-statement). Conversely, a falsy value is any value considered false in a Boolean context. The tricky part of working with truthy and falsy values in JS is understanding the implicit type conversion (type coercion) performed on non-Boolean values.

To avoid confusion, remember that the following values are falsy in JS:

false
0
null
undefined
''
""
NaN

Therefore, anything else must be truthy. Some examples of truthy values include:

true
"true"
"false"
"1"
"0"
"-1"
1
-1
1.62
-1.62
Infinity
-Infinity
{}
[]
function(){}
new Date()

Logical Operators

Logical operators add another layer of complexity in JS due to the nuances of data types and their interactions. This complexity can lead to unexpected behavior, especially when dealing with truthy and falsy values.

Consider the following examples:

// These seem consistent...
false == 0   // true
false == ""  // true
0 == ""      // true

// These are not so consistent...
false == null       // false
false == undefined  // false
null == undefined   // true

// This is common in other languages too, but easily be confused...
false == NaN  // false
NaN == NaN    // false

// Interesting behaviour around [], {} and strings...
false == []    // true
false == [0]   // true
false == [[]]  // true
false == {}    // false
[] == {}       // false
false == "0"      // true
false == "false"  // false

For a more comprehensive table of equality comparisons, go here.

To avoid the variability shown above, you can use the !! unary operator to explicitly convert values to true or false:

false == !!null       // true
false == !!undefined  // true
false == !!NaN        // true
false == !![]         // false
false == !!"0"        // false

However, a more robust solution is to use strict equality operators (=== and !==). These operators compare both value and type, reducing errors that stem from JS’s dynamic typing:

false === null       // false
false === undefined  // false
false === NaN        // false
false === []         // false
false === "0"        // false

Using strict equality helps prevent bugs caused by type coercion, making your code more predictable and easier to maintain. It also ensures that you and others can understand the intended logic without ambiguity.

On the topic of data types, JS has seven data types. Six of these are primitives:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (introduced in ECMAScript 6)

The seventh data type is Object. Understanding the differences between these types is crucial for writing robust JS.

Scope

When transitioning from languages like C to JS, understanding variable scope can be challenging. In C, variables declared within a set of curly brackets {} are confined to that scope. However, in JS, this can be less straightforward, especially when using the var keyword. In JS, variables declared with var are function-scoped, not block-scoped. This can lead to unexpected behavior:

for(var i = 0; i < 10; i++) {}
console.log(i); // 10

if(true) {
    var i = 20;
    var i = 30;
}
console.log(i); // 30

In the above code, the variable i is accessible outside the for-loop and if-statement, which might not be what you expect.

Furthermore, if you declare a variable without any keyword, it becomes global (assuming strict mode is not enabled). In the following code, local is confined to the function scope, but global becomes a global variable.

function globalisation() {
    var local = 3.14;
    global = 'global';
}

To avoid these pitfalls, it’s best to use let and const for variable declarations. These keywords bind variables to block scope, preventing unintentional global declarations and re-declarations.

let allows variables to be block-scoped and reassigned, but not redeclared within the same scope:

for(let i = 0; i < 10; i++) {}
console.log(i); // ReferenceError: i is not defined

if(true) {
    let i = 20;
    i = 10;
    console.log(i); // 10
    let i = 30; // SyntaxError: Identifier 'i' has already been declared
}

const is similar to let, but the variable cannot be reassigned:

if(true) {
    const i = 20;
    i = 10; // TypeError: Assignment to constant variable
    const i = 30; // SyntaxError: Identifier 'i' has already been declared
}

console.log(i); // ReferenceError: i is not defined

In conclusion, always use let and const to ensure variables are scoped appropriately and to avoid common errors associated with var and implicit globals. This practice will lead to more predictable and maintainable code.

Objects

JS objects come with a host of interesting and tricky aspects. One of the first big realisations for beginners is that JS is a prototype-based language language. Another surprising feature is the ability to add properties to objects after they are declared.

const cat = {
    name: 'Mavi',
    breed: 'Birman',
    meow: function() {
        return "Meow! I'm " + this.name + " and I'm a " + this.breed + " cat.";
    }
}
cat.eyeColor = 'blue';

While this flexibility is powerful, it can be dangerous if a developer makes incorrect assumptions about the existence of an object property, or simply makes a typo in attempting to reassign a value to an existing property. To safeguard against such issues, you can use Object.freeze:

const aWellBehavedCat = Object.freeze(cat);

Note that adding a property to a frozen object will only throw an error in strict mode. However, freezing an object prevents accidental modification, ensuring the integrity of your data.

Further Exploration

JS has many more fascinating concepts such as hoisting, closures, destructuring and various methods for creating objects. These topics have become essential knowledge for modern JS development. While I’ll save a deeper dive into these for another day, they’re definitely worth exploring on your own.