Overview
ES6 (2015) and subsequent yearly releases transformed JavaScript from a scripting language into a serious application platform. The features below aren't just syntax sugar — they change how you model data (Map vs Object), handle null safety (optional chaining), and intercept object operations (Proxy). Understanding the semantic differences, not just the syntax, is what matters for senior-level interviews.
Architecture
NULL SAFETY CHAIN
════════════════════════════════════════════════════════
user?.address?.city // optional chaining: short-circuits to undefined
user?.getName?.() // optional method call
user?.roles?.[0] // optional bracket access
user?.address?.city ?? 'Unknown' // ?? = null/undefined only
user?.address?.city || 'Unknown' // || = also triggers on 0, '', false
Common gotcha:
const count = getCount() || 10 // WRONG: returns 10 if count is 0
const count = getCount() ?? 10 // RIGHT: returns 10 only if null/undefined
MAP VS OBJECT
════════════════════════════════════════════════════════
Object Map
────────────────────────── ──────────────────────────
Keys: strings + symbols Keys: any value (obj, fn, NaN)
No size property .size property
Prototype chain pollution No prototype by default
Not iterable directly Iterable (for...of, spread)
JSON.stringify support Must convert to serialize
Slightly faster reads Faster for frequent add/delete
WeakMap: keys must be objects, GC-eligible (no strong ref),
cannot iterate (keys can disappear), .size doesn't exist
Use case: storing private data per DOM node, memoizing without memory leaks
PROXY INTERCEPTS
════════════════════════════════════════════════════════
get(target, prop) → property read
set(target, prop, value) → property write
has(target, prop) → 'in' operator
deleteProperty → delete operator
apply(fn, thisArg, args) → function call
construct(target, args) → new operator
Powers: Vue 3 reactivity, validation proxies, infinite virtual arrays
Technical Implementation
Proxy for Validation and Reactivity
// Validation proxy: throw on invalid type assignment
function createTypedObject<T extends object>(target: T): T {
const types = Object.fromEntries(
Object.entries(target).map(([k, v]) => [k, typeof v])
);
return new Proxy(target, {
set(obj, prop: string, value) {
if (prop in types && typeof value !== types[prop]) {
throw new TypeError(`${prop} must be ${types[prop]}, got ${typeof value}`);
}
obj[prop as keyof T] = value;
return true;
},
});
}
const config = createTypedObject({ port: 3000, host: 'localhost', debug: false });
config.port = 8080; // OK
config.port = '8080'; // TypeError: port must be number, got string
// Reactive object (Vue 3 style)
function reactive<T extends object>(obj: T, onChange: (key: string, val: unknown) => void): T {
return new Proxy(obj, {
set(target, prop: string, value) {
target[prop as keyof T] = value;
onChange(prop, value);
return true;
},
});
}
const state = reactive({ count: 0 }, (key, val) => console.log(`${key} → ${val}`));
state.count = 5; // logs: count → 5
Custom Iterable with Symbol.iterator
// LinkedList that works with for...of, spread, destructuring
class LinkedList<T> {
private head: { val: T; next: { val: T; next: any } | null } | null = null;
private _size = 0;
push(val: T): void {
this.head = { val, next: this.head };
this._size++;
}
get size() { return this._size; }
[Symbol.iterator](): Iterator<T> {
let current = this.head;
return {
next() {
if (current === null) return { value: undefined as any, done: true };
const value = current.val;
current = current.next;
return { value, done: false };
},
};
}
}
const list = new LinkedList<number>();
list.push(3); list.push(2); list.push(1);
for (const n of list) console.log(n); // 1, 2, 3
const arr = [...list]; // [1, 2, 3]
const [first, second] = list; // destructuring works too
WeakMap for Private Data (Memory-Safe)
// WeakMap: associates data with objects without preventing GC
// When the DOM node is removed, its metadata is automatically GC'd
const nodeMetadata = new WeakMap<Element, { clickCount: number; createdAt: Date }>();
function trackNode(el: Element): void {
nodeMetadata.set(el, { clickCount: 0, createdAt: new Date() });
el.addEventListener('click', () => {
const meta = nodeMetadata.get(el)!;
meta.clickCount++;
});
}
function getClickCount(el: Element): number {
return nodeMetadata.get(el)?.clickCount ?? 0;
}
// When el is removed from DOM and dereferenced, WeakMap entry is GC'd automatically.
// A regular Map would hold a strong reference → memory leak.
Interview Preparation
Q: What is the difference between Map and a plain object for key-value storage?
Map allows any value as a key (objects, functions, NaN, primitives), preserves insertion order reliably, has a .size property, is directly iterable, and has better performance for frequent additions and deletions. A plain object's prototype chain means inherited properties like toString and constructor can shadow user keys — you need Object.create(null) to avoid this. Objects are better when: keys are known strings, you need JSON serialization, or you're working with property access patterns that benefit from V8's hidden class optimization. Map is better when: key types are dynamic, you need .size, or you're doing frequent add/delete operations.
Q: When would you use WeakMap instead of Map?
WeakMap when the key is an object whose lifetime you don't control and you want the associated data to be garbage-collected when the object is no longer referenced elsewhere. Classic use cases: caching computed values per DOM node (node removed → cache entry GC'd), storing private state for class instances, memoizing functions where the argument is an object. A regular Map holds a strong reference to its keys — if you associate metadata with every DOM node using a Map and nodes are removed from the DOM but the Map persists, you have a memory leak. WeakMap entries are automatically cleaned up.
Q: How does Proxy differ from Object.defineProperty for reactivity?
Object.defineProperty intercepts access to specific, known properties defined at initialization. Vue 2 used it — it couldn't detect property additions, array index assignments (arr[0] = val), or array length changes, requiring special $set methods. Proxy wraps the entire object and intercepts all operations generically — including property additions, deletions, array mutations, and in operator checks. Vue 3 switched to Proxy-based reactivity to eliminate these edge cases. Proxy is more powerful but requires a modern browser (no IE11 support), and Proxy wrappers cannot be polyfilled.
Q: What is the difference between ?? and ||?
|| (logical OR) returns the right operand if the left is any falsy value: false, 0, '', null, undefined, NaN. This means 0 || 'default' returns 'default' — likely a bug if 0 is a valid value. ?? (nullish coalescing) returns the right operand only if the left is null or undefined. 0 ?? 'default' returns 0. Use ?? for default values to avoid accidentally replacing legitimate falsy values like 0, empty string, or false.
Learning Resources
- MDN JavaScript Reference — authoritative
- ES2015+ compatibility table
- JavaScript.info — modern JS deep dives
- You Don't Know JS — Kyle Simpson — scope, closures, types, async