Variable Declarations and Temporal Dead Zone
ES6 introduced let and const to address the scoping limitations of var. Unlike function-scoped var, these declarations respect block boundaries (curly braces) and exhibit temporal deadd zone behavior—accessing them before declaration throws ReferenceError rather than returning undefined.
function demonstrateScoping() {
console.log(hoisted); // undefined
var hoisted = 'I am hoisted';
// console.log(blocked); // ReferenceError: Cannot access before initialization
let blocked = 'I respect blocks';
if (true) {
const constant = 'immutable binding';
// constant = 'new value'; // TypeError
}
}
const prevents reassignment of the binding but doesn't guarantee deep immutability of objects.
Arrow Functions and Lexical Context
Arrow functions provide concise syntax and inherit this from enclosing scope, eliminating the need for self or bind() patterns.
class DataFetcher {
constructor(endpoint) {
this.endpoint = endpoint;
this.cache = [];
}
fetchItems() {
// Lexical `this` inherits from fetchItems
fetch(this.endpoint).then(response => {
this.cache.push(response.data);
});
// Traditional function loses context
setTimeout(function() {
console.log(this.cache); // undefined
}, 100);
}
}
Limitations include: no arguments object (use rest parameters), inability to use new operator, and incompatibility with generator functions.
Template Literals and Tagged Templates
Backtcik-delimited strings support interpolation and multiline content without concatenation operators.
const user = { handle: 'dev_user', reputation: 1500 };
const query = `SELECT * FROM contributors
WHERE username = '${user.handle}'
AND score > ${user.reputation}`;
Implementing tagged template functionality:
function interpolate(literals, ...expressions) {
let result = '';
literals.forEach((literal, index) => {
result += literal;
if (index < expressions.length) {
result += String(expressions[index]);
}
});
return result;
}
const platform = 'GitHub';
const year = 2024;
interpolate`Welcome to ${platform} in ${year}`;
Destructuring Patterns
ES6 enables unpacking values from arrays and objects into distinct variables.
// Array destructuring with defaults
const coordinates = [10, 20];
const [x = 0, y = 0, z = 0] = coordinates;
// Object destructuring with renaming
const apiResponse = { data: { items: [] }, status: 200 };
const { data: payload, status: httpCode } = apiResponse;
// Swapping without temp variable
let alpha = 1, beta = 2;
[alpha, beta] = [beta, alpha];
// Nested extraction
const metadata = { user: { profile: { name: 'Alex' } } };
const { user: { profile: { name: displayName } } } = metadata;
Iteration Protocols
The for...of loop consumes iterable objects (Arrays, Maps, Sets, Strings) by invoking the Symbol.iterator method, yielding values directly rather than indices.
const metrics = [98.6, 99.1, 97.8];
let aggregate = 0;
for (const reading of metrics) {
aggregate += reading;
}
Contrast with for...in which enumerates enumerable property keys (including prototype chain), and forEach which lacks break/continue control flow.
Collection Types
Set stores unique values of any type, useful for deduplication:
const telemetry = [12, 15, 12, 18, 15, 20];
const uniqueReadings = [...new Set(telemetry)];
const registry = new Set();
registry.add({ id: 1 }); // Different objects
registry.add({ id: 1 });
console.log(registry.size); // 2 (reference equality)
Map maintains insertion order and accepts any value as key:
const config = new Map();
const keyObj = { env: 'production' };
config.set(keyObj, { timeout: 5000 });
config.get(keyObj); // { timeout: 5000 }
Class Syntax and Inheritance
Syntactic sugar over prototype-based inheritance with clearer constructor and method definitions.
class Polygon {
constructor(height, width) {
this.dimensions = { height, width };
}
calculateArea() {
return this.dimensions.height * this.dimensions.width;
}
}
class Square extends Polygon {
constructor(side) {
super(side, side);
this.type = 'square';
}
static isSquare(instance) {
return instance instanceof Square;
}
}
Module Architecture
ES6 modules use static analysis for dependency resolution.
// Named exports
export const computeFactorial = n => n <= 1 ? 1 : n * computeFactorial(n - 1);
export const PI_APPROX = 3.14159;
// Default export
export default class Calculator {
constructor(precision) {
this.precision = precision;
}
}
// Consumption patterns
import Calc, { computeFactorial, PI_APPROX } from './math-utils.js';
import * as MathUtils from './math-utils.js';
Promise Implementation Mechanics
Promises represent eventual completion of asynchronous operations with three states: pending, fulfilled, or rejected.
const executor = (resolve, reject) => {
console.log('Executor runs synchronously');
setTimeout(() => resolve('completed'), 0);
console.log('Executor continues');
};
new Promise(executor).then(result => console.log(result));
console.log('Main thread continues');
// Output: Executor runs synchronously → Executor continues → Main thread continues → completed
Custom Promise implementation sketch:
class Deferred {
constructor(handler) {
this.state = 'pending';
this.value = null;
this.onResolve = [];
const settle = (status, val) => {
if (this.state === 'pending') {
this.state = status;
this.value = val;
if (status === 'fulfilled') {
this.onResolve.forEach(cb => cb(val));
}
}
};
try {
handler(
val => settle('fulfilled', val),
err => settle('rejected', err)
);
} catch (err) {
settle('rejected', err);
}
}
then(callback) {
if (this.state === 'fulfilled') {
callback(this.value);
} else {
this.onResolve.push(callback);
}
}
}
Async/Await and Event Loop
async functions implicitly return Promises. The await operator pauses execution until the Promise settles, yielding control to the event loop.
async function fetchSequential() {
console.log('Initiating requests');
const primary = await fetch('/api/primary'); // Microtask queued
console.log('Primary received');
const secondary = await fetch('/api/secondary');
return [primary, secondary];
}
// Execution order with setTimeout (macrotask) and Promise (microtask)
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('script');
// Output: script → promise → timeout
Error handling uses standard try/catch blocks rather than .catch() chains:
async function robustOperation() {
try {
const result = await riskyAsyncCall();
return result;
} catch (exception) {
return { error: exception.message };
}
}
Symbols and Private Properties
Symbols create unique property keys that avoid name collisions:
const internalId = Symbol('identifier');
const publicName = 'name';
const entity = {
[publicName]: 'Public Entity',
[internalId]: 'uuid-1234'
};
// Enumeration behavior
Object.keys(entity); // ['name']
Reflect.ownKeys(entity); // ['name', Symbol(identifier)]
Proxy Objects
Proxies intercept and customize fundamental object operations:
const validator = {
set(target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be numeric');
}
target[property] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 25; // Valid
// person.age = 'twenty-five'; // Throws TypeError