A Memo for ECMAScript 5 - The Good, Bad, and Awful Parts

2015/12/048 min read
bookmark this
Responsive image

Table of Contents

  1. Introduction
  2. Recommended Reading
  3. The Bad Parts of JavaScript
  4. Understanding the this Keyword
  5. Scope and Hoisting
  6. Common JavaScript Pitfalls
  7. Type Coercion and Falsy Values
  8. Objects and Prototypes
  9. Functions: Statements vs Expressions
  10. Best Practices

Introduction

ECMAScript 5 (ES5) introduced many improvements to JavaScript, but the language still retains some problematic features from its earlier versions. Understanding these quirks is essential for writing robust, maintainable code.

This memo covers the "bad" and "awful" parts of JavaScript as identified by Douglas Crockford, along with practical guidance on how to avoid common pitfalls.

Recommended Reading

Essential resources for understanding JavaScript deeply:

The Bad Parts of JavaScript

These features should be avoided or used with extreme caution:

with Statement

The with statement was intended to provide shorthand for accessing object properties but creates ambiguity and performance issues.

// Avoid this
with (obj) {
  a = b; // Is 'a' a property of obj or a global variable?
}

// Do this instead
obj.a = obj.b;

eval() Function

eval() executes arbitrary strings as code, creating security vulnerabilities and performance problems.

// Never do this
eval('var x = ' + userInput);

// Use JSON.parse for data
var data = JSON.parse(jsonString);

void Operator

The void operator is confusing and rarely needed. It evaluates an expression and returns undefined.

// Confusing
void 0; // returns undefined

// Clearer alternative
undefined;

Type Coercion

JavaScript's implicit type conversion leads to unexpected behavior:

// Surprising results
'5' + 3; // "53" (string concatenation)
'5' - 3; // 2 (numeric subtraction)
[] + {}; // "[object Object]"
{
}
+[]; // 0

// Use explicit conversion
Number('5') + 3; // 8
String(5) + '3'; // "53"

continue and switch Fall-through

Both can create confusing control flow:

// Always use break in switch
switch (value) {
  case 1:
    doSomething();
    break; // Don't forget this!
  case 2:
    doSomethingElse();
    break;
  default:
    doDefault();
}

Understanding the this Keyword

The value of this in JavaScript depends on how a function is called. This is one of the most confusing aspects of the language.

Method Invocation

When called as an object method, this refers to the object:

var obj = {
  name: 'Object',
  getName: function () {
    return this.name; // 'this' is obj
  },
};

obj.getName(); // "Object"

Using call() and apply()

Explicitly set this using call() or apply():

function greet(greeting) {
  return greeting + ', ' + this.name;
}

var person = { name: 'Alice' };

// Using call (arguments listed)
greet.call(person, 'Hello'); // "Hello, Alice"

// Using apply (arguments as array)
greet.apply(person, ['Hi']); // "Hi, Alice"

Constructor Invocation with new

When using new, this refers to the newly created object:

function Person(name) {
  this.name = name; // 'this' is the new instance
}

var alice = new Person('Alice');
console.log(alice.name); // "Alice"

Arrow Functions (ES6+)

Arrow functions inherit this from their enclosing scope:

var obj = {
  name: 'Object',
  delayedGreet: function () {
    // Arrow function preserves 'this'
    setTimeout(() => {
      console.log(this.name); // "Object"
    }, 100);
  },
};

Scope and Hoisting

Variable Hoisting

Variable declarations are "hoisted" to the top of their scope:

function example() {
  console.log(x); // undefined (not ReferenceError)
  var x = 5;
  console.log(x); // 5
}

// Equivalent to:
function example() {
  var x; // Declaration hoisted
  console.log(x); // undefined
  x = 5; // Assignment stays in place
  console.log(x); // 5
}

Function Hoisting

Function declarations are fully hoisted:

sayHello(); // Works! "Hello"

function sayHello() {
  console.log('Hello');
}

// But function expressions are NOT hoisted
sayGoodbye(); // TypeError: sayGoodbye is not a function

var sayGoodbye = function () {
  console.log('Goodbye');
};

Common JavaScript Pitfalls

Global Variables

Undeclared variables become global:

function badFunction() {
  x = 10; // Creates global variable!
}

// Always use var, let, or const
function goodFunction() {
  var x = 10; // Local variable
}

Semicolon Insertion

JavaScript automatically inserts semicolons, sometimes incorrectly:

