Up to date
Published
6 min read

Trevor I. Lasn

Staff Software Engineer, Engineering Manager

How JavaScript Was Written Back In the Day

Have you ever been curious how JavaScript was written back in the day? I was, so I dug into some of the early frameworks and libraries to see what I could learn.

I spent the last week diving into code from 2006-2015. I was expecting to cringe at old patterns and appreciate how far we’ve come. Instead, I found myself surprisingly impressed by some of the solutions these early frameworks came up with.

Web development in 2006 meant dealing with browser inconsistencies. Internet Explorer 6 had most of the market share, and it did things differently. Event handling varied between browsers. CSS selector support was limited.

jQuery 1.0 launched on August 26, 2006, with a specific approach to these challenges. John Resig designed it around making DOM manipulation feel natural. The jQuery prototype pattern was pretty clever:

You could write $('#element').addClass('active').fadeIn() and it felt intuitive. The jQuery object behaved like an array but with methods that returned themselves.

jQuery’s approach was to abstract browser differences behind a consistent API. It implemented CSS3 selectors using JavaScript when browsers didn’t support them. AJAX requests worked the same way regardless of the underlying browser implementation.

The approach worked so well that many developers didn’t need to think about browser differences anymore. When browsers eventually standardized, the abstraction layer had done its job.

Here’s another example I found interesting.

The get method handles three completely different scenarios depending on what you pass to it. This kind of API overloading was common in jQuery and made the library feel intuitive.

This approach allowed jQuery objects to have numeric indices $('div')[0] and a length property $('div').length while still being objects that could have methods like addClass() and fadeIn()

[1] When you pass an array: The method treats this as a way to populate the jQuery object with new elements. It sets the length to 0 (clearing any existing elements) and uses [].push.apply(this, num) to copy all elements from the array into the jQuery object.

[2] When you pass nothing: It returns a “clean array” of all elements using jQuery.map(). This gives you access to the actual DOM elements without the jQuery wrapper.

[3] When you pass a number: It returns just that specific element by index, like $('div').get(0) would give you the first div element.

What’s interesting is how jQuery objects needed to feel like arrays without actually being arrays.

In 2006, JavaScript was much more limited. There was no Array.from(), no spread operator ..., and Array.prototype.push() only accepted individual arguments rather than arrays.

The [].push.apply(this, arguments) pattern was a clever workaround that became very common.

By 2010, JavaScript applications were getting more complex. DOM manipulation was scattered throughout codebases.

This code shows several patterns that were common in early 2010s JavaScript. The event system uses manual callback management with this._callbacks as a plain object hash.

The trigger method handles two types of listeners: specific event names and the special “all” event that fires for everything.

Notice the variable declarations at the top: var list, calls, i, l;. This was standard practice before let and const existed. Declaring all variables at the function top avoided confusion about JavaScript’s function-scoped hoisting behavior.

The method uses _.rest(arguments) to slice off the first argument (the event name) before passing the remaining arguments to callbacks. This was necessary because JavaScript didn’t have rest parameters (...args) or destructuring assignment. The arguments object was array-like but not a real array, so you needed utility functions to work with it.

The dual handling of specific events and “all” events is interesting. For specific events like trigger('save'), it only passes the extra arguments. But for the “all” listener, it passes the complete arguments object including the event name, so listeners know which event actually fired.

The list[i].apply(this, ...) pattern was the standard way to call functions with dynamic arguments before the spread operator existed. It sets the this context and applies an array of arguments to the function.

There was no fetch() API until 2015, so developers had to use XMLHttpRequest or ActiveXObject to make AJAX requests.

The || function() pattern provided polyfills before module systems existed. Multiple try/catch blocks were necessary because ActiveX object creation would throw errors if that version wasn’t available.

Before Object.assign() and the spread operator {...obj} existed, you had to manually copy properties from one object to another:

[1] The dst = dst || {} pattern provided fallback values for optional parameters before default parameter syntax: function shallowCopy(src, dst = {}) { }

[2] The for...in loop manually iterates through properties because Object.assign() and the spread operator {...obj} didn’t exist yet.

Before ES6 classes, constructor functions with prototype methods were the standard OOP approach. Object.create(null) created objects without prototype inheritance.

Global variables like Dep.target were used for state tracking before better patterns emerged. The while (i--) loop was a performance optimization for backwards iteration.

These patterns show how creative developers had to be with limited language features. Many of these techniques are no longer necessary, but they solved real problems elegantly within the constraints of ES3 and ES5.


Found this article helpful? You might enjoy my free newsletter. I share dev tips and insights to help you grow your coding skills and advance your tech career.

Interested in supporting this blog in exchange for a shoutout? Get in touch.


Liked this post?

Check out these related articles that might be useful for you. They cover similar topics and provide additional insights.

Javascript
4 min read

Error.isError(): A Better Way to Check Error Types in JavaScript

Why the new Error.isError() method solves important cross-realm issues and provides more reliable error identification than instanceof

May 9, 2025
Read article
Javascript
4 min read

Understanding Bitwise Shifts in JavaScript: << and >>

A practical guide to left and right shift operators in JavaScript

Nov 12, 2024
Read article
Javascript
9 min read

Exploring JavaScript Symbols

Deep dive into JavaScript Symbols - what they are, why they matter, and how to use them effectively

Nov 15, 2024
Read article
Javascript
4 min read

JavaScript compile hints: what they are and when to use them

V8's compile hints let you control which JavaScript gets compiled immediately during page load

May 12, 2025
Read article
Javascript
7 min read

JavaScript Sets and Maps: Beyond Arrays and Objects

How to handle unique values and key-value pairs properly without type coercion and performance issues

Nov 17, 2024
Read article
Javascript
6 min read

setImmediate() vs setTimeout() in JavaScript

both setImmediate() and setTimeout() are used for scheduling tasks, but they work differently.

Sep 8, 2024
Read article
Javascript
4 min read

Intl.DurationFormat: Format Time Durations with Locale Support

Stop writing manual duration formatting code. Instead, leverage the new powerful Intl.DateTimeFormat API for internationalized time displays

Mar 13, 2025
Read article
Javascript
7 min read

JavaScript Truthy and Falsy: A Deep Dive

Grasp JavaScript's type coercion with practical examples and avoid common pitfalls

Oct 27, 2024
Read article
Javascript
3 min read

navigator.clipboard - The New Asynchronous Clipboard API in JavaScript

Copy and paste text, images, and files using the new navigator.clipboard API

Dec 7, 2024
Read article

This article was originally published on https://www.trevorlasn.com/blog/revisiting-legacy-javascript. It was written by a human and polished using grammar tools for clarity.