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:
jQuery.fn = jQuery.prototype = { addClass: function(className) { // Add class logic here return this; // This enables chaining },
fadeIn: function(speed) { // Fade in animation logic here return this; // This enables chaining }};
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.
// Map the jQuery namespace to the '$' onevar $ = jQuery;
jQuery.fn = jQuery.prototype = { get: function( num ) { // Watch for when an array (of elements) is passed in if ( num && num.constructor == Array ) {
// Use a tricky hack to make the jQuery object // look and feel like an array this.length = 0; [].push.apply( this, num );
return this; } else return num == undefined ?
// Return a 'clean' array jQuery.map( this, function(a){ return a } ) :
// Return just the object this[num]; }};
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.
Backbone.Events = { // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { var list, calls, i, l; var calls = this._callbacks; if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, _.rest(arguments)); } } if (list = calls['all']) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, arguments); } } return this; }}
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.
var XHR = window.XMLHttpRequest || function() { try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} throw new Error("This browser does not support XMLHttpRequest.");};
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:
/** * Create a shallow copy of an object */function shallowCopy(src, dst) { dst = dst || {};
for(var key in src) { if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { dst[key] = src[key]; } }
return dst;}
[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.
Watcher.prototype.beforeGet = function () { Dep.target = this this.newDeps = Object.create(null)}
Watcher.prototype.afterGet = function () { Dep.target = null var ids = Object.keys(this.deps) var i = ids.length while (i--) { var id = ids[i] if (!this.newDeps[id]) { this.deps[id].removeSub(this) } } this.deps = this.newDeps}
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.