JavaScript: The function of web pages
All UI pretty much boils down to this:
- Draw something on the screen
- Listen to and react to user events (clicks, hovers, swipes, etc.) or global events (time changed, window resized, wifi disabled)
JavaScript was developed in response to that second requirement; it was famously “developed in 5 days” as a way to add limited interactivity to documents: for example, to power dropdown menus.
The language
Despite the name, the language shares very little with Java, and is instead based on the ECMAscript standard (opens in a new tab) (an scripting language specification). Because of its rapid adoption and growth from a small scripting language to a full-blown application framework, JavaScript has a fair bit of bloat and plenty of language quirks that should be avoided.
For a good highlight-reel of those quirks, check out the WAT of JavaScript (opens in a new tab).
However, quirks aside, it is an incredibly powerful programming language. In fact, your browser has a full JavaScript REPL embedded in it: open up devtools and click on the “Console” tab and type:
console.log("Hello, world!");
When you click enter, the browser will evaluate that line and print “Hello, world!”. To take it a step further, you can copy the following to print your name!
const name = "YOUR NAME";
console.log("Hello, " + name + "!");
For a crash course on the syntax of JavaScript, check out Learn X in Y (opens in a new tab).
The browser runtime
In the context of web pages, JavaScript is either included as a referenced file (same way CSS is included):
<!DOCTYPE html>
<html lang="en">
<head>
<title>Demo app</title>
<script src="script.js" />
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
Or directly written inline:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Demo app</title>
<script>
console.log("Hello, world!");
</script>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
By default, when the browser is parsing the HTML, if it encounters a script tag, it will stop, load, parse, and run the JavaScript before it resumes parsing the HTML. This is very rarely desired—as you almost always want to display SOMETHING to the user before you start loading a script—so you’ll either see script tags included at the bottom of the body (after the main content has been parsed and displayed) or with an async attribute (e.g. <script src="..." async />
) which does the same thing.
The DOM
JavaScript is loaded along-side the webpage, and the runtime provides hooks into the browser’s in-memory model of the page: called the “document object model” or “DOM”. The DOM lets JavaScript listen for events and modify the page in response.
Much like we did with HTML or CSS, we can experiment using CodeSandbox. For example, I can query for all the paragraph tags using:
console.log(document.querySelectorAll("p"));
And I can modify, or remove, those matching elements:
Events
As I said before, every UI language needs a way to respond to user events. In JavaScript, this takes the form of explicit event listeners that you can attach. For example, I can add an event listener for click events:
Because components are nested, events may occur on multiple components:
You may have noticed that the parent event happens AFTER the child event. by default, events bubble from child to parent up the document tree.
By selecting the element in your browser’s developer tools, you can see any event handlers attached to it:
And if you go to the source
panel, you can even set a breakpoint on it (opens in a new tab).
In these examples, I've mostly focused on the click event, but there are many, many event types available. Some examples include:
- mouse events
- drag and drop events
- animation events (animation started, completed)
- window events (scroll, resize)
- gyroscope and accelerometer events
- network events (went online / offline)
- ...and hundreds more (opens in a new tab)
HTML5 APIs
With the building blocks of HTML, CSS, and JavaScript, the web changed very quickly: JavaScript didn’t stay a scripting language intended to open and close dropdowns for long; developers got increasingly ambitious with what web applications could do; and APIs were added to enable increasingly more native experiences; specifically:
- XHR (opens in a new tab): enabling webpages to programmatically query the network for additional data. (These days, the fetch API (opens in a new tab) is more common.)
- HTML5 (opens in a new tab): an updated specification for HTML, adding APIs targeted at full application development such as offline, history, geolocation, file reading, image rendering, etc.
For a full list of JavaScript APIs, check out MDN (opens in a new tab).
Frontend frameworks
Alongside the new APIs and browser-based applications (or perhaps powering the browser-based applications) came a suite of libraries built at targeting frontend.
Wave 1: Utilities
The first wave of popular frontend frameworks came about in the 2000s and provided utilities to decorate static documents rendered on the server with interactivity. They general shape was "select element" and "do something to it" (at this point, you could not select elements with CSS selectors natively). Alongside that core functionality, they provided shims to simplify various brower tasks, such as requesting data from the network (something that was difficult at the time).
$( "#button-container button" ).on( "click", function( event ) {
hiddenBox.show();
});
Examples include: Mootools (opens in a new tab), Prototype (opens in a new tab), and jQuery (opens in a new tab).
Wave 2: Data binding
The second wave came about in the late 2000s / early 2010s and made it easier to bind data to HTML. Unlike the first wave, which treated the HTML as the source of truth for data, this wave assumed that JavaScript was the source of truth for data (treating JavaScript as a first-class citizen, rather than a progressive enhancement); these frameworks made it easy to store state in JavaScript, interpolate that state into their HTML, and modify that state in response to HTML events.
<html ng-app>
<!-- Body tag augmented with ngController directive -->
<body ng-controller="MyController">
<input ng-model="foo" value="bar">
<!-- Button tag with ngClick directive, and
string expression 'buttonText'
wrapped in "{{ }}" markup -->
<button ng-click="changeFoo()">{{buttonText}}</button>
<script src="angular.js"></script>
</body>
</html>
Examples include: Backbone (opens in a new tab), Angular 1.0 (opens in a new tab), Ember (opens in a new tab), and Knockout (opens in a new tab).
Wave 3: Components
The third wave further abstracted UI into components: bundles of structure, style, and function creating reusable UI concepts (a menu, table row, or form input).
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
Examples include: React (opens in a new tab), Angular 2.0 (opens in a new tab), Svelte (opens in a new tab), Vue (opens in a new tab), and Solid.js (opens in a new tab).
A key innovation of React was JSX, a superset of JavaScript that allowed developers to write HTML-like syntax in JavaScript files. This language could be transpiled into JavaScript (with a object-based representation of the inlined HTML). We'll cover this more later.
Wave 4: Fullstack
More recently, we've seen a fourth wave begin to emerge. Technologies like Next.js take advantage of the fact that JavaScript now powers both the client and server for many sites. By using Next.js, you can write React code that renders on the server, and keeps the client up-to-date.
While this wave is still early, examples include: Next.js (opens in a new tab), Remix (opens in a new tab), and Astro (opens in a new tab).
I will call out that these waves are fuzzy, and the idea of writing JavaScript that was shared between client and server started much earlier with Meteor (opens in a new tab).