MarcusPope.com


Blog » 2013 » Rethinking JavaScript: Changing Course From ES5

There's a fine line between being curmudgeonly and minimalistic when it comes to expanding the feature set of any language. Part of me wants to see JavaScript mature, but most of me thinks ES5 is the wrong path for a language like JavaScript to evolve, particularly by comparison to existing language problems and the opportunity costs for alternative features. Growing older doesn't make it any easier to ignore that my disdain could very well be because I'm getting older so here's a short prose to warrant my opinion.

With 18 years of programming experience I've had plenty of time to tinker with a variety of languages. For me JavaScript is the right combination of simplicity, terseness and flexibility. With first class functions, unrelentingly mutable namespaces, and an object notation without the overhead and complexity of approaches like XML, you rarely get backed into a corner with a bad architecture in JavaScript. And even though you could argue that we end up with a greater quantity of bad architectures in the wild due to this inherent flexibility, in my experience all of the effort to prevent this problem in other stricter languages still results in a vast ocean of bad code still. The difference in my opinion is that non-mutable languages invariably reach a point where a rewrite is necessary to fix inherent design flaws, in mutable languages you can usually monkey patch your way out of any problem.

To me, the intrinsic ability to subclass host objects, native methods, and the prototypes of base scalars and objects is what makes JavaScript the impervious language capable of consistently driving every platform from web browsers, to operating systems and the embedded devices in between. Sometimes it's tough, and it can look ugly, but it can be done across a heterogeneous landscape of hosts and implementations, even when those hosts don't play well together like our existing browser market.

To keep this reigning title the language needs to be that flexible, not just for the sake of platforms but for its community as well. The fact that enterprise applications are being written in a language with such a small syntactical footprint is a testament to the design. And although functional scoping can be difficult for junior developers to grasp, the barriers to entry are not at all like C or even its modern cousin C#. So when a language begins to evolve in an antithetical direction to those tenants of success, it means the features I appreciate so much about the language may soon be no longer available.

Part I: JavaScript is not, and should be anything but, “strict”;

My first gripe for ES5 is the “use strict”; construct. This is probably the most significant change to the language in ES5. Here are a handful of examples that are no longer viable options for developers when using strict mode.

1. Preventing code eval's programmatically*:

In the JavaScript community eval has held a special status of evil. Many consider it to be as verboten as Object.prototype extensions, but that's a different topic altogether. Regardless of opinions, the easiest way to secure your library or codebase from eval misuse was as easy as this:

eval = Function = null;

But that is no longer an option with strict mode and you will receive a compile time error. * There are still ways around this problem via the use of createElement/iFrames/Script Tags etc, however even those methods can be subclassed as well to really lock down a JS environment from XSS injection.

2. Programmatically inspecting evaluated code:

If the above option seems too heavy handed, there was always the option of just keeping a record of what code is being evaluated using this snippet:

eval = (function(evil) {

    return function() {

        console.log(arguments); //some sort of historical tracking

        evil.apply(this, arguments);

    };

})(eval);

With this code you can ensure that any code evaluated after this statement will be logged to the console (or whatever mechanism you wish) and you can visually inspect or programmatically inspect the code. You can even conditionally evaluate the code based on any number of inspection methods. If the code doesn't match a sha1 hash of previously eval'ed snippets as tracked in your sandbox environment then don't run it.

3. Obtaining references to anonymous function definitions programmatically:

setTimeout(function() {

    if (notReady) setTimeout(arguments.callee, 1000);

    ...

}, 1000);

Since strict mode prevents access to arguments.callee, we now have to use a named function reference instead of an anonymous function like so:

setTimeout(function checker() {

    if (notReady) setTimeout(checker, 1000);

}, 1000);

Although this may seem like the ideal approach because it's much more obvious and has a terser syntax, we cannot reliably implement this construct in IE<9. IE<9 will leak “checker” into its parent scope even if that means creating a global method. Admittedly this will no longer be a problem in the distant future when IE8 sunsets. But there are several other concerns beside leaking into a parent scope to contend with until then.

And terseness is achievable via a simple closure:

var closure = function() {

   return arguments.callee.caller;

};

So now we can do this:

setTimeout(function() {

    if (notReady) setTimeout(closure(), 1000);

});

Sure we could implement this logic via other approaches, but the point is that strict mode limits this useful option which is cross browser compatible. And instead of abstracting the “closure()” function universally, we now have to create a new variable for every functional scope we wish to reference, and mentally grok the code each time we see it. This degrades the status of first class functions. And is as superfluous as requiring the declaration of a counter/index variable to do foreach iteration on array values.

