Coding in the Days Before JavaScript ES6

Revisit the early days of JavaScript before ES6 modernized development. Learn how pioneers tamed callbacks, scopes, and quirks with grit and creativity, laying the foundation for today's JavaScript masters.

Brace yourself for a journey into ancient JavaScript history - back to the days of var and function scopes, jQuery chaining, and callback pyramids. A time before ES6 when developers worked closer to the metal and mastered the raw power of JavaScript's quirks.

Let's fire up our trusty Netscape Navigator and revisit the wild west frontier of early JavaScript development. Though lacking many comforts we now take for granted, this was an era that shaped the JavaScript masters of today.

With grit and creativity, developers of this age tamed browser inconsistencies and crafted experiences that brought sites to life with interactivity. Debugging often meant scrolling through pages of console logs, but each small victory brought new breakthroughs.

While today's tools allow us to build complex SPAs faster than ever, early JavaScript required meticulous planning and problem solving. Without Promises and async/await, juggling async callbacks was an art form. Closures and scopes took time to grasp, and a single missing curly brace could derail everything.

But from these challenges came ingenious hacks and workarounds that paved the way. Developers shared their solutions on blog posts and forums, slowly conquering the quirks of the era. Building an animation that worked cross-browser made you a JavaScript legend.

While ES6 may have civilized JavaScript land, we owe much to the trailblazers of early JS. So as we enjoy the comforts of modern frameworks and composable functions, let us not forget where we came from. For nostalgia still lingers for the raw excitement and challenges of those formative years.

Now lower your Netscape logo hat, and let's journey back to explore the trials and triumphs of...The Age Before ES6!

No Const or Let - Just Var

One of the biggest annoyances was only having the var keyword for declaring variables:

var name = 'John';
var age = 25; 

There was no const or let, so variables declared with var could be redefined or redeclared within the same scope, leading to bugs:

var age = 25;
// do stuff 
var age = 30; // redeclared, so age is now 30

Limited Arrow Functions

Arrow functions made our code so much cleaner in ES6. Before that, anonymous functions were the standard way to pass functions as arguments:

setTimeout(function() {
  console.log('Hello!'); 
}, 1000);

This could get messy with nested callbacks. Arrow functions really cleaned this up:

setTimeout(() => {
  console.log('Hello!');
}, 1000);

No Default Parameters

Initializing function parameters to default values was not supported:

function volume(length, width, height) {
  length = length || 1;
  width = width || 1; 
  height = height || 1;
  
  return length * width * height;
}

We had to use logical ORs to simulate default values. ES6 gave us clean default parameters:

function volume(length = 1, width = 1, height = 1) {
  return length * width * height;
}

Lack of Template Literals

Without template literals, string concatenation and interpolation was messy:

var name = 'John';
var greet = 'Hello, ' + name + '! How are you?';

Template literals in ES6 made this much nicer:

const name = 'John';
const greet = `Hello, ${name}! How are you?`; 

No Built-In Classes

ES6 introduced the class syntax, but before that we had to rely on functions and prototypes to simulate classes:

function Person(name) {
  this.name = name;
}

Person.prototype.walk = function() {
  console.log(`${this.name} is walking!`);
};

const person = new Person('Jane');
person.walk();

This was verbose compared to ES6 classes:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  walk() {
    console.log(`${this.name} is walking!`);
  }
}

const person = new Person('Jane');
person.walk();

Lack of Promises

Callbacks were the common way to handle asynchronous actions before Promises:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error('Script load error'));
  
  document.head.append(script);
}

loadScript('/js/lib.js', (err, script) => {
  if (err) {
    // handle error
  } else {
    // script loaded successfully
  }
});

Promises simplified asynchronous logic flow:

function loadScript(src) {
  return new Promise((resolve, reject) => {
    // load script
    if (/* succeeded */) {
      resolve(script);
    } else {  
      reject(new Error('Script load error'));
    }
  })
}

loadScript('/js/lib.js')
  .then(script => {
    // script loaded successfully
  })
  .catch(err => {
    // handle error
  });

No Destructuring

Extracting values from objects and arrays required more cumbersome code:

// Object
var person = { name: 'John', age: 25 };
var name = person.name; 
var age = person.age;

