ES Modules, Proxy, Generators & Modern JavaScript
Senior engineers need to understand the features that power modern tooling and frameworks. ES modules are the foundation of tree shaking. Proxies power Vue's reactivity and MobX. Generators power Redux-Saga and async iteration. These aren't trivia â they're architecture.
ES Modules
Module Basics
// Named exports
export function formatDate(date) { ... }
export const API_URL = 'https://api.example.com';
// Default export (one per module)
export default class UserService { ... }
// Named imports
import { formatDate, API_URL } from './utils.js';
// Default import (any name)
import UserService from './services/user.js';
// Namespace import
import * as utils from './utils.js';
utils.formatDate(new Date());
// Rename
import { formatDate as format } from './utils.js';ES Modules vs CommonJS
| Feature | ES Modules | CommonJS |
|---|---|---|
| Syntax | import / export | require() / module.exports |
| Loading | Static (parsed at compile time) | Dynamic (evaluated at runtime) |
| Binding | Live bindings (read-only reference) | Value copy |
| Tree shaking | Yes (static analysis possible) | No (dynamic, can't analyze) |
| Top-level await | Yes | No |
| this | undefined | The exports object |
| Environment | Browser + Node | Node (originally) |
Live Bindings (Critical Difference)
// counter.js (ES Module)
export let count = 0;
export function increment() { count++; }
// app.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 â live binding, sees updated value!
// CommonJS equivalent would show 0 both times (value copy)Dynamic Imports
// Load module on demand (returns Promise)
const module = await import('./heavy-library.js');
module.doThing();
// Conditional loading
if (feature.enabled) {
const { analyze } = await import('./analytics.js');
analyze();
}
// Pattern: lazy-load based on user action
button.addEventListener('click', async () => {
const { openEditor } = await import('./editor.js');
openEditor();
});Dynamic imports are the foundation of code splitting in webpack, Vite, and Next.js.
Proxy & Reflect
Proxy creates a trap layer around any object, intercepting fundamental operations:
const handler = {
get(target, property, receiver) {
console.log(`Reading ${String(property)}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Writing ${String(property)} = ${value}`);
return Reflect.set(target, property, value, receiver);
},
};
const user = new Proxy({ name: 'Alice' }, handler);
user.name; // Logs: Reading name â "Alice"
user.age = 30; // Logs: Writing age = 30Proxy Traps
| Trap | Intercepts | Example Use |
|---|---|---|
get | Property read | Validation, logging, default values |
set | Property write | Validation, reactivity, tracking |
has | in operator | Custom containment checks |
deleteProperty | delete operator | Prevent deletion |
apply | Function call | Logging, memoization |
construct | new operator | Singleton, validation |
Real-World Patterns
// Reactive state (how Vue 3 works internally)
function reactive(target) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
if (oldValue !== value) {
notify(prop, value); // Trigger UI update
}
return true;
},
});
}
// Validation proxy
function validated(target, schema) {
return new Proxy(target, {
set(obj, prop, value) {
const rule = schema[prop];
if (rule && !rule(value)) {
throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
}
obj[prop] = value;
return true;
},
});
}
const user = validated({}, {
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
email: (v) => typeof v === 'string' && v.includes('@'),
});
user.age = 25; // OK
user.age = -5; // TypeError
user.email = 'bad'; // TypeError
// Auto-vivification (create nested objects on access)
function deepProxy() {
return new Proxy({}, {
get(target, prop) {
if (!(prop in target)) target[prop] = deepProxy();
return target[prop];
},
});
}
const config = deepProxy();
config.database.host.primary = 'localhost'; // No intermediate assignments neededGenerators & Iterators
Iterators â The Protocol
Any object with a [Symbol.iterator]() method that returns { next() â { value, done } } is iterable:
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
},
};
},
};
for (const n of range) console.log(n); // 1, 2, 3, 4, 5
[...range]; // [1, 2, 3, 4, 5]Generators â Pausable Functions
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
fib.next(); // { value: 0, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 2, done: false }
// Take first N values
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
[...take(fibonacci(), 8)]; // [0, 1, 1, 2, 3, 5, 8, 13]Async Generators â Streaming Data
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) return;
yield* data.items;
page++;
}
}
for await (const item of fetchPages('/api/products')) {
renderProduct(item);
}Symbol â Metaprogramming Keys
// Unique property keys (no collisions)
const ID = Symbol('id');
const user = { [ID]: 42, name: 'Alice' };
// Well-known symbols customize language behavior
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount} ${this.currency}`;
return this.amount;
}
[Symbol.iterator]() {
return [this.amount, this.currency][Symbol.iterator]();
}
}
const price = new Money(42, 'USD');
+price; // 42
`${price}`; // "42 USD"
const [amt, cur] = price; // [42, "USD"]Modern Features (ES2022-2024)
// Top-level await (ES2022)
const config = await fetch('/config.json').then(r => r.json());
// Array.at() â negative indexing
const last = [1, 2, 3].at(-1); // 3
// Object.hasOwn() â better than hasOwnProperty
Object.hasOwn(obj, 'key'); // true/false
// structuredClone â deep copy
const copy = structuredClone(complex);
// Array grouping (ES2024)
const grouped = Object.groupBy(users, (u) => u.role);
// { admin: [...], user: [...] }
// Promise.withResolvers (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();
// Set methods (ES2025)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // Set {1, 2, 3, 4}
a.intersection(b); // Set {2, 3}
a.difference(b); // Set {1}Interview Signal
Senior candidates demonstrate:
- Module system understanding â Live bindings, static analysis, why tree shaking needs ESM
- Proxy mechanics â How reactivity systems work, validation patterns, Reflect
- Iterator/generator fluency â Custom iterables, async generators for streaming
- Practical application â Dynamic imports for code splitting, Symbol for metaprogramming
- Modern JS awareness â Keeping up with the language (groupBy, Set methods, structuredClone)