JavaScript Essentials
Execution Context and Call Stack
When the JavaScript engine executes code, it creates an Execution Context. Each context contains three parts:
- Variable Environment: Stores
vardeclarations and function declarations - Lexical Environment: Stores
let/constdeclarations - this binding: Determined by how the function is called
The execution context lifecycle: creation → execution → cleanup. The call stack manages the entry and exit of contexts:
function greet(name) {
return `Hello, ${name}`;
}
function start() {
const msg = greet("World");
console.log(msg);
}
start();
// Call stack changes: start() → greet() → return → console.log() → return
The call stack is a Last-In-First-Out (LIFO) structure. When the call stack becomes too deep, it throws RangeError: Maximum call stack size exceeded—a stack overflow. The typical scenario is recursion without a termination condition.
Closures, Scope Chain, and Hoisting
Scope Chain
JavaScript uses Lexical Scope—a function’s scope is determined at definition time, not at call time. The engine looks up variables through the scope chain: starting from the current scope, searching outward layer by layer until reaching the global scope.
Closures
A closure is when a function can access variables in its lexical scope, even when the function is executed outside that scope:
function createCounter() {
let count = 0; // Captured by closure
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2
Practical value of closures: Data privacy (module pattern), function factories (currying), callbacks with state (event handlers). Note that closures prevent garbage collection—improper use can cause memory leaks.
Variable Hoisting
console.log(a); // undefined (var is hoisted, assignment is not)
console.log(b); // ReferenceError (let Temporal Dead Zone TDZ)
var a = 1;
let b = 2;
var declarations are hoisted to the top of the scope and initialized to undefined; let/const declarations are also hoisted, but are in a Temporal Dead Zone (TDZ) before the declaration statement—accessing them throws an error. Function declarations are hoisted entirely (including the function body), while function expressions only hoist the variable declaration.
Prototype Chain and Inheritance
JavaScript’s inheritance is based on the prototype chain, not classes. Every object has an internal [[Prototype]] property pointing to its prototype object.
graph TD
A["Instance obj"] -->|"__proto__"| B["Person.prototype"]
B -->|"__proto__"| C["Object.prototype"]
C -->|"__proto__"| D["null"]
B -->|"constructor"| E["Person function"]
E -->|"prototype"| B
Property lookup searches up the prototype chain: obj.name → obj.__proto__.name → obj.__proto__.__proto__.name → … → null.
Evolution of Inheritance Patterns
// 1. Prototype chain inheritance
Child.prototype = new Parent();
// 2. Constructor stealing
function Child() {
Parent.call(this);
}
// 3. Combination inheritance (most common ES5 pattern)
function Child() {
Parent.call(this); // Instance properties
}
Child.prototype = Object.create(Parent.prototype); // Prototype methods
Child.prototype.constructor = Child;
// 4. ES6 class syntactic sugar
class Child extends Parent {
constructor() {
super();
}
}
class is essentially syntactic sugar over the prototype chain, but the syntax is cleaner and closer to the conventions of other languages.
ES6+ Core Features
Destructuring Assignment
// Array destructuring
const [first, , third] = [1, 2, 3];
// Object destructuring + renaming + defaults
const { name: userName = "Anonymous", age } = user;
// Function parameter destructuring
function render({ title, items = [] }) {
// ...
}
Arrow Functions
Arrow functions don’t have their own this, arguments, or super—they inherit this from the outer lexical scope:
const team = {
members: ["Alice", "Bob"],
list() {
// Arrow function inherits this, pointing to team
this.members.forEach(member => console.log(member, this.members));
},
};
Symbol and Iterator
// Symbol — unique identifier
const id = Symbol("id");
const user = { [id]: 123, name: "Alice" };
// Iterator — unified traversal interface
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => current <= this.to
? { value: current++, done: false }
: { done: true },
};
},
};
[...range]; // [1, 2, 3, 4, 5]
Objects that implement the Symbol.iterator protocol can be traversed with for...of and the spread operator.
Module System Evolution
flowchart LR
A["IIFE<br/>Immediately Invoked Function"] --> B["CommonJS<br/>require/exports"]
B --> C["AMD<br/>define/require"]
C --> D["ES Modules<br/>import/export"]
style D fill:#4caf50,color:#fff
| Standard | Loading | Use Case | Characteristics |
|---|---|---|---|
| IIFE | Synchronous | Early browsers | Global pollution isolation, no dependency management |
| CommonJS | Synchronous | Node.js | Runtime loading, value copying |
| AMD | Asynchronous | Early browsers | RequireJS, verbose syntax |
| ES Modules | Static | Browser + Node | Compile-time static analysis, value references, Tree-shaking friendly |
// ES Modules — modern standard
export const API_BASE = "/api";
export function fetchUser(id) { /* ... */ }
export default class UserService { /* ... */ }
// Importing
import UserService, { API_BASE } from "./user.js";
The static nature of ES Modules allows build tools to analyze the dependency graph at compile time and implement Tree-shaking—removing unused code. This is the foundation of modern build tools like Vite and Rollup.
6. Asynchronous Programming Core
6.1 Promise Chaining
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error('Request failed:', err));
6.2 async/await Error Handling
// Recommended error handling pattern
async function fetchUser(id) {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
console.error(`Failed to fetch user ${id}:`, error);
return null;
}
}
// Parallel requests
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
6.3 Event Loop Mechanism
┌───────────────────────┐
│ Call Stack │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Microtask Queue │ ← Promise.then, queueMicrotask
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Macrotask Queue │ ← setTimeout, setInterval, I/O
└───────────────────────┘
Execution order: Sync code → Drain microtasks → Next macrotask
7. Proxy and Reflect
// Reactive data proxy
const reactive = (target) => {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key); // Dependency collection
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
const oldValue = obj[key];
Reflect.set(obj, key, value, receiver);
if (oldValue !== value) trigger(obj, key); // Trigger update
return true;
}
});
};
8. Functional Programming
8.1 Pure Functions and Composition
// Pure function: same input always produces same output, no side effects
const add = (a) => (b) => a + b;
const multiply = (a) => (b) => a * b;
// Function composition
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const add10 = add(10);
const double = multiply(2);
const add10ThenDouble = compose(double, add10);
add10ThenDouble(5); // 30
8.2 Currying
const curry = (fn) => {
const arity = fn.length;
return function curried(...args) {
return args.length >= arity
? fn.apply(this, args)
: (...more) => curried.apply(this, args.concat(more));
};
};
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3); // 6
sum(1, 2)(3); // 6
sum(1)(2, 3); // 6
Comments