If you've been around as long as I have, you've probably noticed something remarkable about JavaScript (as compared to other languages). It's evolving at a breakneck pace.
In general, this is a very good thing (although it can make it quite challenging to keep your skills current). And if you're part of the "new generation" of coders - many of whom only code in JavaScript - this might not even strike you as extraordinary. But as someone who's watched numerous languages evolve over the better part of three decades, let me assure you that, in this category, JavaScript is in a class of its own.
History
Most "modern" languages are maintained (in the worst-case scenario) by a single company. Or they're maintained (in the best-case scenario) by a vast, (virtually) nameless, open-source consortium.
Single-company stewardship isn't necessarily "bad". On one hand, it can allow the (small) braintrust of stewards to make quick-and-decisive corrections wherever the language is deemed to be "lacking". On the other hand, it can lead to stagnation if the company's braintrust doesn't favor Improvement X, even in the face of a development community that may be clamoring for Improvement X. It can also cause serious headaches if the parent company has objectives that clash with the broader community - or if they ever decide to abandon the project altogether.
Open-source projects are generally assumed to be "better". But even they have their downsides. Open-source projects are often plagued with "groupthink" and "analysis paralysis". If you think it's difficult to get a single room of people in your company to agree on anything, try getting a new feature proposal approved in a well-established open-source project.
You can submit a proposal saying that, "Bugs are bad." And you can almost be assured that someone in the open-source community (probably, a well-established and respected senior voice in the community) will chime in and say, "Ummm... No, they're not. And we definitely don't need a removeBugs()
function in the core language."
JavaScript's Accidental Solution
If you're part of the latest-generation of JavaScript devs, you can be forgiven if you think that the language has always evolved at its current pace. After all, the 2010s have seen an impressive array of new features and syntactic shortcuts added to the language. But let me assure you, it hasn't always been so.
The early days of JavaScript were strikingly similar to other trying-to-get-a-foothold languages. The ECMAScript committee had good intentions - but change was slowwwww. (Like it is for nearly any decision-by-large-committee process.)
If you don't believe me, just look at what happened (or... didn't happen) with the ECMAScript standard for nearly 10 years, starting in the early 00s. We went many years without having any substantive improvements to the language. And for most of those years, JavaScript was much more of a "toy" than a serious programming language.
This was, perhaps, best illustrated by TypeScript. TypeScript wasn't supposed to be a separate "language". It was supposed to be a major enhancement to core JavaScript - an enhancement that was heralded by none other than: Microsoft.
But through a series of last-minute backtracking decisions that would require multiple blog posts to explain, MS's TypeScript modifications ended up being rejected. This eventually led to MS releasing TypeScript as its own, separate, open-source project. It also led to years of stagnation in JavaScript.
We might still be wallowing in that general malaise if it weren't for the introduction of several key technologies. I'm talking about:
- Node.JS
- Node Package Manager (NPM)
- Babel
[NOTE: If you're part of the adoring Yarn crowd, this isn't meant to sidestep you in any way. Yarn's wonderful. It's great. But I firmly believe that, with respect to the evolution that I'm trying to outline in this article, Node/NPM/Babel were the "sparks" that drove this initial advancement.]
Sidestepping Committees
There's nothing "magical" about JavaScript. Like any other language, it has its strength (and its flaws). And like any other language, if a broad internet consortium needs to reach consensus about the language's newest features, we could well be waiting a decade-or-more for such improvements.
But a funny thing happened on the way to the endless open-source release-candidate debates. Node spurred a dynamic package model called NPM. (Which has had its own share of growing pains - but that's for another article...) And NPM spurred a fantastical, automagical package called Babel.
For the first time, Babel gave the burgeoning JavaScript community an incredible ability to evolve the language on their own. Babel created a vast, real-world "proving ground" for advancements in the JavaScript language. If you look at the major advancements in the ECMAScript spec over the last 10+ years, you'd be hard-pressed to find any improvements that weren't first encapsulated in NPM packages, that were then transpiled to backward-compatible JavaScript in Babel, before they were eventually absorbed into the core language itself.
To put this another way, Babel freed JS devs from constantly having to code for the "lowest common denominator". If there was an exciting new (proposed) language construct that wasn't recognized by older browsers - but Babel could transpile it down to older-browser-compliant code, then you were free to code away with the cool new feature. And if your users were using ancient browsers, that was OK - because Babel would basically take care of the messy new-to-old translations.
"Innovations" Are Rarely... Inventions
Maybe you're thinking that an NPM package is not an innovation in the language itself. And many times, I would agree with you. But when something becomes sufficiently-useful to a huge portion of the programming ecosystem, it can, in fact, become an innovation in the underlying language.
Let's imagine that, in your JavaScript applications, you repeatedly have the need to makeToast()
. Of course, JavaScript has no native makeToast()
functionality - so you've coded up some grand, extensive, convoluted utility function that will allow you to makeToast()
right in the middle of any application where you feel the need to, well, you know... make toast.
After a while, you find yourself copying-and-pasting this amazing utility into all of your applications. Eventually, you feel a bit of developer guilt over this repeated copying-and-pasting, so you encapsulate your awesome makeToast()
functionality into an NPM package. The NPM package allows you to do this:
import toast from 'make-toast';
const noToastHere = 'plain ol bread';
const itsToasted = toast.make(noToastHere);
There's a good chance that your incredible toast.make()
function utilizes a ton of non-ECMA-standard language constructs. But that's OK. Because all of your non-ECMA-standard language constructs are just a pile of syntactic sugar for things that you could always do in JavaScript - but with a lot more hellacious-looking code. And when you run your revolutionary new toast.make()
function through Babel, it transpiles it back down into that old, ugly, IE7-compliant JavaScript that you never wanna have to type out manually.
You see, there's a good chance that your amazing toast.make()
functionality isn't doing anything that you couldn't always, theoretically, do with old-skool JavaScript. toast.make()
presumably just gives you a faster, sleeker, more-efficient way to make toast, rather than relying on every dev team, in every codebase, having to manually figure out how to make their own toast from scratch.
To be clear, such "advancements" aren't just about semantics. They absolutely are advancements. If we always had to do this:
export default function makeToast() {
// here are my 200 lines of custom, painfully crafted,
// cross-browser-compliant code that allows me to makeToast()
}
And now we can just do this:
import toast from 'make-toast';
const noToastHere = 'plain ol bread';
const itsToasted = toast.make(noToastHere);
And, if many thousands of developers all around the world find themselves repeatedly having to either A) import your make-toast
package, or B) figure out a way to manually craft the functionality from scratch, then your amazing toast.make()
feature is a potentially-significant advancement in the language. More important, if your toast.make()
feature becomes so ubiquitous that it's more-or-less "standard" in modern codebases, there's a chance that the ECMAScript committee might actually decide to promote it to the level of being a language construct.
(Granted, they may not choose to implement it in the exact same way that you did in your NPM package. But the point is that they might eventually look at what's happening in the broader JS community, realize that vast numbers of codebases now see a need to make toast, and find a way to incorporate this as a base feature in the core language itself.)
Lodash & Underscore
To see this in action, look at many of the core functions that are available in the Underscore or Lodash libraries. When those libraries first rose to prominence, they provided a ton of functionality that you just couldn't do in JavaScript without manually coding out all the functions yourself.
Nowadays, those libraries still offer some useful functionality that simply doesn't exist in core JavaScript. But many of their features have actually been adopted into the core language. A good example of this is the Array.prototype
functions.
One of my pet peeves is when I see a dev import Lodash so they can loop through an array. When Lodash was first introduced, there was no one-liner available in JavaScript that did that. Now... we have Array.prototype
functions.
But that's not a knock on Lodash. Lodash, Underscore, and other similar libraries did their jobs so well, and become so ubiquitous, that some of their core features ended up being adopted into the language itself. And this all happened in a relatively short period of time (by the standard of typical language evolution).
Innovation vs Stagnation
If you think that JavaScript's recent barrage of advancements is "normal" for a programming language, let me assure you: It's not. I could probably come up with 50 sad examples of language stagnation, but let me give you one very-specific scenario where one language (JavaScript) shines and another (Java) has crawled into a corner to hide.
In JavaScript, I can now do this:
import SlidingSwitch from '@material-ui/core/Switch';
import RouterSwitch from 'react-router-dom/Switch';
import CustomSwitch from './common/form-elements/Switch';
export default function Foo() {
return (
<>
<RouterSwitch>
<Route path={'/path1'} component={Path1Component}/>
<Route path={'/path2'} component={Path2Component}/>
</RouterSwitch>
<div>Here is my SlidingSwitch <SlidingSwitch/></div>
<div>Here is my CustomSwitch <CustomSwitch/></div>
</>
);
}
There's nothing too rocket-sciencey going on here. I'm importing three different components into my code. It just so happens that all three of them were originally written with the same name. But that's OK. JavaScript gives us an easy way to deal with the naming collisions by aliasing them at the point where they're imported.
This doesn't make JavaScript unique or special. Other languages have import aliasing features. But it's worthwhile to note that, a decade-or-more ago, you couldn't do this (natively) in JavaScript.
So how do we handle such naming collisions in Java??? It would look something like this:
import material.ui.core.Switch;
import react.router.dom.Switch;
import com.companydomain.common.utils.Switch;
public class MyClass {
material.ui.core.Switch slidingSwitch = new material.ui.core.Switch;
react.router.dom.Switch routerSwitch = new react.router.dom.Switch;
com.companydomain.common.utils.Switch customSwitch = new com.companydomain.common.utils.Switch;
}
If that looks like a vomiting of word soup, that's because... it is. Since you can't alias imports in Java, the only way to deal with the issue is to handle each different type of Switch
by using its fully-qualified name.
To be fair, every language has quirks and, at times, some annoying little limitations. The point of this post is: When the language runs into limitations, how are those limitations resolved??
Five years ago there were no imports in JavaScript. But now we have imports, with import aliasing thrown in as a bonus.
Java's had import capabilities since it was introduced. But it's never had import aliasing. Is that because no one wants import aliasing in Java? Nope. Import aliasing has been proposed as a new feature numerous times over the last twenty+ years. Every single time, it's been shot down - usually by a single, senior member of the Java open-source committee who just looks at the proposal and says, "Ummm... No. You don't need that. DENIED."
This is where Node/NPM/Babel is so critical in vaulting JavaScript past other languages. In Java, if you really want to have import aliasing, this is what that process looks like:
- Submit a JDK Enhancement Proposal (JEP).
- Have your JEP summarily dismissed with a one-line rejection like, "You don't need that."
- Just accept that the language doesn't have your desired feature and trudge along accordingly.
- Maybe, a few years later, submit a new JEP (and probably have it denied again).
This is the way it worked in JavaScript:
- No one had to sit around and wait for imports or import aliasing to be added to JavaScript by a committee. They went out and made their own packages - like,
RequireJS
. - As it became clear that third-party import solutions were becoming ubiquitous, the ECMAScript committee began working on a formal spec.
- Even if the ECMAScript committee had ignored imports altogether, or had denied import aliasing as a feature, anyone who wanted it could continue to use the third-party packages such as
RequireJS
- so no one was ever beholden to the whims of a stodgy old committee.
Agility... By Accident
First it bears mentioning that JavaScript's NPM/Babel ecosystem is not a magic cure-all for the administrative hurdles inherent in upgrading an entire programming language. With these tools, we can do an amazing "end-around" to get non-standard functionality that would take years - or decades - to get approved through regular channels. But it can still only provide features that could already be done, in some longer and more-manual form, through the language itself.
If you want JavaScript to to do something that it simply cannot do today, you still have to go through the ECMAScript committee. But for everything else, NPM/Babel provide a dynamic playground in which proposed features can be tested in live apps. And the download/installation of those features serves as a de facto vote in favor of those approaches.
I don't pretend for an instant that this state-of-affairs was a conscious strategy devised by anyone involved in the planning process for JavaScript. In fact, JavaScript's early history shows that it's just as susceptible to "analysis paralysis" as any other language. But the Babel/NPM revolution has allowed for the ecosystem of devs to put natural pressure on the planning committees by allowing us to install-and-run experimental packages without the fear that they won't compile on our users' systems (i.e., browsers). This, in turn, has sparked rapid evolution from a sleepy little language, in the early part of the the century, to a full-bore programming juggernaut today.
Of course, this doesn't make JavaScript any "better" than Java (or any other language). There are certain virtues to waiting a quarter century (or more) for something as simple and as basic as import aliasing. Waiting builds character. Waiting helps you appreciate life's more refined pleasures - like, Tiger King, or WWE wrestling. If Zen Buddhists had created software development, they would have most certainly included vast amounts of waiting. And multiple layers of petty denials.
I'm pretty sure that if I can just live to be 100, I'll probably see the day when Java finally implements import aliasing. And ohhhhh man! Will that day be grand!
Top comments (2)
Mr. Davis;
You made me laugh out loud. Excellent analysis of exactly what happened.
Great Article Adam,
I would love to read those typescript articles