// This returns undefined, not the object!
function getObject() {
  return;
  {
    name: 'value';
  }
}

// Correct way
function getObject() {
  return {
    name: 'value',
  };
}

Reserved Words

Some words cannot be used as identifiers:

// These will cause errors
var class = "CSS";   // Error
var return = 5;      // Error

// Object properties are safer but still problematic
var obj = {
  class: "CSS"       // Works in ES5+
};

The typeof Operator

typeof has some quirks:

typeof []; // "object" (not "array"!)
typeof null; // "object" (historical bug)
typeof function () {}; // "function"
typeof undefined; // "undefined"

// Use Array.isArray() for arrays
Array.isArray([]); // true

parseInt() Issues

Always specify the radix:

parseInt('08'); // 0 in older browsers (octal)
parseInt('08', 10); // 8 (always use radix!)

Floating Point Precision

JavaScript uses IEEE 754 floating point:

0.1 + 0.2; // 0.30000000000000004 (not 0.3!)

// For currency, use integers (cents)
var cents = 10 + 20; // 30
var dollars = cents / 100; // 0.30

NaN (Not a Number)

NaN is not equal to itself:

NaN === NaN; // false!

// Use isNaN() to check
isNaN(NaN); // true
Number.isNaN(NaN); // true (ES6, more reliable)

The + Operator

The + operator is overloaded for addition and concatenation:

5 + 3; // 8 (addition)
'5' + 3; // "53" (concatenation)
5 + '3'; // "53" (concatenation)

// Be explicit
5 + Number('3'); // 8
String(5) + '3'; // "53"

Type Coercion and Falsy Values

Falsy Values in JavaScript

These values evaluate to false in boolean context:

false
0
"" (empty string)
null
undefined
NaN

Truthy Gotchas

Some values are truthy but might surprise you:

Boolean([]); // true (empty array)
Boolean({}); // true (empty object)
Boolean('0'); // true (non-empty string)
Boolean('false'); // true (non-empty string)

Using hasOwnProperty

Always use hasOwnProperty when iterating:

var obj = { a: 1, b: 2 };

for (var key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key, obj[key]);
  }
}

Objects and Prototypes

Prototype Inheritance

JavaScript uses prototypal inheritance:

var animal = {
  speak: function () {
    console.log('Sound');
  },
};

var dog = Object.create(animal);
dog.speak = function () {
  console.log('Bark');
};

dog.speak(); // "Bark"

Object.create() for Inheritance

Prefer Object.create() over constructor patterns:

// Using Object.create (cleaner)
var parent = {
  greet: function () {
    return 'Hello';
  },
};

var child = Object.create(parent);
child.greet(); // "Hello"

Functions: Statements vs Expressions

Function Declaration (Statement)

// Hoisted - can be called before declaration
sayHello();

function sayHello() {
  console.log('Hello');
}

Function Expression

// NOT hoisted - must be defined before use
var sayHello = function () {
  console.log('Hello');
};

sayHello();

Named Function Expression

// Useful for recursion and debugging
var factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);
};

Best Practices

Avoid These Features

Feature Why Avoid Alternative
with Ambiguous scope Direct property access
eval() Security risk JSON.parse()
== Type coercion === (strict equality)
++/-- Can be confusing += 1 or -= 1
Bitwise operators Confusing in JS Explicit operations
Typed wrappers (new String()) Creates objects, not primitives Use literals

Always Do These

  1. Use strict mode: "use strict";
  2. Declare variables: Always use var, let, or const
  3. Use ===: Avoid loose equality
  4. Specify radix: parseInt(str, 10)
  5. Use semicolons: Don't rely on automatic insertion
  6. Use hasOwnProperty: When iterating with for-in

Code Example: Modern ES5 Pattern

'use strict';

var MyModule = (function () {
  // Private variables
  var privateVar = 'secret';

  // Private function
  function privateFunction() {
    return privateVar;
  }

  // Public API
  return {
    publicMethod: function () {
      return privateFunction();
    },
  };
})();

MyModule.publicMethod(); // "secret"

Conclusion

ECMAScript 5 is a capable language, but understanding its quirks is essential for writing reliable code. Key takeaways:

  1. Avoid the "bad parts": with, eval, loose equality
  2. Understand this: Know how function invocation affects this
  3. Be explicit: Use strict equality, specify radix, declare variables
  4. Use strict mode: Catches common mistakes early
  5. Learn prototypes: Understand JavaScript's inheritance model

These practices will help you write cleaner, more maintainable JavaScript code.