JavaScript in 101 Lessons: The Language That Runs Everything and Explains Nothing
JavaScript is a wonderful programming language to learn for beginners. It’s also a horrible one. Both things are true at once, and that contradiction is the whole story.
On one hand, you can build almost anything with it and get a job anywhere if you master it. On the other, it’s weird, ugly, and surrounded by a dystopian wasteland of frameworks and libraries. Welcome to JavaScript 101.
Where It Came From
It was created in 1995 by Brendan Eich at Netscape, back when the web browser was cutting-edge technology and websites were completely static — pure HTML, no interaction.
JavaScript was designed as an easy-to-use, high-level language to help developers make those pages interactive. Today it’s arguably the most popular language in the world. Its standard implementation is called ECMAScript, and it’s the default language in every browser. In fact, it’s the only code that natively runs in a browser, aside from WebAssembly.
But the browser isn’t the only runtime. Tools like Node.js and Deno let you run JavaScript on a server.
And as the name implies, it’s a scripting language. You can execute code on the fly — open the console in your developer tools and change the appearance of a website in real time. It’s interpreted line by line, unlike compiled languages such as C. Though “interpreted” isn’t quite accurate either: under the hood, engines like V8 make JavaScript extremely fast by converting your code into machine code through just-in-time compilation.
Variables and the Great Semicolon War
To use JavaScript on a page, you need an HTML document with a script tag. You can write code inside the tag, or reference an external file with the source attribute. To say hello, use console.log — JavaScript’s built-in tool for printing to standard output.
There are several ways to define variables. The most common today is let. Give it a name, usually in camel case, then assign a value. JavaScript is dynamically typed: no type annotations required. Assign a number — one of seven primitive types — and later reassign the same variable to a string. Different type, perfectly fine.
If you declare a variable without assigning a value, it defaults to undefined. You can represent emptiness explicitly with null. Anything that isn’t a primitive inherits from the object class.
const is used for values that can’t be reassigned. var was the original and is best ignored entirely, even though you’ll find it in the wild.
The reason there are so many options is scope. Global scope means a variable is available everywhere. Define a variable inside a function and it becomes local to that function. Statements like if-conditions create block scope — unless you used var, which isn’t block-scoped and gets hoisted into the function’s local scope. You don’t want that weirdness in your life.
And yes — semicolons are technically optional, because the parser inserts them automatically. In real life, developers fight to the death over whether to use them.
Functions, Closures, and the Dreaded this
A function definition takes an input, optionally returns a value. Functions are objects, which means they can be used as expressions, assigned to variables, or passed as arguments to higher-order functions. They can also be nested to create closures that encapsulate data and logic from the rest of the program.
Primitives are stored on the call stack, the browser’s short-term memory. But when you create a closure, the inner function can still access variables from the outer function even after the outer call has finished. That’s because JavaScript stores that data in heap memory, which persists between calls.
Then there’s this — a keyword that references an object based on how a function is called. Called from global scope, it references the window object. Called as a method on an object, it references that object. You can bind it manually with bind.
Modern JavaScript offers an alternative: arrow functions. They don’t have their own this, and they’re always anonymous, which makes them ideal for function expressions.
One last thing about functions. Primitives are passed by value — a copy is made. Objects are stored in the heap and passed by reference, which means multiple parts of your code might be mutating the same object.
Objects, Prototypes, and Class Sugar
The easiest way to define an object is the literal syntax with braces. There’s also the constructor form with new. An object contains key-value pairs, and objects can inherit properties from each other through the prototype chain. Every object has a private property linking to exactly one prototype.
This differs from class-based inheritance in other languages, because you’re dealing with real objects that take up memory rather than abstract classes in code.
Confusingly, JavaScript also supports the class keyword — but classes are just syntacti
c sugar for prototypal inheritance. A class can define a constructor, properties, getters and setters, and methods, including static methods attached to the class name itself.
Beyond objects, JavaScript ships with built-in data structures: arrays for indexed collections, sets for unique values, maps for key-value pairs that are easier to iterate than plain objects.
And JavaScript is garbage collected — it automatically deallocates objects when they’re no longer referenced. If that’s not optimal, weak maps and weak sets hold properties that can be collected to reduce memory usage.
The Event Loop
JavaScript’s most interesting feature is its non-blocking event loop. Code normally executes synchronously, line by line — the next line can’t start until the previous one finishes. With the event loop, you can write asynchronous code that runs in a separate thread pool while the rest of the application continues.
This matters because modern websites have multiple things happening at once but only one thread for computing: the main thread. Without async code, multitasking would be impossible.
The classic demonstration is setTimeout, which takes a function and doesn’t call it until a set number of milliseconds have elapsed. That’s a callback — queued in the event loop, called back later when it’s ready.
Callbacks are common, but nesting them too deeply produces callback hell. Promises are the better way: a wrapper for a value that’s unknown now but will resolve in the future. If something goes wrong, the promise rejects. Consumers use then and catch.
Better still, async functions return a promise automatically, and await pauses execution until another promise resolves. Wrap it in try/catch for error handling and you get readable code.
Modules and the npm Ecosystem
As code grows, it won’t fit in one file. Modules let you share code between files. By default everything in a file is private; mark something as a default export and import it elsewhere. You can also export multiple values and import them by name.
In practice, you’ll use code written by other people. npm is the largest JavaScript package manager. Installing a package downloads it into node_modules and records it in package.json.
The Browser and the DOM
On the web, code runs in the browser, which is built on the document object model — the UI represented as a tree of HTML nodes. The browser provides APIs to interact with those nodes, the most important being document.
querySelector grabs an individual element using a CSS selector. querySelectorAll grabs many. Most importantly, addEventListener lets you respond to events — like a button click — by assigning a function to run when the event happens.
Much of web development is listening to events and updating the UI accordingly.
Frameworks and the Problem With Imperative Code
One thing many developers dislike about vanilla JavaScript is that it produces imperative code, where the UI is mutated directly.
Many now use front-end frameworks that produce declarative code, where the UI is a function of its input data. These libraries encapsulate JavaScript, HTML and CSS into components, composed into a component tree. Inside a component, data is reactive — change the data and the UI updates automatically.
After you build an app, you’ll need to bundle your files into something the browser can use efficiently. Module bundlers like Vite or webpack handle that. But bundles can get too big, which hurts page load performance — measurable in the network waterfall of your dev tools. The fix is splitting the bundle and using dynamic imports to load JavaScript only when it’s needed.
Beyond the Browser
JavaScript doesn’t only run in browsers. Node.js is the most popular server runtime, and you can execute code at any time with the node command.
That opens the door to Electron, which combines Node.js with a browser to build full-stack desktop apps in JavaScript, and React Native for iOS and Android.
At this point you’ve got 99 problems and JavaScript is every single one of them. If you want to make life easier, use a tool like TypeScript or ESLint that performs static analysis to improve code quality.
The Honest Summary
JavaScript will let you build a website, a mobile app, a desktop app, a server, an operating system, an artificial intelligence — and all kinds of other things you probably shouldn’t build with it.
That’s the deal. It’s flexible to the point of being shapeless, forgiving to the point of being dangerous, and ubiquitous to the point where avoiding it is a career decision rather than a technical one.
Learn it because it runs everywhere. Learn it carefully because it will let you do almost anything — including the wrong thing.
Related Articles
Comments
No comments yet.