// Array
var colors = ['red', 'green', 'blue'];
var red = colors[0];
var green = colors[1];

Destructuring simplified this immensely:

// Object 
const { name, age } = person;

// Array
const [red, green] = colors;

Managing Asynchronous Code Flow

Coordinating asynchronous operations was very challenging without Promises and async/await. Callbacks often led to deeply nested code:

function getUser(id, callback) {
  // fetch user from database
  callback(user);
}

function getUserPosts(user, callback) {
  // fetch posts for user
  callback(posts);  
}

function getPostComments(post, callback) {
  // fetch comments for post
  callback(comments);
}

getUser(1, function(user) {
  getUserPosts(user, function(posts) {
    getPostComments(posts[0], function(comments) {
      // finally got the comments!
    });
  }); 
});

Async/await in ES7 enabled linear code flow:

async function getComments() {
  let user = await getUser(1);
  let posts = await getUserPosts(user);
  let comments = await getPostComments(posts[0]);
  return comments;
}

getComments().then(comments => {
  // got comments!
});

Limited Object Capabilities

Object manipulation was more cumbersome before enhancements like object destructuring and shorthand property names:

var firstName = person.firstName;
var lastName = person.lastName;

var name = firstName + ' ' + lastName;

var person2 = {
  firstName: firstName,
  lastName: lastName
};

ES6 introduced nicer ways to work with objects:

const { firstName, lastName } = person; 

const name = `${firstName} ${lastName}`;

const person2 = {
  firstName,
  lastName  
};

No Spread Operator

The spread operator (...) provided a simpler way to copy arrays and objects:

// Before ES6
var arr1 = [1, 2, 3];
var arr2 = arr1.slice(); 

var obj1 = { a: 1, b: 2}; 
var obj2 = Object.assign({}, obj1);

// With Spread Operator
const arr1 = [1, 2, 3];
const arr2 = [...arr1];

const obj1 = { a: 1, b: 2};
const obj2 = {...obj1}; 

Overall, the spread operator enabled cleaner code when working with arrays and objects.

Lack of Flexible importing

ES6 introduced module imports which allowed flexible and encapsulated imports:

// ES6 
import { sum, pi } from './math.js';

Before this, dependencies had to be managed through globals, script tags or CommonJS requires:

// Globals
<script src="math.js"></script> 
sum(1, 2);

// CommonJS
const math = require('./math.js');
math.sum(1, 2);

These approaches polluted the global namespace or required manual dependency management.

No Map/Set

ES6 gave us Map and Set objects which provided better ways to handle key-value pairs and unique values versus plain objects and arrays:

// Map
const map = new Map();
map.set('name', 'John');

// Set 
const set = new Set([1, 2, 3]);
set.has(2);

Prior to ES6, plain objects and arrays were commonly used instead:

// Object as Map
const obj = {};
obj.name = 'John';

// Array as Set
const arr = [1, 2, 3];
arr.indexOf(2) > -1; 

This could result in unintended behavior like exposing properties or array methods.

Handling Unicode

Working with Unicode strings required manual encoding/decoding:

// Convert string to UTF-16 encoded array buffer
const encoder = new TextEncoder();
const uint8Array = encoder.encode('Hello world!'); 

// Convert array buffer back to string
const decoder = new TextDecoder();
const str = decoder.decode(uint8Array);

String methods like codePointAt() and fromCodePoint() in ES6 provided better native Unicode support.

Here's some additional content contrasting coding in pre-ES6 JavaScript vs ES6+:

Iterating Over Collections

Looping over arrays and objects was very common in JavaScript code. Pre-ES6, we relied on basic for and forEach loops:

const arr = [1, 2, 3];

// for loop
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

// forEach
arr.forEach(num => {
  console.log(num);  
});

ES6 introduced more options like for-of loops and array methods such as map(), filter() and reduce() for iterating:

// for-of
for (let num of arr) {
  console.log(num); 
}

// map 
const squared = arr.map(num => num * num); 

// filter
const evens = arr.filter(num => num % 2 === 0);

// reduce 
const sum = arr.reduce((prev, curr) => prev + curr);

This enabled more declarative and functional programming styles.

Handling Errors

Pre-ES6, error handling was done through basic try/catch:

try {
  // do something that might throw
  throw new Error('Oops!');
} catch (error) {
  console.log(error.message);
}

