ES6 Core Syntax
ES6 (ECMAScript 2015) was the largest syntax overhaul in JavaScript's history. It introduced template literals, arrow functions, classes, modules, and other features that form the foundation of modern JavaScript.
Template Literals
A string representation method using backticks (`).
const name = 'Alice';
const age = 30;
// Old way
const msg1 = 'Hello, ' + name + '! Age: ' + age;
// Template literal
const msg2 = `Hello, ${name}! Age: ${age}`;
// Expression interpolation
const result = `2 + 3 = ${2 + 3}`;
const status = `Status: ${age >= 18 ? 'adult' : 'minor'}`;
const func = `Length: ${name.length}`;
// Multiline strings
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
// Nested template literals
const items = ['apple', 'banana', 'cherry'];
const list = `Item list:
${items.map((item, i) => ` ${i + 1}. ${item}`).join('\n')}`;
Tagged Template Literals
// Tag function: processes the template literal directly
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1];
return result + (value ? `<mark>${value}</mark>` : '') + str;
});
}
const name = 'Alice';
const score = 95;
const msg = highlight`${name}'s score is ${score} points.`;
// <mark>Alice</mark>'s score is <mark>95</mark> points.
// Real-world: SQL injection prevention
function sql(strings, ...values) {
const escaped = values.map(v => escapeSQL(v));
return strings.reduce((query, str, i) => {
return query + (escaped[i - 1] ?? '') + str;
});
}
const username = "Alice'; DROP TABLE users; --";
const query = sql`SELECT * FROM users WHERE name = ${username}`;
// Safely escaped
// styled-components pattern (React CSS-in-JS)
const Button = styled.button`
background: ${props => props.primary ? '#007bff' : 'white'};
color: ${props => props.primary ? 'white' : '#007bff'};
padding: 0.5rem 1rem;
`;
Default Parameters
// Old way
function greet(name, greeting) {
name = name || 'Guest';
greeting = greeting || 'Hello';
return `${greeting}, ${name}!`;
}
// ES6 default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
greet(); // Hello, Guest!
greet('Alice'); // Hello, Alice!
greet('Bob', 'Hi'); // Hi, Bob!
// Expressions can be used as default values
function createUser(
name,
id = Math.random().toString(36).slice(2),
createdAt = new Date().toISOString()
) {
return { name, id, createdAt };
}
// Can reference earlier parameters
function makeRange(start, end = start + 10) {
return Array.from({ length: end - start }, (_, i) => start + i);
}
makeRange(5); // [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
makeRange(3, 7); // [3, 4, 5, 6]
// Only undefined triggers defaults (null does not)
function test(x = 'default') {
return x;
}
test(undefined); // 'default'
test(null); // null
test(0); // 0
test(''); // ''
Rest/Spread Operators in Depth
Rest Parameters
// Can only be used as the last parameter
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// Combined with other parameters
function logMessage(level, ...messages) {
console[level](...messages);
}
logMessage('log', 'Hello', 'World', '!');
logMessage('error', 'Error occurred:', new Error('Problem'));
// Destructuring with rest
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first: 1, second: 2, rest: [3, 4, 5]
const { name, age, ...others } = { name: 'Alice', age: 30, city: 'Seoul', job: 'Dev' };
// name: 'Alice', age: 30, others: { city: 'Seoul', job: 'Dev' }
Spread Operator
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const withMiddle = [...arr1, 10, ...arr2]; // [1, 2, 3, 10, 4, 5, 6]
const copy = [...arr1]; // shallow copy
// As function arguments
Math.max(...arr1); // 3
console.log(...arr1); // 1 2 3
// String spreading
const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
// Object spreading
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
const overridden = { ...obj1, b: 99, ...obj2 }; // { a: 1, b: 99, c: 3, d: 4 }
// Object copy (shallow copy)
const original = { name: 'Alice', settings: { theme: 'dark' } };
const copy = { ...original };
copy.name = 'Bob'; // no effect on original
copy.settings.theme = 'light'; // original.settings also changes! (reference)
// Real-world: immutable update pattern (React state)
const updateUser = (user, updates) => ({ ...user, ...updates });
const updated = updateUser(original, { name: 'Bob', email: 'bob@example.com' });
Arrow Functions in Depth
// Various forms
const double = x => x * 2;
const add = (a, b) => a + b;
const greet = () => 'Hello!';
const getObject = () => ({ name: 'Alice' }); // wrap objects in parentheses
// Multiple lines
const process = (data) => {
const filtered = data.filter(Boolean);
const mapped = filtered.map(x => x * 2);
return mapped;
};
// The this binding difference — the most important feature!
const counter = {
count: 0,
// Regular function: has its own this
incrementRegular: function() {
setInterval(function() {
this.count++; // this is undefined (strict mode) or window
console.log(this.count);
}, 1000);
},
// Arrow function: inherits this from enclosing scope
incrementArrow: function() {
setInterval(() => {
this.count++; // this is the counter object
console.log(this.count); // works correctly
}, 1000);
}
};
// Event handlers in classes
class Button {
constructor() {
this.clickCount = 0;
// Arrow function maintains this binding
this.handleClick = () => {
this.clickCount++;
console.log(`Clicked ${this.clickCount} times`);
};
}
}
// When NOT to use arrow functions
const obj = {
name: 'Alice',
// Bad: arrow functions have no own this
getName: () => this.name, // undefined
// Good
getName() {
return this.name; // 'Alice'
}
};
Shorthand Method Notation
// Shorthand for object methods
const user = {
name: 'Alice',
age: 30,
// Old way
greet: function() {
return `Hello, ${this.name}!`;
},
// Shorthand method
greet() {
return `Hello, ${this.name}!`;
},
// async method
async fetchData() {
return await fetch('/api/data');
},
// getter/setter
get info() {
return `${this.name} (${this.age})`;
},
set info(value) {
[this.name, this.age] = value.split(',');
}
};
// Property shorthand
const name = 'Alice';
const age = 30;
// Old way
const user1 = { name: name, age: age };
// Shorthand
const user2 = { name, age }; // shorthand when variable name matches key
// Computed property names
const prefix = 'get';
const dynamicObj = {
[`${prefix}Name`]() { return this.name; },
[`${prefix}Age`]() { return this.age; },
name: 'Bob',
age: 25
};
dynamicObj.getName(); // 'Bob'
for...of and Iterables
// for...of: iterate over iterables
const numbers = [1, 2, 3, 4, 5];
for (const num of numbers) {
console.log(num);
}
// Strings are also iterable
for (const char of 'hello') {
console.log(char); // h, e, l, l, o
}
// Map and Set
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const [key, value] of map) {
console.log(key, value);
}
// for...in vs for...of
const arr = [10, 20, 30];
arr.custom = 'extra';
for (const key in arr) {
console.log(key); // '0', '1', '2', 'custom' (all enumerable properties)
}
for (const value of arr) {
console.log(value); // 10, 20, 30 (values only, no 'custom')
}
// Use entries() when you need the index
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
Symbol
// Symbol: unique primitive value
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // false - always unique
// Used as object keys (avoids collisions)
const ID = Symbol('id');
const user = {
[ID]: 12345,
name: 'Alice'
};
user[ID]; // 12345
// Not included in JSON.stringify
// Not enumerable with for...in
// Global Symbol registry
const globalSym = Symbol.for('app.id');
const sameSym = Symbol.for('app.id');
console.log(globalSym === sameSym); // true
// Well-known Symbols
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
for (const num of new Range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
Map and Set
// Map: key-value pairs (any type can be a key, unlike plain objects)
const map = new Map();
map.set('name', 'Alice');
map.set(42, 'number key');
map.set({ id: 1 }, 'object key');
map.get('name'); // 'Alice'
map.has('name'); // true
map.size; // 3
map.delete('name');
map.clear();
// Initialize with constructor
const map2 = new Map([
['key1', 'value1'],
['key2', 'value2']
]);
// Iteration
for (const [key, value] of map2) {
console.log(key, value);
}
// Object to Map
const obj = { a: 1, b: 2 };
const mapFromObj = new Map(Object.entries(obj));
// Set: collection of unique values
const set = new Set([1, 2, 3, 2, 1]);
console.log([...set]); // [1, 2, 3] - duplicates removed
set.add(4);
set.has(3); // true
set.delete(1);
set.size; // 3
// Remove array duplicates
const unique = [...new Set([1, 2, 3, 2, 1, 3])]; // [1, 2, 3]
// Intersection, union, difference
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union = new Set([...a, ...b]); // union
const intersection = new Set([...a].filter(x => b.has(x))); // intersection
const difference = new Set([...a].filter(x => !b.has(x))); // difference
WeakMap and WeakSet
// WeakMap: keys must be objects, weak references (GC eligible)
const cache = new WeakMap();
function processUser(user) {
if (cache.has(user)) {
return cache.get(user);
}
const result = expensiveComputation(user);
cache.set(user, result);
return result;
}
// When the user object has no other references, it's automatically removed from WeakMap
// Prevents memory leaks!
// WeakSet: stores objects only, weak references
const visited = new WeakSet();
function visitPage(pageObj) {
if (visited.has(pageObj)) {
console.log('Already visited this page');
return;
}
visited.add(pageObj);
renderPage(pageObj);
}
// Map vs WeakMap
// Map: keys are not GC'd even when no longer referenced elsewhere (potential memory leak)
// WeakMap: entries are automatically removed when the key object is GC'd
Expert Tips
1. Creating a DSL with tagged templates
// HTML escaping
function safe(strings, ...values) {
const escaped = values.map(v =>
String(v).replace(/&/g, '&').replace(/</g, '<')
);
return strings.reduce((result, str, i) => result + (escaped[i-1] ?? '') + str);
}
const userInput = '<script>alert("xss")</script>';
const html = safe`<p>User input: ${userInput}</p>`;
// <p>User input: <script>alert("xss")</script></p>
2. Customizing type conversion with Symbol.toPrimitive
const temperature = {
celsius: 100,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.celsius;
if (hint === 'string') return `${this.celsius}°C`;
return this.celsius; // default
}
};
console.log(+temperature); // 100 (number hint)
console.log(`${temperature}`); // '100°C' (string hint)
console.log(temperature + 0); // 100 (default hint)
3. Using a Map like an object with Proxy
function createReactiveObject(initial = {}) {
const data = new Map(Object.entries(initial));
const listeners = new Set();
return new Proxy({}, {
get(_, key) {
if (key === 'subscribe') return (fn) => listeners.add(fn);
return data.get(key);
},
set(_, key, value) {
data.set(key, value);
listeners.forEach(fn => fn(key, value));
return true;
}
});
}
const state = createReactiveObject({ count: 0 });
state.subscribe((key, val) => console.log(`${key} = ${val}`));
state.count = 1; // prints 'count = 1'
state.count = 2; // prints 'count = 2'