4. Stack trace inspection

No longer can we do the following to obtain a stack trace on environments that do not natively/non-standardly support stack traces otherwise:

Function getStackTrace() {

    var stack = arguments.callee;

    while (stack = stack.caller) {

        console.log(stack.name || anonymous);

    }
}

Unless your host implements a native stack trace inspector, you are out of luck. Which ultimately means we're out of luck for any cross browser approach.

5. with statements

Although they have a sordid past, with statements are a key substitute for let expressions in older browsers. They also work well for creating embedded scripting hosts where access to special utility functions is available “globally” without actually modifying the global scope. Various projects use this mechanism including Firebug. There is no other way to obtain a closure without affecting the scope of this in JavaScript.

Sadly ES5 could have fixed the major complaint against with by making assignment expressions always operate on the with-scoped object, but it's a mindset issue not a technical issue and so the technical solution was ignored (and there are no good solutions to opinionated mindsets.)

If anything, “use strict”; should have been “use perf”;. Because the only argument against features like arguments.caller is performance. It's not a great argument mind you because the performance loss is paltry, and if all browsers optimized the problem in the same manner we would see only 2x performance loss to the thousandth-of-a-millisecond call to arguments.callee. Frankly there are far better optimizations to be made in the browser, the dom, and end-developer logic than .001 milliseconds. And the practice of not using the arguments object already optimizes this performance loss in some browsers so there's no need to create a “use strict”; mode when opting into the performance gain is easily achievable by the end-developer's coding practices.

Part II: JavaScript Developer Namespace Pollution.

Ultimately I base my position on a philosophical stance: The syntax and host extensions of any language should only be modified when there are technical hurdles to doing so natively via code and when the modification produces a net win for the language.

These hurdles could be for accessibility in the sense that a camera is only accessible if the JS host provides an interface to it. Feasibility – implementing a construct like introspectable-scopes is just not currently feasible with JavaScript today. Or performance – Array.prototype.sort() is orders of magnitude more efficient than any implementation written in pure JS. Otherwise they should be left to the domain of 3rd party libraries to implement, solidify and maintain for perpetuity (this includes you Function.prototype.bind!)

Adding to the namespace for any other reason (that I could think of anyway) is pollution from my perspective. That's developer land, not JavaScript reserved word land. If JavaScript is to be extended for reasons not germane to reserved words/syntax, then it should be placed under the navigator object or some other universally decided top level namespace, like our little friend localStorage.

One of the main reasons ES5 syntax wasn't added as reserved words was due to the difficulty of implementing such features with backwards compatibility and the failed adoption of such features when only experimental hosts implement them (ahem ES4.) And this, in my opinion, is a good consequence because it forces technical committees to really want a feature before deciding to integrate it. And it means we don't end up with crappier versions of functionality that already exist today.

Enter Stage Left: Array.prototype[forEach map every some filter reduce reduceRight]

Not only are these implementations 100% solvable in raw JavaScript code, but most libraries already implement them in one form or another. And frankly the ES5 versions are poor substitutes for some existing implementations if you ask me. Had forEach been designed as a keyword, map, every, some and filter would have been completely unnecessary because you'd be able to use break and continue statements with even greater flexibility. Object.keys() wouldn't be necessary either because forEach could and should be applicable to Objects, since the purpose of forEach is to iterate over locally defined keys without the confusion of typical for/for..in syntax. And had forEach been implemented on Object.prototype instead, we could have ended the pitiful complaints of “never use for..in on arrays” and “never use for..in on objects without hasOwnProperty() checks.”

Instead, because these functions were implemented in the developer namespace, the implementation differences between existing functions and these uniquely blessed ES5 versions fall on the developer's plate to sort out. And because they were implemented on the Array.prototype no less, we are left without a solution to for..in loops on objects that do not rely on the almost-always-forgotten or intentionally-left-out hasOwnProperty calls.

It's as if the creation of Object.keys() was only really designed so we could do this:

Object.keys(someObj).forEach(func(key) {

    var val = someObj[key];

    handle(val);

});

And frankly that looks like it was designed by someone who didn't understand JavaScript's prototypal inheritance very well...

someObj.forEach(handle);

This not only makes your forEach function handler more cleanly isolated from the iteration object, which is critical for using function references defined outside the current scope of someObj, but it's optimized for value iteration instead of requiring key lookup operations on every index!