ES6 introduced Promise.catch() for cleaner promise-based error handling:

doSomething()
  .then(result => {
    // handle result
  })
  .catch(error => {
    console.log(error.message);
  });

This enabled promise chains with linear error handling.

Declaring Classes

ES6 classes provided nicer syntactic sugar over constructor functions and prototypes:

// ES6
class Person {
  constructor(name) {
    this.name = name; 
  }
  
  introduce() {
    console.log(`Hi, I'm ${this.name}!`)
  }
}

// Pre-ES6
function Person(name) {
  this.name = name;
}

Person.prototype.introduce = function() {
  console.log(`Hi, I'm ${this.name}!`);
}

Classes help organize code into cleaner and more readable structures.

Mastering the Old Ways

The era of pre-ES6 JavaScript was a time of grit and ingenuity. Faced with the limitations of the time, developers forged clever workarounds that became the foundation of JavaScript mastery. By revisiting some key lessons from that formative age, we can enrich our understanding and appreciation of modern JavaScript.

Function Scope Mastery

Before ES6 let and const, variable scoping tripped up many beginners. The only option was good ol' var and its function-level scope:

function doStuff() {
  var name = 'Yoda';
  
  if (true) {
    var age = 900; 
  }
  
  console.log(name, age); // 'Yoda', 900
}

Mastering function scope and closures was crucial:

function JediTrainer() {
  var padawans = [];
  
  return function train(name) {
    padawans.push(name);
    console.log(padawans);
  }
}

var train = JediTrainer();
train('Luke'); // ['Luke'] 

This knowledge paved the way for modern scope mastery.

Callback Juggling

Asynchronous callbacks required careful orchestration:

getData(function(err, data) {
  parseData(data, function(err, parsed) {
    processData(parsed, function(err, result) {
      displayData(result);
    });
  });
});

Mastering these "pyramid of doom" structures trained coordination skills.

Cross-Browser Quirks

Browser inconsistencies abounded. A single line of CSS could appear drastically different across browsers. JavaScript also required per-browser tweaks:

// Cross-browser event binding
var event = event || window.event; 
var target = event.target || event.srcElement;

Dealing with these quirks honed debugging skills and resilience.

The Art of Minification

In the early days, minimizing file sizes was crucial for web performance. Complex minification techniques were developed to obfuscate code:

// Before
function doSomething(x) {
  return x + 100;
}

// After
function d(x){return x+100}

Variables were reduced to single letters, whitespace removed, and code folded into single lines. This advanced skill aided performance.

Flexibility of Loose Typing

JavaScript's loose typing allowed creative manipulation of values:

// Loose typing
let x = 1;
x = 'Hello'; // Allowed!

// Strict checks
x === 1; // false
typeof x; // 'string'

Leveraging this flexibility powered innovative techniques, even if it introduced bugs.

The Power of Immediate Functions

Immediately invoked function expressions (IIFEs) encapsulated logic:

// IIFE
(function() {
  // logic here
  
  var private = 'secret'; 
})();

console.log(private); // ReferenceError 

This pattern brought organization before modules formalized it.

Bottom Line

As we conclude our journey through early JavaScript history, one thing is clear - a great mastery was forged in those formative days. Early JavaScript was unrefined, but beneath its rough edges lay immense potential for those bold enough to harness it.

The true measure of a JavaScript developer is not what tools they use, but their ability to adapt and problem-solve. Limitations should inspire creativity, not discouragement. Every roadblock is a chance to learn.

While frameworks and standards have evolved, the core spirit that drove JavaScript pioneers remains unchanged. A drive to push limits. An appetite to solve puzzles. A call to build something amazing from simple code.

That raw essence still lives on today in JavaScript’s endless possibilities. So code boldly, debug relentlessly, and push boundaries. Stand on the shoulders of giants, then reach higher.

The era of early JavaScript pioneers has passed, giving way to a new generation. One that carries forward their creativity, grit, and passion to mold JavaScript into whatever they dare to dream. The future masters are out there right now, pushing limits and redefining what’s possible.

So code on young padawans! Though tools and tricks may change, the spirit of mastery endures. Make those JavaScript pioneers of yesterday proud!

Subscribe to JS Dev Journal

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe