๐ Modern JavaScript: ECMAScript 6 (ES6, or ES2015) is a major update to the JavaScript standard released in 2015
These Study Room materials were written and edited by keysub with the assistance of AI.
๐ 1. ES6โ7 through ES12: update history (click to expand)
1. es6
- New variable declarations:
let, which has block scope, andconstfor declaring constants were added in place of the existingvar, making variable management safer. - Template literals: Backticks (`) let you easily insert variables into strings and include line breaks.
- Destructuring: Extract only the values you need from an object or array and assign them to variables concisely.
- Arrow functions: The
=>syntax makes function declarations more concise, andthisbinding is determined lexically. - Classes: Class syntax was introduced to make object-oriented programming more intuitive.
- Promise: Promise became a standard object for escaping callback hell.
- Spread syntax (...): Use it to create or combine copies without modifying the original array or object (preserving immutability).
- Modules (import / export): The standard of the modern JavaScript ecosystem: split code across files and assemble it. Native module support lets you organize code into independent modules using
importandexport. - Map objects
- Set objects
2. es7
- Array.includes() (check whether a specific element exists in an array)
3. ES8 (ES2017)
- async / await (the standard for asynchronous processing)
- Object.values() / Object.entries(): Often used to turn an object's values into an array, or to iterate over its keys and values together.
6. ES11 (ES2020)
Preventing those frustrating errors These two features are lifesavers in production: they prevent a TypeError from crashing the screen.
- Optional chaining (?.): When accessing nested objects, it safely returns
undefinedwithout throwing an error if an intermediate value isnullorundefined. (Example:user?.profile?.address) - Nullish coalescing (??): Uses the value on the right only when the left side is
nullorundefined. It completely solves a bug with the existing||operator, which treats0as falsy. - Promise.allSettled() (returns the results of all Promises in an array)
7. ES12 (ES2021)
- String.replaceAll() (replace every occurrence of a pattern in a string)
- In the past, replacing every occurrence of a word required a regular expression (
/word/g). Now this method makes replacing all occurrences intuitive and clean.
๐ก Concepts, focusing on what has changed
๐ฆ 1. Variables
1.1 Variable declarations
The differences among var, let, and const mainly concern reassignment, redeclaration, and scope (the region where a variable is valid).
Modern JavaScript rarely uses var.
[Summary of Key Differences]
Scope var let const
Reassignment Allowed Allowed Not allowed (constant)
Duplicate Declaration Allowed (risky) Not allowed Not allowed
Scope Function-based (Function) Block-based ({ }) Block-based ({ })
Hoisting Usable before declaration (undefined) Cannot be used before declaration (error) Cannot be used before declaration (error)
1.2 Examples
๐จ 1.2.1 Redeclaration
var user = "Chul-soo";
var user = "Young-hee";
console.log(user); // "Young-hee"
// [let / const] - Duplicate declarations not allowed
let score = 100;
// let score = 200; // Error: SyntaxError (variable already declared)
const ID = "admin";
// const ID = "guest"; // Error: SyntaxError (variable already declared)
const ID_OBJ = {id1: 1, id2: 2};
ID_OBJ.id1 = 3;
console.log(ID_OBJ); // Object {id1: 3, id2: 2} // Even when declared with `const`, properties and property values of objects or arrays can be modified
๐ก๏ธ 1.2.2 Scope A var declared inside a function can be used anywhere within that function, but let and const can never escape the braces { } (block) that enclose them.
var declared inside a function can be used anywhere within that function, but let and const can never escape the braces { } (block) that enclose them.if (true) {
var varName = "I am var";
}
console.log(varName); // "I am var" (Accessible even outside the block)
// [let / const] - Block scope
if (true) {
let letName = "I am let";
const constName = "I am const";
console.log(letName); // "I am let" (Of course, this is possible within the block)
}
1.3 Data types
๐ 1.3.1 Symbol: a unique identifier that is never duplicated.
- Uniqueness: Values created with
Symbol()are treated as different even if their descriptions look the same. - Immutability: Once created, a Symbol cannot be changed.
- Concealment: Symbols are not found by ordinary loops (
for...in) orObject.keys(), making them useful for creating private object properties.
var sym = Symbol();
// 1. Symbol creation (The string inside the parentheses is merely a 'description')
const sym1 = Symbol('id');
const sym2 = Symbol('id');
console.log(sym1 === sym2); // false (Even if the descriptions are the same, the values are different)
// 2. Used as an object key
const user = {
name: "John",
[sym1]: 123 // Assign a symbol as a key
};
console.log(user[sym1]); // 123
// 3. Checking for encapsulation (The key is not visible using standard methods)
for (let key in user) {
console.log(key); // Only "name" is printed; sym1 is skipped
}
console.log(Object.keys(user)); // ["name"]
- When creating an object, putting square brackets [] in the key position means that the result of evaluating the expression inside as code will be used as the key. (This is also a modern JavaScript feature.)
๐ฌ 1.3.2 Template literals
1.3.2.1 The traditional quotes (' ', " ") are replaced with a backtick (`).
1.3.2.2 Placeholder: Use the ${...} syntax to insert a variable or expression directly in the middle of a string.
const name = "Jquery";
const version = "1.0";
// 1. Traditional method (ES5) โ Using quotes and the + operator
const oldWay = "The current library is " + name + " and the version is " + version + ".";
// 2. Template literal method (ES6) - Back
const newWay = `The current library is ${name} and the version is ${version}.`;
// 3. Line break example
const multiLine = `
This is an example that
outputs multiple lines
exactly as they are.
`;
console.log(newWay);
console.log(multiLine);
1.3.2.3 Usage examples
// 1. Traditional method
var html = "<div class='user'>" +
"\n <h1>Name: " + this.name + "</h1>" +
"\n <p>Age: " + this.age + "</p>" +
"\n</div>";
// 2. Template Literal
const htmlTemplate = `
<div class="user">
<h1>Name: ${this.name}</h1>
<p>Age: ${this.age}</p>
</div>
`;
๐งฉ 2. Objects: ES6 introduced property shorthand and computed properties for writing objects more concisely.
2.1 Basic object concepts
๐ 2.1.1 Create an object with an object literal
var me = {name : "keysub", age: 11};
๐ 2.1.2 Read or write values in an object
me.name or me["age"]
โ 2.1.3 Assigning a value to a nonexistent property adds a new property:
me.hobby = "Swimming";
โ 2.1.4 Check whether a property exists with the in operator:
console.log("hobby" in me); // true
2.2 Objects and collections (arrays, Set, Map)
โ๏ธ 2.2.1 Property shorthand
If the variable name and object key are the same, you only need to write it once.
const name = "Younghee";
const age = 30;
// ES5
const user5 = { name: name, age: age };
// ES6 (shorthand)
const user6 = { name, age };
console.log(user6); // { name: "Younghee", age: 30 }
โก 2.2.2 Method shorthand
You can omit the function keyword when defining a function.
const dog = {
name: "Mungmungi",
// ES5: bark: function() { ... }
bark() {
console.log("Woof woof!");
}
};
๐งฎ 2.2.3 Computed property names
You can set an object's key dynamically using a variable or expression.
const prefix = "user";
const i = 1;
const member = {
[`${prefix}_${i}`]: "Hong Gil-dong" // A key named 'user_1' is created
};
console.log(member.user_1); // "Hong Gil-dong"
2.3 Set (a container that does not allow duplicates)
๐ซ 2.3.1 Arrays in the past and how Set differs
In the past, arrays were the default way to store multiple values. But arrays allow duplicates, so code could get messy when asked to "remove all duplicate data." Set is a container that never allows duplicates by design. Add the same value multiple times, and only one copy remains.
๐ ๏ธ 2.3.2 Creating a Set (constructor)
Creating a Set is simple: use the new keyword.
โ Create an empty container
const mySet = new Set();
โก Create it with data from the start (most common!) Pass iterable data, such as an array, to the constructor, and it removes duplicates to create a Set.
// Let's try passing an array that contains duplicate numbers 2 and 3.
const numbers = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log(numbers);
// Output: Set(4) { 1, 2, 3, 4 } <- Only one instance each of the duplicate elements 2 and 3 remains!
๐ช 2.3.3 Set's useful tools (methods and properties)
Set provides intuitive methods for working with data.
โ Add data (add)
mySet.add("apple");
mySet.add("banana");
mySet.add("apple"); // Ignored because "apple" is already in the set!
console.log(mySet); // Output: Set(2) { 'apple', 'banana' }
๐ก Tip: You can chain calls like mySet.add(1).add(2);.
โก Check for data (has)
Checks whether a specific value exists in the container. (It serves the same role as an array's includes, but is much faster!)
console.log(mySet.has("apple")); // Output: true
console.log(mySet.has("watermelon")); // Output: false
โข Delete data (delete / clear)
mySet.delete("banana"); // Removes only "banana"
mySet.clear(); // Empties the entire set! (Initializes it)
โฃ Check the size (the size property)
Arrays use .length to get their length; Sets always use .size.
const animalSet = new Set(["dog", 'cat', "rabbit"]);
console.log(animalSet.size); // Output: 3
๐ฏ 2.3.4 The perfect time to use Set in practice
When is Set most commonly used in practice? When removing duplicates from an array. Previously, removing duplicates required complex filtering or loops. Combining Set with spread syntax (...) reduces it to a single line.
const dirtyArray = ["Hong Gil-dong", "Lee Sun-shin", "Hong Gil-dong", "Yoo Gwan-soon", "Lee Sun-shin"];
// 1. Convert the array to a Set to remove duplicates,
// 2. Then use square brackets [] and ... to wrap it back into an array.
const cleanArray = [...new Set(dirtyArray)];
console.log(cleanArray);
// Output: [ 'Hong Gil-dong', 'Lee Sun-shin', 'Yoo Kwan-soon' ]
2.4 Map (a locker that stores key-value pairs)
๐ก 2.4.1 Why Map was introduced
In JavaScript, ordinary objects ({}) can also act as lockers for keys and values, but Map is an upgraded locker designed to address the drawbacks of objects.
๐๏ธ 2.4.2 Working with Map
2.4.2.1 Creating a Map
โ Create an empty locker
const myMap = new Map();
โก Create it with data from the start
Map requires keys and values to come in pairs, so initialize it with small [key, value] arrays inside an array.
const userMap = new Map([
["name", "Hong Gil-dong"],
["age", 25],
["role", "admin"]
]);
2.4.2.2 Essential methods for working with Map
Add or update data (set)
Put an item (Value) in a locker (Key). If the key already exists, its value is overwritten (updated).
myMap.set("color", "red");
myMap.set("size", 100);
myMap.set("color", "blue"); // The value of the "color" key changes from 'red' to "blue"!
// Chaining is also possible, just like with a Set.
myMap.set("a", 1).set("b", 2);
Retrieve data (get)
Pass a key to get its value. If the key does not exist, undefined is returned.
console.log(myMap.get("color")); // Output: 'blue'
Check for data (has)
Checks whether the key exists. (It looks only for the key, regardless of the value.)
console.log(myMap.has("size")); // Output: true
Delete data and check the size (delete, clear, size)
myMap.delete("size"); // Delete the "size" key and its value
console.log(myMap.size); // Check the number of data pairs currently stored
myMap.clear(); // Clear the entire map
๐ก 2.4.2.3 Why use Map instead of an object ({})?
โ You can use anything as a key! (the biggest difference) Ordinary objects require keys to be strings. Even if you use a number, it is converted to a string internally. Map, however, lets you use numbers, booleans (true/false), and even functions or other objects as keys.
const map = new Map();
// 1. Using a number as a key
map.set(1, "This is number 1");
// 2. Using another object as a key (Magic that's absolutely impossible with a regular object {}!)
const kim = { id: 101, name: "Kim Cheol-su" };
map.set(kim, "This user is a VIP member.");
console.log(map.get(kim)); // Output: "This user is a VIP member."
โก You can get the size immediately.
With an ordinary object, you need a calculation such as Object.keys(obj).length to find the number of entries. With Map, just use map.size.
โข It remembers insertion order perfectly. With an ordinary object, iterating later does not guarantee that entries will come out in insertion order. Map always returns them in the order they were added.
๐๏ธ 2.5 Object.keys, values, entries: Convert object data to arrays for processing
2.5.1 Object.keys(): Extract only an object's keys into an array
๐ Useful for checking an object's properties or looping through its keys.
const user = {
name: "Son Heung-min",
age: 32,
team: "Tottenham"
};
const keys = Object.keys(user);
console.log(keys);
// Output: ["name", 'age', "team"]
// Application: Iterating through a key-value array
keys.forEach(key => {
console.log(`${key}: ${user[key]}`);
});
2.5.2 Object.values(): Extract only an object's values into an array (ES8)
๐ Use this when you do not need the key names and only need the data they contain.
const scores = {
korean: 90,
english: 85,
math: 100
};
const values = Object.values(scores);
console.log(values);
// Output: [90, 85, 100]
// Application: Calculating the total score
const totalScore = values.reduce((acc, curr) => acc + curr, 0);
console.log(`Total Score: ${totalScore}`); // Total Score: 275
2.5.3 Object.entries(): Turn keys and values into [Key, Value] pairs (ES8)
๐ฏ Converts an object into a two-dimensional array of [key, value] pairs. Essential for converting to a Map or performing complex transformations.
const user = {
id: 1,
role: "admin",
status: "active"
};
const entries = Object.entries(user);
console.log(entries);
/* Output:
[
["id", 1],
["role", "admin"],
["status", "active"]
]
*/
// Application: Combining the `for...of` loop with destructuring assignment (Recommended for real-world use๐)
for (const [key, value] of Object.entries(user)) {
console.log(`The value of the ${key} field is ${value}.`);
}
2.5.4 Practical tip: Convert an object back to an object (Object.fromEntries)
๐ Use this when you want to turn data converted to an array back into an object.
const arr = [
['name', 'Lee Kang-in'],
['age', 23]
];
const obj = Object.fromEntries(arr);
console.log(obj);
// Output: { name: "Lee Kang-in", age: 23 }
๐ 2.6 includes: Check whether a value is present (Array & String)
2.6.1 includes on arrays (Array.prototype.includes) - ES7
๐ Completely replaces the old, cumbersome indexOf() !== -1 approach and returns true / false.
indexOf() !== -1 approach and returns true / false.const fruits = ['apple', 'banana', 'mango'];
// [Old ES5 approach]
// `indexOf` returned the index number (0, 1, 2...) if the value was found, and -1 if it wasn't.
// Therefore, cumbersome code was required to check whether the result was -1 or not.
if (fruits.indexOf('banana') !== -1) {
console.log("There is a banana!");
}
// [Latest ES7 method] ๐
// If you simply ask, "Does this contain it?", it gives a clear true/false answer.
if (fruits.includes('banana')) {
console.log("There is a banana!");
}
// ๐ก (Important difference) Finding NaN (Not a Number)
// indexOf cannot find NaN, but includes finds it accurately.
const arr = [1, 2, NaN];
console.log(arr.indexOf(NaN)); // -1 (not found)
console.log(arr.includes(NaN)); // true (found)
2.6.2 includes on strings (String.prototype.includes) - ES6
๐ You can easily check whether a specific word appears in a long text (string).
const sentence = "Hello, I'm studying JavaScript.";
console.log(sentence.includes("JavaScript")); // true
console.log(sentence.includes("Python")); // false
// ๐ก Tip: You can specify the "starting position for the search" as the second argument.
// Checks for "Hello" starting from index 10.
console.log(sentence.includes("Hello", 10)); // false
2.6.3 Practical tip: Simplify multiple OR conditions
โ๏ธ When an if statement has an overly long || (OR) expression, using an array and includes can dramatically shorten the code.
if statement has an overly long || (OR) expression, using an array and includes can dramatically shorten the code.const myFruit = 'apple';
// ๐จ [Bad Example] The code becomes infinitely long as conditions increase
if (myFruit === 'apple' || myFruit === 'banana' || myFruit === 'mango') {
console.log("These are the fruits in the fruit basket.");
}
// โ
[Using `includes`] Group the items to be checked into an array (Recommended for real-world use๐)
const bucket = ['apple', 'banana', 'mango', 'grape', 'orange'];
if (bucket.includes(myFruit)) {
console.log("These are the fruits in the fruit basket.");
}
โ๏ธ 3. Destructuring
3.1 Object destructuring:
When extracting data from an object, variables are created based on the key names.
const user = {
name: "Son Heung-min",
age: 32,
team: "Tottenham"
};
// [ES5 approach] Create and assign variables one by one
var name = user.name;
var age = user.age;
// [ES6 approach] Create and assign variables all at once
const { name, age, team } = user;
console.log(name); // "Son Heung-min"
console.log(team); // "Tottenham"
// What if you want to rename a variable? (name: newName)
const { name: userName, age: userAge } = user;
console.log(userName); // "Son Heung-min"
3.2 Array destructuring:
Arrays have no keys, so variables are created based on their order (index).
const colors = ["Red", "Green", "Blue"];
// [ES5 approach] Create and assign variables one by one
var first = colors[0];
var second = colors[1];
var third = colors[2];
// [ES6 Style] Using square brackets []
const [first, second, third] = colors;
console.log(first); // "Red"
console.log(second); // "Green"
// When you want to skip a specific element (using commas)
const [first_color , , third_color] = colors;
console.log(third_color); // "Blue"
3.3 Where it shines in practice
๐ผ 3.3.1 Passing an object as a function parameter
Instead of receiving the whole object and accessing properties with dot notation, you can name just the data you need.
function printUser({ name, age }) {
console.log(`My name is ${name}, and I am ${age} years old.`);
}
const person = { name: "Lee Kang-in", age: 23, city: "Paris" };
printUser(person); // "Name is Lee Kang-in, age is 23."
๐ก๏ธ 3.3.2 Setting default values:
You can specify a default value to use when the object does not contain the property.
const { name, hobby = "soccer" } = { name: "Kim Min-jae" };
console.log(hobby); // Since the object doesn't have a "hobby" property, the default value "soccer" is printed
๐ ๏ธ 4. Functions
4.1 Create a function with a function declaration:
A function name must follow the function keyword. (Feature: hoisting applies.)
function keyword. (Feature: hoisting applies.)sayHello(); // Executes successfully! (Hoisting)
function sayHello() {
console.log("Hello!");
}
4.2 Function literals (function expressions)
A way to assign a function to a variable as if it were a value. (Feature: it is not hoisted.)
// sayHi(); // Throws an error!
const sayHi = function() {
console.log("Nice to meet you!");
};
sayHi(); // Can only be called after declaration
4.3 Arrow functions
4.3.1 Differences from function expressions
4.3.1.1 ๐ฆ Traditional approach (function expression)
const add = function(a, b) {
return a + b;
};
4.3.1.2 ๐ Arrow function
const add = (a, b) => {
return a + b;
};
// If there is only one argument, () can be omitted
const add1 = a => { return a + 1; };
// Cannot omit () when there are no arguments
const add2 = () => { return 1 + 2; };
// If the only statement inside a function is `return`, the curly braces and `return` can be omitted
const add3 = a => a + 1;
// Even if the only statement inside a function is `return`, if the return value is an object literal, it must be enclosed in parentheses ()
const f = (a, b) => ({ x: a, y: b });
๐ 4.3.2 The real difference in how they work: how this is determined (key point ๐)
this is determined (key point ๐)4.3.2.1 ๐ฆ this in traditional functions: "Who called me?" (dynamic binding)
In a traditional function, this changes depending on who calls it when the function is executed.
const user = {
name: "Hong Gil-dong",
sayHi: function() {
console.log(this.name); // Outputs correctly: Hong Gil-dong
// ๐จ Problem occurs: A function that greets the user after 1 second is created inside this function
setTimeout(function() {
// Since this inner function is treated as having been called by the browser (window), it loses `this`.
console.log(`1 second later: Hello, I'm ${this.name}`);
}, 1000);
}
};
user.sayHi();
// Output: Hong Gil-dong
// Output 1 second later: Hello, I'm undefined <-- ๐ฅ Error! Lost `this`!
4.3.2.2 ๐ this in arrow functions: "Where is my birthplace?" (lexical binding)
An arrow function uses the this value of the surrounding environment where it was created.
const user = {
name: "Hong Gil-dong",
sayHi: function() {
console.log(this.name); // Outputs correctly: Hong Gil-dong
// โ
Solution: Use an arrow function
setTimeout(() => {
// The arrow function inherits the `this` (user) from the outer `sayHi` function.
console.log(`1 second later: Hello, I'm ${this.name}`);
}, 1000);
}
};
user.sayHi();
// Output: Hong Gil-dong
// Outputs after 1 second: Hello, I'm Hong Gil-dong <-- ๐ Works perfectly!
๐ ๏ธ 4.3.3 Using arrow functions
๐ฆ Scenario: Customer data received from a server (raw data)
const users = [
{ id: 1, name: "Hong Gil-dong", age: 25, isVip: true },
{ id: 2, name: "Lee Sun-shin", age: 40, isVip: false },
{ id: 3, name: "Yoo Gwan-soon", age: 18, isVip: true },
{ id: 4, name: "Gang Gam-chan", age: 35, isVip: false }
];
1. map: "data-processing factory" (change the shape)
// Iterate through the `users` array and create a new array containing only the names (`u.name`) of each user.
const names = users.map(u => u.name);
console.log(names);
// Output: ["Hong Gil-dong", "Lee Sun-shin", "Yoo Gwan-soon", "Kang Gam-chan"]
2. filter: "data sieve" (keep only items that match a condition)
// Iterate through the `users` array and keep only the users for whom `isVip` is true.
const vipUsers = users.filter(u => u.isVip === true);
console.log(vipUsers);
/* Output:
[
{ id: 1, name: 'Hong Gil-dong', age: 25, isVip: true },
{ id: 3, name: 'Yoo Gwan-soon', age: 18, isVip: true }
]
*/
๐ 3. The practical champion: chaining
const vipNames = users
.filter(u => u.isVip) // Step 1: Filter out only VIPs first
.map(u => u.name); // Step 2: Extract only the names from the remaining data
console.log(vipNames);
// Output: ["Hong Gil-dong", "Yoo Gwan-soon"]
4.4 Rest parameters:
Syntax that gathers an unspecified number of arguments into a single array
4.4.1 Basic example: Receive a function's arguments as an array
function sumAll(...numbers) {
// `numbers` becomes an array of [1, 2, 3, 4, 5].
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sumAll(1, 2)); // 3
console.log(sumAll(1, 2, 3, 4, 5)); // 15
4.4.2 Using it with ordinary parameters (it must always be last)
function showGrade(name, ...scores) {
console.log(`Number of scores for ${name}: ${scores.length}`);
console.log(scores); // [90, 85, 100]
}
showGrade("Cheol-su", 90, 85, 100);
4.4.3 Rest in destructuring
// โ Usage with arrays
const [first, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2, 3, 4, 5]
// โก Usage with objects
const user = { id: 1, name: "Young-hee", age: 20, city: "Seoul" };
const { id, ...userInfo } = user;
console.log(id); // 1
console.log(userInfo); // { name: "Young-hee", age: 20, city: "Seoul" }
4.5 Spread syntax:
Unpacks an "array or object container" and spreads its contents out as individual items
4.5.1 Spread syntax with arrays
const parts = ["shoulder", "knee"];
const body = ["head", ...parts, "feet"];
console.log(body); // ["head", "shoulders", 'knees', "feet"]
// Copy an array (creates a completely new array)
const original = [1, 2, 3];
const copy = [...original];
4.5.2 Spread syntax with objects
const user = { name: "Chul-soo", age: 20 };
const updatedUser = { ...user, city: "Seoul" };
console.log(updatedUser); // { name: "Chul-soo", age: 20, city: "Seoul" }
4.5.3 Distinguishing Rest (gathering) from Spread (unpacking)
They look the same (...), but their purposes are opposite.
* Rest: Takes multiple arguments and makes one array. (Function parameter list)
* Spread: Splits one array/object into multiple values. (Function calls or outside declarations)
4.6 Shallow copy and deep copy
๐ 4.6.1 Shallow copy
Copies only the top level of an object; nested objects inside it are copied only as references (addresses).
const original = {
name: "Chul-su",
skills: ["JS", "HTML"] // Nested array (object)
};
const copy = { ...original }; // Shallow copy
copy.name = "Young-hee"; // Modify values at level 1
copy.skills.push("CSS"); // Modify values at level 2 (nested object)
console.log(original.name); // "Chul-soo" (Level 1 remains unaffected)
console.log(original.skills); // ["JS", 'HTML', "CSS"] (The original is also changed!)
๐ 4.6.2 Deep copy
Copies every level of an object into entirely new memory. (The original and copy are completely separate.)
const original = {
name: "Chul-su",
skills: ["JS", "HTML"]
};
// Use `structuredClone`, the modern browser standard (recommended)
const deepCopy = structuredClone(original);
deepCopy.skills.push("React");
console.log(original.skills); // ["JS", "HTML"] (The original is preserved!)
console.log(deepCopy.skills); // ["JS", 'HTML', "React"] (Only the copy is modified)
4.7 Closures (reference):
๐ A function carries a "backpack" that remembers the environment (variables) of the place where it was created
4.7.1 A normal world without closures
function sayHello() {
let name = "Hong Gil-dong"; // Create a variable inside the function
console.log(name);
}
sayHello(); // Outputs "Hong Gil-dong"
// Since the function has finished executing at this point, the variable `name` is completely destroyed.
4.7.2 Create an inner function inside a function and return it to preserve variables
function makeCounter() {
let count = 0; // 1. Variable in the outer function
// 2. Create an inner function and return it to the outer function.
return function() {
count = count + 1; // 3. Uses the variable `count` from the home environment.
return count;
};
}
const myCounter = makeCounter();
// The execution of `makeCounter()` has ended, and the `count` variable should be destroyed, but it continues to work!
console.log(myCounter()); // Output: 1
console.log(myCounter()); // Output: 2
console.log(myCounter()); // Output: 3
๐๏ธ 5. Introducing classes
5.1 ES5: Constructor functions and the new operator
โ๏ธ 5.1.1 How it works:
function Person(name, age) {
this.name = name;
this.age = age;
}
// The method is explicitly added to the prototype
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name + ".");
};
// Create an instance using `new`
var user1 = new Person("Kim Cheol-su", 25);
user1.sayHello(); // Output: Hello, my name is Kim Cheol-su.
5.2 ES6: Introduction of the class keyword
๐ 5.2.1 How it works:
class Person {
// [Element 2: Properties] Data that must never be exposed externally (private fields)
#name;
// [Element 1: Constructor] Initializing properties
constructor(name) {
// This does not assign a value directly to the `this.#name` variable; instead, it calls the setter.
this.name = name;
}
// [Element 4: Getter]
get name() {
return this.#name;
}
// [Element 4: Setter] Add a setter if you want to allow the name to be changed from outside
set name(value) {
this.#name = value;
}
// [Element 3: Method] Methods are automatically added to the prototype.
sayHello() {
// Using `this.name` here automatically calls the getter to retrieve the value
console.log(`Hello, my name is ${this.name}.`);
}
}
// The method for creating an instance is the same
const user = new Person("Lee Young-hee");
user.sayHello(); // Output: Hello, my name is Lee Young-hee.
// Call the setter
user.name = "Chul-soo";
// Call the getter
console.log(user.name); // Output: Cheol-su
// Attempting to access a private field directly (not allowed outside the class)
// console.log(user.#name); // ๐ฅ Error!
๐ฆ 6. Modules: Syntax for splitting and assembling code by file (import / export)
6.1 Named export & import (exporting / importing multiple named items)
๐งฉ Use this to export multiple functions or variables from one file. They must be imported with braces { } using their original names.
[math.js] - the file that creates the components (module)
// Prepend `export` to the declaration to export it externally.
export const PI = 3.141592;
export function add(a, b) {
return a + b;
}
export const multiply = (a, b) => a * b;
// ๐ก (Note) You can also group them together and export them all at once at the very bottom of the file.
// export { PI, add, multiply };
[app.js] - the file that imports and uses the components
// Just like destructuring assignment, you can import only what you need by enclosing them in curly braces {}.
import { PI, add } from './math.js';
console.log(PI); // 3.141592
console.log(add(10, 20)); // 30
// ๐ก If you want to rename the imported module, use the `as` keyword.
import { multiply as multi } from './math.js';
console.log(multi(2, 5)); // 10
6.2 Default export & import (exporting / importing one main item per file)
๐ Use this when a file has one main class or function. You can import it under any name you choose, without braces { }.
[User.js] - the file that creates the components
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, I am ${this.name}.`);
}
}
// Specify the `User` class as the default module for this file.
export default User;
[main.js] - the file that imports and uses the components
// Modules exported by default can be imported using any name without parentheses.
import Customer from './User.js'; // Import User under the name "Customer"
const user1 = new Customer("Hong Gil-dong");
user1.greet(); // Hello, I am Hong Gil-dong.
6.3 Import everything at once with * (wildcard)
๐ฆ If there are too many exported modules to list one by one, import the entire file as a single object.
[app.js]
// Imports all exports from math.js and places them into a single object named 'MathUtils'.
import * as MathUtils from './math.js';
// Accesses them via the object's properties (dot notation).
console.log(MathUtils.PI);
console.log(MathUtils.add(5, 5));
6.4 Linking modules in HTML (important caveat ๐จ)
๐ To use import / export in a browser (HTML), the script tag must include the type="module" attribute.
type="module" attribute.[index.html]
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Module Test</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>
โณ 7. Promise: The standard object for asynchronous processing that escapes callback hell
7.1 The three states of a Promise
๐ฆ A Promise is an object that "promises" a result when a task finishes. The task can be in one of three states.
- Pending: Asynchronous processing has not finished yet (the promise was made, but there is no result yet).
- Fulfilled (success): Asynchronous processing completed successfully and returned the promised result.
- Rejected (failure): Asynchronous processing failed or an error occurred.
7.2 Creating a Promise (producer)
๐ ๏ธ Create one with new Promise, passing two callback functions: resolve (called on success) and reject (called on failure).
new Promise, passing two callback functions: resolve (called on success) and reject (called on failure).// Let's assume a scenario where data is retrieved from the server.
const fetchData = new Promise((resolve, reject) => {
console.log("Retrieving data...");
setTimeout(() => {
const success = true; // Indicates whether communication was successful (for testing purposes)
if (success) {
// 1. When successful: Return the result data inside `resolve()`. (Status: Fulfilled)
resolve({ id: 1, name: "Hong Gil-dong" });
} else {
// 2. On failure: Pass an error object inside `reject()`. (Status: Rejected)
reject(new Error("A network error occurred."));
}
}, 2000); // Assuming the operation takes 2 seconds
});
7.3 Using a Promise (consumer)
๐ง Use .then() for success, .catch() for failure, and .finally() for code that always runs.
.then() for success, .catch() for failure, and .finally() for code that always runs.// Let's use the fetchData(Promise object) created above.
fetchData
.then((data) => {
// 1. This code runs when resolve() is called.
console.log("โ
Success:", data); // Output: โ
Success: { id: 1, name: 'Hong Gil-dong' }
})
.catch((error) => {
// 2. This code runs when reject() is called. (Error handling)
console.log("โ Failure:", error.message);
})
.finally(() => {
// 3. This code runs exactly once at the end, regardless of success or failure. (Typically used to remove the loading spinner)
console.log("๐ All tasks are complete.");
});
7.4 A practical favorite: Promise chaining
โ๏ธ Previously, nested callbacks created "callback hell." Promises handle this cleanly by linking .then() calls like train cars.
.then() calls like train cars.// A sequence of operations: retrieve a number from the server -> find the user by that number -> find that user's posts
const fetchNumber = new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
});
fetchNumber
.then(num => {
console.log(`First result: ${num}`); // 1
return num * 10; // When `return` is called, this value is passed to the next `.then()` call. (This automatically becomes a new Promise)
})
.then(num => {
console.log(`Second result: ${num}`); // 10
return num * 10;
})
.then(num => {
console.log(`Third result: ${num}`); // 100
})
.catch(error => {
// If an error occurs anywhere in between, the nearest `catch` block catches it all.
console.log("Error occurred!", error);
});
7.5 Commonly used Promise methods in practice
๐ฆ These methods are useful for handling several asynchronous tasks together and concurrently.
const p1 = new Promise(resolve => setTimeout(() => resolve("Data #1"), 1000));
const p2 = new Promise(resolve => setTimeout(() => resolve("Data #2"), 2000));
// 1. Promise.all()
// Wait until all promises "succeed," then receive the results as an array all at once.
// (However, if even one fails, the entire chain is immediately caught by the catch block)
Promise.all([p1, p2])
.then(results => console.log("All complete:", results));
// Output after 2 seconds: All complete: ["Data #1", "Data #2"]
// 2. Promise.race()
// Like a running race, this retrieves only the result of the "first to finish" among multiple tasks.
Promise.race([p1, p2])
.then(result => console.log("1st place:", result));
// Output after 1 second: 1st place: Data #1
7.6 Promise.allSettled() (returns the results of all Promises in an array)
๐ก๏ธ Addresses a drawback of Promise.all() (it stops if even one fails): it waits for every result and returns an array with each status, whether fulfilled or rejected. (Introduced in ES11 / recommended in practice ๐)
const p1 = new Promise(resolve => setTimeout(() => resolve("Success Data"), 1000));
const p2 = new Promise((_, reject) => setTimeout(() => reject("Failure Error"), 2000));
Promise.allSettled([p1, p2])
.then(results => console.log(results));
/*
Output (returns an array after 2 seconds):
[
{ status: "fulfilled", value: "Success Data" },
{ status: "rejected", reason: "Failure Error" }
]
*/
๐ 8. async / await: The magic that makes Promises as readable as synchronous code (ES8)
8.1 Basic rules for async and await
๐ก Remember just these two keywords, and asynchronous code becomes surprisingly easy.
async: Put it before a function to make that function always return a Promise object.await: Can be used only inside anasyncfunction. It pauses and waits until a Promise operation finishes. (In other words, it replaces.then().)
8.2 Comparing Promise (.then) and async/await
๐ฅ Compare the code to see why async/await is useful. (Scenario: retrieving user information from a server)
[Traditional approach] Promise chaining (using .then)
function getUser() {
// fetch() is a built-in Promise function that retrieves data from the server.
fetch('[https://jsonplaceholder.typicode.com/users/1](https://jsonplaceholder.typicode.com/users/1)')
.then(response => response.json())
.then(user => {
console.log("Username:", user.name);
})
.catch(error => {
console.log("Error occurred:", error);
});
}
[Modern approach] Using async / await ๐ (the standard in practice)
// Prefix the function with "async".
async function getUserAsync() {
// Using `await` waits for the data to arrive, and when it does, it assigns it to the variable on the left.
const response = await fetch('[https://jsonplaceholder.typicode.com/users/1](https://jsonplaceholder.typicode.com/users/1)');
const user = await response.json();
// It reads from top to bottom like water flowing, without any callback functions (arrows)!
console.log("Username:", user.name);
}
getUserAsync();
8.3 Error handling (try ... catch)
๐ก๏ธ With async/await, use JavaScript's traditional error handling, try/catch, instead of .catch().
try/catch, instead of .catch().async function getUserSafe() {
try {
// 1. Place the code to be executed inside the `try` block.
const response = await fetch('https://invalid.example/users/1');
const user = await response.json();
console.log(user);
} catch (error) {
// 2. If an error occurs inside the `try` block, the code does not stop but immediately proceeds to the `catch` block.
console.log("โ A problem occurred during communication!", error.message);
} finally {
// 3. (Optional) This runs at the end, regardless of whether the operation succeeds or fails.
console.log("Loading screen complete");
}
}
8.4 Practical tip: Use it with Promise.all (parallel processing)
โก await makes the code wait. If you blindly use await to fetch unrelated data, the operation becomes slow.
await makes the code wait. If you blindly use await to fetch unrelated data, the operation becomes slow.๐จ Slow approach (sequential processing)
async function getDashboardData() {
// Wait 2 seconds to fetch the weather
const weather = await fetch('/api/weather');
// Then wait 2 seconds to fetch the news (Takes a total of 4 seconds)
const news = await fetch('/api/news');
}
๐ Fast approach (parallel processing - using Promise.all)
If the data can be fetched at the same time, group the operations with Promise.all.
async function getDashboardDataFast() {
// Send both requests simultaneously and wait only once (await) until both are complete. (Takes a total of 2 seconds)
const [weather, news] = await Promise.all([
fetch('/api/weather'),
fetch('/api/news')
]);
console.log("Data loading complete!");
}
๐ก๏ธ 9. Optional chaining (?.): Safely find values in nested objects without errors (ES11)
9.1 The painful error handling of the past
๐ฃ When accessing a nested object, a missing intermediate value (undefined) causes a TypeError and can crash the screen.
undefined) causes a TypeError and can crash the screen.const user = {
name: "Hong Gil-dong",
// Let's assume the user has no profile information at all.
};
// ๐จ [Danger] An error occurred! This is because an attempt was made to read `undefined.address`.
// console.log(user.profile.address);
// ๐ฆ [Old ES5 style] To prevent the error, we had to chain the `&&` operators together like a train.
if (user && user.profile && user.profile.address) {
console.log(user.profile.address);
} else {
console.log("No address information.");
}
9.2 Introducing optional chaining (?.)
๐ช Using ?. prevents an error when a value is missing in the middle; it quietly returns undefined and stops.
?. prevents an error when a value is missing in the middle; it quietly returns undefined and stops.const user = { name: "Hong Gil-dong" };
// โ
[Latest ES11 approach]
// "Is there a 'profile' property in 'user'? (?. ) If so, get the 'address' too!"
const address = user?.profile?.address;
console.log(address);
// It quietly outputs 'undefined' without throwing an error.
9.3 Using it with arrays and functions
๐ฏ You can safely access not only object properties, but also array indexes and function calls.
// 1. Safely accessing array elements
const users = null;
// Returns `undefined` without an error even if `users` is not an array or is null
console.log(users?.[0]?.name);
// 2. Safely calling functions (methods)
const admin = {
adminPlay() {
console.log("Running in admin mode!");
}
};
const guest = {};
// Execute only if the function "adminPlay" exists!
admin.adminPlay?.(); // "Run in admin mode!"
guest.adminPlay?.(); // Proceeds silently without an error
๐ 10. Nullish coalescing (??): Set reliable, safe default values (ES11)
10.1 The critical bug in the existing OR (||) operator
๐จ Previously, || was used to provide a default when a value was missing, but it also treated 0 and an empty string ("") as false (missing), causing a serious bug.
|| was used to provide a default when a value was missing, but it also treated 0 and an empty string ("") as false (missing), causing a serious bug.// Let's assume the quantity in the shopping cart for a product has been set to 0.
const count = 0;
// Developer's intent: "If the 'count' value is completely missing, set the default value to 10!"
const result = count || 10;
console.log(result);
// Output: 10 (Bug occurs ๐ฅ)
// Since JavaScript treats the number 0 as "false," it ignores the 0 entered by the user.
10.2 The perfect solution: nullish coalescing (??)
๐ก๏ธ ?? uses the value on the right only when the value is null or undefined. It treats 0 and empty strings as valid values.
?? uses the value on the right only when the value is null or undefined. It treats 0 and empty strings as valid values.const count = 0;
const text = "";
// โ
[Latest ES11 approach]
const safeCount = count ?? 10;
const safeText = text ?? "default text";
console.log(safeCount); // Output: 0 (Works as expected! Recognizes 0 as a valid value)
console.log(safeText); // Output: "" (Works as expected! An empty string is also recognized as a valid value)
// Returns the default value only when the value is null or undefined.
const notExist = null ?? "Default value works";
console.log(notExist); // Output: "Default value in effect"
10.3 A practical champion combo: use ?. and ?? together
๐ A perfect practical pattern: prevent errors with optional chaining and set a default with nullish coalescing.
const user = {
name: "Lee Sun-shin",
// No profile information
};
// "Find the user's city, using the ?. operator to prevent the chain from breaking,
// and if there's ultimately no value (??), display 'Unknown'!"
const userCity = user?.profile?.city ?? "Unknown";
console.log(`Location: ${userCity}`);
// Output: Location: Unknown
๐ 11. String.replaceAll(): Replace every occurrence in a string at once (ES12)
11.1 The limitations of replace() and the old approach
๐จ The old replace() had the serious limitation of changing only the first occurrence of a word.
replace() had the serious limitation of changing only the first occurrence of a word.const text = "apple, banana, apple, grape, apple";
// 1. Using the standard `replace()` method (only the first "apple" is replaced with "watermelon")
const result1 = text.replace("apple", "watermelon");
console.log(result1);
// Output: "watermelon, banana, apple, grape, apple"
// 2. [Before ES11] To replace the entire string, you had to use complex "regular expressions (Regex)" and the global flag (g).
const result2 = text.replace(/apple/g, "watermelon");
console.log(result2);
// Output: "watermelon, banana, watermelon, grape, watermelon"
11.2 The perfect solution: replaceAll()
โจ Now you can replace every occurrence using an intuitive string, without complex regular-expression syntax (/ /g).
const text = "apple, banana, apple, grape, apple";
// โ
[Latest ES12 method] Just enter the word you want to replace as a stringโthat's it!
const finalResult = text.replaceAll("apple", "watermelon");
console.log(finalResult);
// Output: "watermelon, banana, watermelon, grape, watermelon"
11.3 Practical tip: Method chaining and data cleansing
๐งน Very useful for cleaning up messy user input (such as excessive special characters) all at once.
// User-entered messy phone number data
const dirtyPhone = "010-1234-5678 (myself)";
// When you want to remove all '-' symbols and the text '(myself)' (using chaining)
const cleanPhone = dirtyPhone
.replaceAll("-", "") // Remove all hyphens
.replaceAll(" (Me)", ''); // Remove the text " (Me)"
console.log(cleanPhone);
// Output: "01012345678"