Function.prototype.bind():

Had the .bind() implementation been left in JS land, the previous solutions would remain consistent across browsers. Now however, we cannot create an ES5 compatible version in ES3. You can get close, but not 100%. Fortunately we can override the .bind() implementation in ES5 and ES3, and artificially create a 100% consistent environment, a prime example of the flexibility of the language.

Array.isArray():

Was this too difficult?

Array.prototype.isArray = function() { return true; }

Array.isArray = function(a) {

    return a && a.isArray && typeof a.isArray == 'function' && a.isArray();

};

Or this?

Array.isArray = function(a) {

    return Object.prototype.toString(a) == "[object Array]";

};

And why limit to just one argument? Everyone complains when IE only allows one argument for 'alert()' and when Chrome & Safari only allowed for one argument to console.log, yet if the language itself restricts it, we seemingly don't care?

Part III: Opportunity cost for features we could really use:

1. Obtaining a reference to the calling scope when executing prototype extensions:

Historically, the keyword “this” refers to the caller's context when used inside of a function scope.

var t = function() { console.log(typeof this); };

t(); //outputs “[object global]” because it is called from the global context

t.bind({})(); //outputs “[object Object]” because it was bound to an object before being called

But if you needed to call another function inside t() with the same context as was used to call t() itself, you had to do this:

var t = function() {

    if (oldbrowser) hack.apply(this, arguments); //keeps the context

    ...

}

Because otherwise, just calling hack() would change the calling context back to global. You would also have to specify a “this” if you were relying on a variable length of arguments, but that's a design pattern that could easily be solved with a simple addition to the arguments object which I'll discuss later.

//browser host

alert = (function(native) {

    if (!window.silent) return native.apply(this, arguments);

}(alert);

But all of that changes when you are writing prototype extensions! Because when used in the following manner, “this” no longer refers to the calling scope but instead the actual object of the prototype chain.

Function.prototype.t = function() {

    this; //no longer refers to scope, it now refers to whatever function object invoked t()

    //It would be nice if we could parlay the current calling scope along to the call to hack()
    if (oldbrowser) return hack.apply(arguments.scope, arguments);

};

console.log.t.bind({})();

As it stands we have to explicitly pass the scope into a prototype function and then splice off the first or last param.

Function.prototype.t = function(scope) {

    var scope = arguments[0];

    var args = Array.prototype.slice.apply(arguments, 1);

    if (oldbrowser) return hack.apply(scope, args);

};

And now every call to .t() requires an explicit scope argument, making the consumption of this interface linearly more complex than necessary.

The problem with the direction is that strict mode is intended to be universally switched on at some point (ES6?) Frankly I'd rather have “use loose”; instead where the dynamicism of the language is pushed even further.

1. Object.__noSuchMethod__

2. Undefined.prototype, Null.prototype

I would love it if null's and undefined's were treated exactly as they are today with the exception that methods/properties called on null and undefined objects could be caught via prototype extensions. I consider this dependency injection for prototypal functions.

Null.prototype.toString = function() {
    return "";
};

var x = null;

//Now these two constructs would behave identically
var y = String.prototype.toString(x);
var z = x.toString(); //and clearly this is easier to read, type, maintain, etc

//Instead we always have to check for nulls, or risk throwing an exception
y = x == null ? "" : x.toString();

3. Option to never throw exceptions

This one is definitely controversial, but jQuery paved the way with the premise:

//assume .not_found is not a class in the dom:
$('.not_found').chain().still().works(); //the rest is a noop

3. arguments.scope / arguments.callee.caller.scope

ES5 is a step away from a language that really changed my perspective on what is “right” or “required” in programming. Very few things, like JavaScript, have had such a profound effect on me. Unfortunately ES6 is being planned with even more unnecessary logic, syntax and restrictions. And I think the day classes make their way into JavaScript is the day that I have to give up on the language altogether. Fortunately, the committee responsible for ES6 and the future of JavaScript moves slowly enough that I won't have to worry about it for a while. And even when/if they do pass ES6, there will still be plenty of time before the industry implements the specification. And even then, there will be some lengthy period of time where backwards compatibility will still require ES5 coding standards - which I can live with, even if curmudgeonly. But finally, if that day does come where my favorite language is only available to me in unsupported deprecated versions of node.js, there will still have been enough time for me to polish up my C skills and fork the project for my own selfish interests.

Now please excuse me while I trail off ranting about how things used to be in my day...