Notes on
You Don't Know JS Yet: Get Started
by Kyle Simpson
| 17 min read
The truth about JavaScript’s name is fascinating—it’s purely an artifact of marketing shenanigans. Brendan Eich originally code-named it Mocha, and internally at Netscape, they used the brand LiveScript. But when it came time to publicly name the language, “JavaScript” won because they wanted to appeal to Java programmers and “script” was a popular term for lightweight programs that would embed inside web pages.
The official name specified by TC39 and formalized by ECMA is ECMAScript, and since 2016, it’s been suffixed by the revision year (like ECMAScript 2019 or ES2019). So the JavaScript that runs in browsers or Node.js is actually an implementation of the ES2019 standard.
Transpiling and Polyfilling
For new and incompatible syntax, the solution is transpiling—a community-invented term describing the use of tools to convert source code from one form to another while keeping it as textual source code. Babel is the most common transpiler, converting newer JS syntax to equivalent older syntax. This allows developers to write code using the latest syntax features while ensuring it runs on older browsers.
When the forwards-compatibility issue isn’t about new syntax but rather missing API methods, we use polyfills (also called “shims”). These provide definitions for missing API methods that act as if the older environment had them natively defined.
I found the distinction between polyfills and shims interesting:
- Polyfills are typically focused on implementing standardized features and ensuring compatibility with modern web standards in older browsers.
- Shims can include polyfills but are broader in scope, addressing functionality gaps, fixing bugs, or extending the capabilities of the environment beyond standard compliance.
In practice, the terms are often used interchangeably, but understanding the nuances helps when implementing browser support. Since JS won’t stop improving, the gap will never go away—both transpilation and polyfilling should be embraced as standard parts of every JS project’s production chain.
JavaScript as a Compiled Language
This was really interesting to learn about how JS goes from source code to execution! All compiled languages are parsed first, and parsed languages usually perform code generation before execution. JS source code must be parsed before execution because the spec requires “early errors” to be reported before code starts executing.
The compilation process is:
- After leaving the developer’s editor, code gets transpiled by Babel, packed by Webpack (and other build processes)
- The JS engine parses the code to an Abstract Syntax Tree (AST)
- The engine converts the AST to a kind of byte code—a binary intermediate representation (IR)
- The optimizing JIT compiler further refines/converts this
- Finally, the JS VM executes the program
“I think it’s clear that in spirit, if not in practice, JS is a compiled language.”
Since JS is compiled, we’re informed of static errors before execution—a substantively different and more helpful interaction model than traditional “scripting” programs.
Web Assembly (WASM)
Performance concerns have driven much of JS’s evolution, leading to innovations like ASM.js (demonstrated in 2013 with Unreal 3 engine ported to JS) and eventually Web Assembly. WASM is a binary format similar to Assembly that allows non-JS programs (like C) to run efficiently in JS engines by skipping the parsing/compilation that JS normally requires.
Key points about WASM:
- Enables ahead-of-time (AOT) compilation for minimal runtime processing
- Allows features from other languages (like threading in Go) without adding them to JS
- Emerging as a cross-platform virtual machine beyond just the web
- Requires static typing, making JS and even TypeScript less suitable as source languages
WASM will not replace JS. WASM significantly augments what the web (including JS) can accomplish.
JavaScript’s Definition
“JS is an implementation of the ECMAScript standard (version ES2019 as of this writing), which is guided by the TC39 committee and hosted by ECMA. It runs in browsers and other JS environments such as Node.js.
JS is a multi-paradigm language, meaning the syntax and capabilities allow a developer to mix and match (and bend and reshape!) concepts from various major paradigms, such as procedural, object-oriented (OO/classes), and functional (FP).
JS is a compiled language, meaning the tools (including the JS engine) process and verify a program (reporting any errors!) before it executes.”
This basically summarizes everything about what JavaScript is.
Files as Programs
In JS, each standalone file is its own separate program. Many projects use build tools that combine files into a single file for delivery. When this happens, JS treats the combined file as the entire program.
This matters primarily for error handling—one file may fail without preventing the next file from being processed. If your application depends on five .js files and one fails, the application will probably only partially operate at best. The only way multiple .js files act as a single program is by sharing state via the global scope namespace.
Values and Types
Values are the most fundamental unit of information in a program—they’re data and how programs maintain state. Values come in two forms: primitive and object.
The primitive types in JavaScript are:
Strings
(ordered collections of characters)Numbers
Booleans
null
andundefined
(both indicate emptiness/absence of value)Symbols
(special-purpose values that behave as hidden unguessable values, mostly used as special keys on objects)
I’ve never really used symbols—nice to know they exist.
Besides primitives, the other value type is object
values. Arrays and functions are special kinds of objects (sub-types). Converting from one value type to another is called “coercion” in JS.
Primitive values and object values behave differently when assigned or passed around (pass by value vs reference).
// Primitives: Pass by VALUE (copies the value)
let a = 42;
let b = a; // b gets a COPY of a's value
a = 100; // changing a doesn't affect b
console.log(a); // 100
console.log(b); // 42 (unchanged)
/*
Memory visualization:
┌─────────┐ ┌─────────┐
│ a: 100 │ │ b: 42 │ ← separate memory locations
└─────────┘ └─────────┘
*/
// Objects: Pass by REFERENCE (copies the reference)
let obj1 = { name: "Kyle" };
let obj2 = obj1; // obj2 gets a copy of the REFERENCE
obj1.name = "Simpson"; // modifying through obj1...
console.log(obj1.name); // "Simpson"
console.log(obj2.name); // "Simpson" (same object!)
/*
Memory visualization:
┌───────────┐ ┌───────────┐ ┌──────────────────────┐
│ obj1: ────┼────┼─ obj2: ───┼─────▶│ { name: "Simpson" } │
└───────────┘ └───────────┘ └──────────────────────┘
Both variables point to the same object in memory
*/
Variable Declaration and Scoping
The let
keyword provides block scoping, unlike var
which provides function scoping. In the example with var myName
and let age
inside an if statement, myName can be accessed outside but age cannot. Block-scoping is useful for preventing accidental overlap of variable names.
function example() {
if (true) {
var myName = "Kyle"; // function-scoped
let age = 39; // block-scoped
}
console.log(myName); // ✅ "Kyle" - var escapes the block
console.log(age); // ❌ ReferenceError - let stays in block
}
/*
Scope visualization:
┌─ function example() ──────────────────┐
│ ┌─ if block ─────────────────────┐ │
│ │ var myName ← hoisted to here │ │ ← myName lives here
│ │ let age ← stays here │ │
│ └────────────────────────────────┘ │
│ console.log(myName); // accessible │
│ console.log(age); // not here! │
└───────────────────────────────────────┘
*/
The author argues against the common advice to avoid var
in favor of let
, believing it’s overly restrictive. Both declaration forms can be appropriate depending on circumstances—var
communicates wider scope visibility. I’m mostly in the always-use-const camp, but I also believe in knowing the ins and outs of a language to choose the most appropriate tool.
Regarding const
with objects, the author says it’s ill-advised because values can still be changed even though the variable can’t be reassigned. I don’t agree with this. You say it’s OK to use ‘var’ but then say this about const? I think it’s not unreasonable to expect JS users to know object behavior when assigned with const. The best semantic use of const
is for simple primitive values with useful names.
Functions and Procedures
In JavaScript, we should consider “function” to take the broader meaning of “procedure”—a collection of statements that can be invoked multiple times, may take inputs, and may give outputs.
Function declarations appear as standalone statements and the association between identifier and function value happens during compile phase:
function awesomeFunction(coolThings) {
// ..
return amazingStuff;
}
Function expressions are assigned to variables and not associated with their identifier until runtime:
var awesomeFunction = function(coolThings) {
// ..
return amazingStuff;
};
This is why you can use function declarations anywhere in a file (even “before” the declaration), but not arrow functions, as they are expressions and aren’t evaluated before being run.
Equality Comparisons
The ===
operator has some surprising behaviors:
- For NaN, it lies and says
NaN !== NaN
- For -0 (a real, distinct value!), it lies and says
-0 === 0
Use Number.isNaN()
for NaN comparisons and Object.is()
for -0 comparisons. You could think of Object.is()
as the “quadruple-equals” (====)—the really-really-strict comparison!
For objects, ===
uses identity equality, not structural equality. Objects are held by reference, assigned by reference-copy, and compared by reference equality. Two objects with the same structure aren’t equal unless they’re the exact same object in memory.
Coercive Comparisons
Coercion means converting a value of one type to its representation in another type. The ==
operator performs “coercive equality”—if types match, it works exactly like ===
, but if types differ, it allows coercion first.
Relational operators (<
, >
, <=
, >=
) typically use numeric comparisons, except when both values are strings (then they use alphabetical comparison). This leads to common pitfalls like "10" < "9"
being true! There’s no way to avoid coercion with these operators. TypeScript helps prevent these issues.
// === vs == comparison examples
console.log(42 === "42"); // false (different types)
console.log(42 == "42"); // true (string coerced to number)
console.log(true === 1); // false (different types)
console.log(true == 1); // true (boolean → number: true becomes 1)
console.log(null === undefined); // false (different types)
console.log(null == undefined); // true (special case in spec!)
// Tricky relational operator coercions
console.log("10" < "9"); // true! (alphabetical: "1" < "9")
console.log("10" < 9); // false (string → number: 10 < 9)
console.log(10 < "9"); // false (string → number: 10 < 9)
console.log("a" < "b"); // true (alphabetical comparison)
console.log("a" < 1); // false ("a" → NaN, NaN < 1 is false)
/*
Coercion flow visualization:
== operator:
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ 42 == "42" │ ──▶│ Convert "42" to │ ──▶│ 42 === 42 │ ✅
│ │ │ number (42) │ │ │
└─────────────┘ └─────────────────┘ └──────────────┘
< operator with mixed types:
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ "10" < 9 │ ──▶│ Convert "10" to │ ──▶│ 10 < 9 │ ❌
│ │ │ number (10) │ │ │
└─────────────┘ └─────────────────┘ └──────────────┘
< operator with both strings:
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ "10" < "9" │ ──▶│ Alphabetical │ ──▶│ "1" < "9" │ ✅
│ │ │ comparison │ │ │
└─────────────┘ └─────────────────┘ └──────────────┘
Gotchas to watch out for:
• [] == false → true (empty array coerces to "")
• "" == 0 → true (empty string coerces to 0)
• "0" == false → true (boolean → number, string → number)
• null >= 0 → true (null → 0 for comparisons)
• null == 0 → false (special case: null only == undefined)
*/
// Safer alternatives:
console.log(Number("42") === 42); // explicit conversion
console.log(String(42) === "42"); // explicit conversion
console.log(Object.is(NaN, NaN)); // true (better than ===)
console.log(Object.is(-0, 0)); // false (better than ===)
Iterator Pattern
The iterator pattern is a standardized approach to consuming data from a source one chunk at a time. ES6 standardized this with a protocol defining a next()
method that returns an iterator result object with value
and done
properties.
The for..of
loop provides clean consumption of iterators. I like this pattern—off by one errors and out of bounds issues are annoying to deal with manually.
An iterable is a value that can be iterated over. The protocol creates an iterator instance from an iterable and consumes it to completion. A single iterable can be consumed multiple times with new iterator instances each time.
You can use arr.entries()
to get both index and value in iteration. All built-in iterables have three iterator forms: keys()
, values()
, and entries()
.
let data = ["apple", "banana", "cherry"];
// ❌ Manual iteration - error-prone!
console.log("=== Manual Iteration ===");
for (let i = 0; i < data.length; i++) {
console.log(`${i}: ${data[i]}`);
}
// Risk: off-by-one errors, out-of-bounds access
// ✅ Iterator pattern with for..of - clean and safe!
console.log("=== Iterator Pattern ===");
for (let [index, value] of data.entries()) {
console.log(`${index}: ${value}`);
}
/*
Iterator pattern visualization:
Manual iteration:
┌─ for loop ──────────────────────────────────────┐
│ i = 0, check i < length, access data[i] │
│ i++, check i < length, access data[i] │ ← You manage everything
│ i++, check i < length, access data[i] │
│ i++, check i < length → false, exit │
└─────────────────────────────────────────────────┘
↑ Manual bounds checking
↑ Manual index management
↑ Potential for mistakes!
for..of iteration:
┌─ for..of loop ───────────────────────────────────┐
│ Get iterator from data.entries() │
│ ┌─ Iterator ──────────────────────────────────┐ │
│ │ Call next() → {value: [0,"apple"], done:F} │ │ ← Iterator manages state
│ │ Call next() → {value: [1,"banana"], done:F}│ │
│ │ Call next() → {value: [2,"cherry"], done:F}│ │
│ │ Call next() → {value: undefined, done: T} │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
↑ Automatic bounds checking
↑ Clean, declarative syntax
↑ No off-by-one errors!
*/
// Different iterator forms:
let map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let key of map.keys()) {
console.log(`Key: ${key}`);
}
for (let value of map.values()) {
console.log(`Value: ${value}`);
}
for (let [key, value] of map.entries()) {
console.log(`${key} = ${value}`);
}
// Custom iterables work too!
let fibonacci = {
*[Symbol.iterator]() {
let [a, b] = [0, 1];
yield a; yield b;
while (true) {
[a, b] = [b, a + b];
yield b;
}
}
};
// Take first 10 fibonacci numbers
let count = 0;
for (let num of fibonacci) {
console.log(num);
if (++count >= 10) break;
}
Closure
“Closure is when a function remembers and continues to access variables from outside its scope, even when the function is executed in a different scope.”
This pragmatic definition captures the essence of one of JavaScript’s most powerful features.
function makeCounter() {
let count = 0; // private variable
return function() { // inner function has closure
count++; // remembers and accesses 'count'
return count;
};
}
let counter1 = makeCounter();
let counter2 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate closure!)
console.log(counter1()); // 3
/*
Closure visualization:
┌─ Global Scope ──────────────────┐
│ │
makeCounter() ──────┼─────────────────────────────────┤
execution: │ ┌─ makeCounter execution ──┐ │
│ │ let count = 0; │ │
│ │ │ │
│ │ return function() { │ │
│ │ count++; ← CLOSURE │ │
│ │ return count; │ │
│ │ } │ │
│ └──────────────────────────┘ │
│ ↓ │
counter1 ───────────┼─ function() + closure to count │
│ (count lives on!) │
└─────────────────────────────────┘
Even after makeCounter() finishes executing, the returned function
remembers its lexical scope and keeps 'count' alive!
*/
The this
Keyword
One of JS’s most misunderstood mechanisms! Common misconceptions are that this
refers to the function itself or to the instance a method belongs to—both wrong.
While functions have scope (static, fixed at definition time), they also have execution context (dynamic, determined each time the function is called). The this
keyword exposes this execution context. It’s not a fixed characteristic based on definition but a dynamic characteristic determined by how the function is called.
Think of execution context as a tangible object whose properties are available to the function while it executes, compared to the hidden scope object that’s always the same for that function.
function greet() {
console.log(`Hello, ${this.name}!`);
}
let person1 = { name: "Kyle" };
let person2 = { name: "Simpson" };
// Different ways to call the same function = different 'this'
greet(); // this = global object (undefined in strict mode)
greet.call(person1); // this = person1 → "Hello, Kyle!"
greet.call(person2); // this = person2 → "Hello, Simpson!"
person1.sayHi = greet; // add method to person1
person1.sayHi(); // this = person1 → "Hello, Kyle!"
/*
'this' binding visualization:
┌─ Function Definition ───────┐ ┌─ Execution Context ────┐
│ │ │ │
│ function greet() { │ ──▶ │ this = ??? │
│ console.log( │ │ (determined by │
│ `Hello, ${this.name}!` │ │ how function │
│ ); │ │ was called) │
│ } │ │ │
└─────────────────────────────┘ └────────────────────────┘
↑ ↑
Static (always same) Dynamic (changes)
Call site determines 'this':
┌─────────────────────┬─────────────────────┐
│ Call Site │ this points to │
├─────────────────────┼─────────────────────┤
│ greet() │ global / undefined │
│ greet.call(person1) │ person1 │
│ person1.sayHi() │ person1 │
│ new greet() │ new object │
└─────────────────────┴─────────────────────┘
*/
// Arrow functions are different - they inherit 'this' lexically
let obj = {
name: "Context",
regularMethod() {
console.log(this.name); // "Context"
let arrow = () => {
console.log(this.name); // "Context" (inherited!)
};
arrow();
function regular() {
console.log(this.name); // undefined (new 'this')
}
regular();
}
};
obj.regularMethod();
Prototypes
A prototype is a characteristic of an object, specifically for resolution of property access. It’s a hidden linkage between objects that occurs when an object is created. A series of objects linked via prototypes is called the “prototype chain.”
The purpose is delegation—when object B doesn’t have a property/method, the access is delegated to linked object A. For example, homework.toString()
works even though homework doesn’t have that method; it delegates to Object.prototype.toString()
.
let homework = {
topic: "JS"
};
console.log(homework.toString()); // "[object Object]"
// homework doesn't have toString(), so JS looks up the chain!
/*
Prototype chain visualization:
┌─────────────────┐
│ homework │ ← doesn't have toString()
│ topic: "JS" │
└─────────┬───────┘
│ [[Prototype]]
▼
┌───────────────────┐
│ Object.prototype │ ← has toString()! ✅
│ toString() │
│ valueOf() │
│ hasOwnProperty()│
└─────────┬─────────┘
│ [[Prototype]]
▼
┌─────────────────┐
│ null │ ← end of chain
└─────────────────┘
homework.toString() lookup process:
1. Check homework → no toString() found
2. Follow [[Prototype]] to Object.prototype
3. Check Object.prototype → toString() found! ✅
4. Call Object.prototype.toString() with this = homework
*/
// Creating explicit prototype linkage
let otherHomework = Object.create(homework);
otherHomework.assignment = "Read Ch4";
console.log(otherHomework.topic); // "JS" (from homework)
console.log(otherHomework.toString()); // "[object Object]" (from Object.prototype)
/*
Extended chain:
┌──────────────────┐
│ otherHomework │ ← assignment: "Read Ch4"
└─────────┬────────┘
│ [[Prototype]]
▼
┌─────────────────┐
│ homework │ ← topic: "JS"
└─────────┬───────┘
│ [[Prototype]]
▼
┌─────────────────┐
│ Object.prototype│ ← toString(), etc.
└─────────┬───────┘
│ [[Prototype]]
▼
┌─────────────────┐
│ null │
└─────────────────┘
*/
To create prototype linkage, use Object.create()
. The first argument specifies what to link to.
When you assign to a property that exists up the prototype chain, it creates a new property on the object rather than modifying the prototype’s property—this “shadows” the prototype property.
Prototypes and this
The true importance of this
shines with prototype-delegated function calls. One main reason this
supports dynamic context is so method calls on objects that delegate through the prototype chain maintain the expected this
. When calling a method that exists on a prototype, this
still refers to the object the method was called on, not where the method is defined.
Lexical Scope
Scopes nest inside each other, and only variables at the current level or higher/outer scopes are accessible. This is lexical scope—the scope structure is determined at parse/compile time based on where you write functions in the program.
JS is lexically scoped despite claims otherwise, though it has unique characteristics:
- Hoisting: Variables declared anywhere in a scope are treated as if declared at the beginning
- Function-scoped
var
: var-declared variables are function scoped even if they appear inside blocks - Temporal Dead Zone (TDZ): let/const declarations have peculiar error behavior with observable but unusable variables
None of these invalidate lexical scoping—they’re just unique parts of the language to learn and understand.
Beyond Classes
Classes are just one pattern built on top of JavaScript’s object and prototype system. But prototypes aren’t just primitive classes—they enable other patterns like behavior delegation. Simply embracing objects as objects and letting them cooperate through the prototype chain can be more powerful than class inheritance for organizing behavior and data in programs.
The old “prototypal class” pattern using prototype manipulation is now strongly discouraged in favor of ES6’s class syntax. It doesn’t make sense to write classes with prototypes anymore—just use classes.
Values vs References
JavaScript chooses value-copy vs reference-copy behavior based on the value type. Primitives are held by value, objects are held by reference. There’s no way to override this in either direction—it’s a fundamental characteristic of the language.
Liked these notes? Join the newsletter.
Get notified whenever I post new notes.