Skillversitas

48. ES6+ Features Recap: Modern JavaScript Features

Introduction

ES6 (ECMAScript 2015) and later versions brought many new features that make JavaScript more powerful, expressive, and readable. These features are now standard and widely used across all modern browsers and Node.js.

For IT students, understanding ES6+ features is important because every modern codebase uses them. From React and Vue to Node.js backends, everything relies on ES6+ features.

Why Are ES6+ Features Important?

Many old codebases still use verbose, hard-to-read ES5 patterns. By understanding ES6+, you can:
- Read and write cleaner, more modern code
- Work with modern frameworks/libraries that use ES6+ syntax
- Understand code reviews from senior developers
- Migrate old projects to better patterns

let and const

// ES5: var - function scoped, can be redeclared, hoisting
var name = 'John';
var name = 'Mike'; // OK, redeclaration

// ES6: let - block scoped, cannot be redeclared
let usia = 20;
// let usia = 21; // Error: Identifier 'usia' has already been declared

usia = 21; // OK, reassignment

// ES6: const - block scoped, cannot be reassigned
const StudentID = '1234567890';
// StudentID = '1111111111'; // Error: Assignment to constant variable

// Const with an object - properties can still be mutated
const student = { name: 'Mike', gpa: 3.5 };
student.gpa = 3.7; // OK, mutation
// student = {}; // Error, reassignment

// Block scope demo
if (true) {
    let x = 10;
    const y = 20;
    var z = 30; // Hoisted to function/global scope
}
// console.log(x); // Error: x is not defined
// console.log(y); // Error: y is not defined
console.log(z); // 30 (var hoisting)

Arrow Functions

// ES5: function expression
const add = function(a, b) {
    return a + b;
};

// ES6: Arrow function - concise syntax
const add = (a, b) => a + b;

// Single parameter - without parentheses
const square = x => x * x;

// Multiple statements - need curly braces and an explicit return
const process = (a, b) => {
    const result = a * b;
    return result + 10;
};

// Object literal - needs extra parentheses
const createUser = (name, role) => ({ name, role });

// Arrow functions do not have their own 'this'
// Useful for callbacks
class Student {
    constructor(name) {
        this.name = name;
        this.grade = [];
    }

    // ES5: needs bind or self=this
    addGrade(grade) {
        this.grade.push(grade);
        setTimeout(function() {
            console.log(`${this.name} mendapat grade ${grade}`);
        }.bind(this), 1000);
    }

    // ES6: arrow function uses the 'this' from the parent scope
    addGradeModern(grade) {
        this.grade.push(grade);
        setTimeout(() => {
            console.log(`${this.name} mendapat grade ${grade}`);
        }, 1000);
    }
}

// Common usage
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
const evens = numbers.filter(x => x % 2 === 0);
const sum = numbers.reduce((acc, x) => acc + x, 0);

Template Literals

// ES5: string concatenation
const name = 'Mike';
const studentId = '1234567890';
const message = 'Hello, my name is ' + name + ' with StudentID ' + studentId;

// ES6: Template literals
const message = `Hello, my name is ${name} with StudentID ${studentId}`;

// Multi-line strings
const address = `
    Department Technology Industri
    Computer Science Building, Floor 3
    Campus University
`;

// Expression inside a template literal
const gpa = 3.75;
const status = `GPA ${name}: ${gpa >= 3.5 ? 'Cum Laude' : 'Regular'}`;

// Tagged template literals
function highlight(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = values[i] ? `<mark>${values[i]}</mark>` : '';
        return result + str + value;
    }, '');
}

const nameHighlight = highlight`Name student: ${name}, StudentID: ${studentId}`;

Destructuring

// Object destructuring
const student = {
    name: 'Mike',
    studentId: '1234567890',
    majorCode: 'Computer Science',
    year: 2021,
    gpa: 3.75
};

// ES5
const name = student.name;
const studentId = student.studentId;

// ES6: Destructuring
const { name, studentId, majorCode } = student;
console.log(name, studentId, majorCode);

// Rename while destructuring
const { name: fullName, gpa: gpaStudent } = student;

// Default value
const { address = 'Not set' } = student;

// Nested destructuring
const university = {
    name: 'Technology University',
    faculty: {
        name: 'Faculty of Engineering',
        majorCode: ['CS', 'IS', 'CE']
    }
};

const { faculty: { name: facultyName, majorCode: [majorCode1] } } = university;
console.log(facultyName, majorCode1);

// Array destructuring
const [first, second, third] = [10, 20, 30];
console.log(first, second); // 10, 20

// Skip elements
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1, 3

// Rest pattern
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head, tail); // 1, [2, 3, 4, 5]

// Swap variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1

// Function parameter destructuring
function printStudent({ name, studentId, gpa = 0 }) {
    console.log(`${name} (${studentId}) - GPA: ${gpa}`);
}

printStudent(student);

// Destructuring inside a loop
const studentList = [
    { name: 'Mike', studentId: '001' },
    { name: 'John', studentId: '002' },
    { name: 'Anna', studentId: '003' }
];

for (const { name, studentId } of studentList) {
    console.log(`${name}: ${studentId}`);
}

Spread & Rest Operator

// Spread - expand an array/object
// Array
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Copy array
const copy = [...arr1];

// Object spread
const defaults = { theme: 'light', lang: 'id' };
const userPrefs = { theme: 'dark', fontSize: 14 };
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'id', fontSize: 14 }

// Spread in function arguments
const numbers = [3, 7, 1, 9, 4];
const max = Math.max(...numbers); // 9
const min = Math.min(...numbers); // 1

// Rest - collect the remaining elements
function calculateTotal(...items) {
    return items.reduce((total, item) => total + item.price, 0);
}

const cart = [
    { name: 'Pencil', price: 5000 },
    { name: 'Book', price: 25000 },
    { name: 'Ruler', price: 10000 }
];

calculateTotal(...cart); // 40000

// Rest in destructuring
const [first, ...rest] = [1, 2, 3, 4];
console.log(rest); // [2, 3, 4]

const { name, ...restData } = student;
console.log(restData); // { studentId: ..., majorCode: ..., etc }

Default Parameters

// ES5
function createStudent(name, studentId, majorCode) {
    name = name || 'No name';
    studentId = studentId || '0000000000';
    majorCode = majorCode || 'Undetermined';

    return { name, studentId, majorCode };
}

// ES6: Default parameters
function createStudent(name = 'No name', studentId = '0000000000', majorCode = 'Undetermined') {
    return { name, studentId, majorCode };
}

// Default with an expression
function kalkulasiGPA(grade, weight = grade >= 70 ? 4 : grade >= 60 ? 3 : 2) {
    return { grade, weight };
}

// A default parameter can use earlier parameters
function createEmail(name, domain = `${name.toLowerCase()}@university.edu`) {
    return domain;
}

// Usage example
const student1 = createStudent('Mike'); // { name: 'Mike', studentId: '0000000000', majorCode: 'Undetermined' }
const student2 = createStudent('John', '1234567890', 'CS');

Classes

// ES5: Prototype-based
function Student(name, studentId) {
    this.name = name;
    this.studentId = studentId;
    this.grade = [];
}

Student.prototype.addGrade = function(grade) {
    this.grade.push(grade);
};

Student.prototype.calculateGPA = function() {
    if (this.grade.length === 0) return 0;
    return this.grade.reduce((a, b) => a + b, 0) / this.grade.length;
};

// ES6: Class syntax
class Student {
    // Private field
    #password;

    constructor(name, studentId, password) {
        this.name = name;
        this.studentId = studentId;
        this.#password = password;
        this.grade = [];
    }

    // Method
    addGrade(grade) {
        if (grade >= 0 && grade <= 100) {
            this.grade.push(grade);
            return true;
        }
        return false;
    }

    // Getter
    get average() {
        if (this.grade.length === 0) return 0;
        return this.grade.reduce((a, b) => a + b, 0) / this.grade.length;
    }

    // Setter
    set info(newName) {
        this.name = newName;
    }

    // Static method
    static createFromData(data) {
        return new Student(data.name, data.studentId, data.password);
    }

    // Static property
    static MAX_GRADE = 100;

    // Override toString
    toString() {
        return `${this.name} (${this.studentId}) - GPA: ${this.average.toFixed(2)}`;
    }
}

// Inheritance
class StudentS2 extends Student {
    constructor(name, studentId, password, thesis) {
        super(name, studentId, password); // Call the parent constructor
        this.thesis = thesis;
    }

    // Override method
    get thesisInfo() {
        return `Thesis: ${this.thesis.title} - Status: ${this.thesis.status}`;
    }
}

// Usage
const student = new Student('Mike', '1234567890', 'secret123');
student.addGrade(85);
student.addGrade(90);
console.log(student.average); // 87.5

const student2 = Student.createFromData({
    name: 'John',
    studentId: '0987654321',
    password: 'pass456'
});

Modules (import/export)

// file: utils.js - Named export
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Kalkulator { /* ... */ }

// Default export
export default class Logger {
    log(msg) { console.log(msg); }
}

// file: app.js - Import
import Logger from './utils.js';           // Default import
import { PI, add, Kalkulator } from './utils.js';  // Named import
import * as Utils from './utils.js';       // Namespace import

// Renamed import
import { add as add } from './utils.js';

// Dynamic import
async function loadModule() {
    const module = await import('./utils.js');
    module.add(1, 2);
}

// Re-export
export { add } from './utils.js';
export * from './utils.js';
export { default as Logger } from './utils.js';

Promises and Async/Await

// Promise
function fetchStudent(studentId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (studentId) {
                resolve({ name: 'Mike', studentId: studentId });
            } else {
                reject(new Error('Invalid StudentID'));
            }
        }, 1000);
    });
}

// Promise chaining
fetchStudent('1234567890')
    .then(student => fetchGrades(student.studentId))
    .then(grade => console.log(grade))
    .catch(err => console.error(err))
    .finally(() => console.log('Done'));

// Async/Await
async function getStudentData(studentId) {
    try {
        const student = await fetchStudent(studentId);
        const grade = await fetchGrades(studentId);
        return { student: student, grade: grade };
    } catch (error) {
        console.error('Failed to fetch data:', error);
        throw error;
    }
}

// Parallel execution
async function getDashboardData() {
    const [student, schedule, grade] = await Promise.all([
        fetchStudent(),
        fetchSchedule(),
        fetchGrades()
    ]);

    return { student, schedule, grade };
}

// Promise.allSettled - all promises complete (success/failure)
const results = await Promise.allSettled([
    fetch('/api/student'),
    fetch('/api/schedule'),
    fetch('/api/grade')
]);

const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');

for...of Loop

// Iterate over an array
const numbers = [10, 20, 30, 40, 50];
for (const num of numbers) {
    console.log(num);
}

// Iterate over a string
const name = 'Mike';
for (const char of name) {
    console.log(char);
}

// Iterate over a Map
const studentMap = new Map();
studentMap.set('001', 'Mike');
studentMap.set('002', 'John');

for (const [studentId, name] of studentMap) {
    console.log(`${studentId}: ${name}`);
}

// Iterate over a Set
const unique = new Set([1, 2, 3, 3, 4]);
for (const num of unique) {
    console.log(num);
}

// With entries()
const student = ['Mike', 'John', 'Anna'];
for (const [index, name] of student.entries()) {
    console.log(`${index + 1}. ${name}`);
}

// Break and continue work
for (const num of numbers) {
    if (num === 30) break;
    console.log(num);
}

Symbol

// Creating a Symbol
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false

// Symbol as a property key
const STUDENT_ID = Symbol('student_id');
const student = {
    name: 'Mike',
    [STUDENT_ID]: '1234567890'
};

console.log(student[STUDENT_ID]); // '1234567890'

// Well-known symbols
class Collection {
    constructor() {
        this.items = [];
    }

    // Custom iterable
    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;

        return {
            next() {
                return index < items.length
                    ? { value: items[index++], done: false }
                    : { done: true };
            }
        };
    }

    // Custom string representation
    [Symbol.toString]() {
        return `Collection(${this.items.length} items)`;
    }
}

// Global Symbol registry
const sym1 = Symbol.for('shared');
const sym2 = Symbol.for('shared');
console.log(sym1 === sym2); // true

Real-World Example: Modernizing Old JavaScript Code

// === OLD CODE (ES5) ===
var students = [
    { name: 'Mike', studentId: '001', scores: [85, 90, 78] },
    { name: 'John', studentId: '002', scores: [92, 88, 95] },
    { name: 'Anna', studentId: '003', scores: [78, 82, 90] }
];

function calculateAverage(scores) {
    var total = 0;
    for (var i = 0; i < scores.length; i++) {
        total += scores[i];
    }
    return total / scores.length;
}

function getTopStudents(students, minAverage) {
    var result = [];
    for (var i = 0; i < students.length; i++) {
        var avg = calculateAverage(students[i].scores);
        if (avg >= minAverage) {
            result.push({
                name: students[i].name,
                studentId: students[i].studentId,
                average: avg
            });
        }
    }
    result.sort(function(a, b) {
        return b.average - a.average;
    });
    return result;
}

// === MODERN CODE (ES6+) ===
const students = [
    { name: 'Mike', studentId: '001', scores: [85, 90, 78] },
    { name: 'John', studentId: '002', scores: [92, 88, 95] },
    { name: 'Anna', studentId: '003', scores: [78, 82, 90] }
];

const calculateAverage = (scores) =>
    scores.reduce((total, score) => total + score, 0) / scores.length;

const getTopStudents = (students, minAverage) =>
    students
        .map(({ name, studentId, scores }) => ({
            name,
            studentId,
            average: calculateAverage(scores)
        }))
        .filter(({ average }) => average >= minAverage)
        .sort((a, b) => b.average - a.average);

// With template literals
const displayStudents = (students) => {
    students.forEach(({ name, studentId, average }) => {
        console.log(`${name} (${studentId}) - Average: ${average.toFixed(2)}`);
    });
};

// With destructuring
const [{ name: topStudent }, ...rest] = getTopStudents(students, 85);
console.log(`Top student: ${topStudent}`);

// Combining all the features
const processStudentData = async (studentIds) => {
    const fetchStudent = async (id) => {
        const response = await fetch(`/api/students/${id}`);
        return response.json();
    };

    const promises = studentIds.map(fetchStudent);
    const results = await Promise.allSettled(promises);

    return results
        .filter(({ status }) => status === 'fulfilled')
        .map(({ value: { name, studentId, scores } }) => ({
            name,
            studentId,
            average: calculateAverage(scores),
            status: calculateAverage(scores) >= 70 ? 'Pass' : 'Fail'
        }));
};

// A class with all the modern features
class StudentManager {
    #students = new Map();

    add(student) {
        const { studentId, ...rest } = student;
        this.#students.set(studentId, rest);
    }

    get(studentId) {
        return this.#students.get(studentId);
    }

    *filterByPredicate(predicate) {
        for (const [studentId, data] of this.#students) {
            if (predicate({ studentId, ...data })) {
                yield { studentId, ...data };
            }
        }
    }

    get topStudents() {
        return [...this.#students.entries()]
            .map(([studentId, data]) => ({
                studentId,
                ...data,
                average: calculateAverage(data.scores)
            }))
            .filter(s => s.average >= 85)
            .sort((a, b) => b.average - a.average);
    }

    [Symbol.iterator]() {
        return this.#students[Symbol.iterator]();
    }
}

Example: Async Data Processing Pipeline

class DataPipeline {
    constructor() {
        this.stages = [];
    }

    addStage(name, fn) {
        this.stages.push({ name, fn });
        return this;
    }

    async execute(initialData) {
        let data = initialData;

        for (const { name, fn } of this.stages) {
            console.log(`Executing stage: ${name}`);
            const start = performance.now();

            data = await fn(data);

            const duration = (performance.now() - start).toFixed(2);
            console.log(`${name} completed in ${duration}ms`);
        }

        return data;
    }
}

// Usage
const pipeline = new DataPipeline()
    .addStage('Fetch', async (ids) => {
        const responses = await Promise.all(
            ids.map(id => fetch(`/api/students/${id}`).then(r => r.json()))
        );
        return responses;
    })
    .addStage('Filter', async (students) => 
        students.filter(s => s.status === 'active')
    )
    .addStage('Transform', async (students) =>
        students.map(({ name, studentId, grade }) => ({
            name,
            studentId,
            average: grade.reduce((a, b) => a + b, 0) / grade.length
        }))
    )
    .addStage('Sort', async (students) =>
        [...students].sort((a, b) => b.average - a.average)
    )
    .addStage('Display', async (students) => {
        students.forEach((s, i) => {
            console.log(`${i + 1}. ${s.name} - ${s.average.toFixed(2)}`);
        });
        return students;
    });

// Run the pipeline
const studentIds = ['001', '002', '003', '004', '005'];
const result = await pipeline.execute(studentIds);

Example: Modern State Management

class Store {
    #state;
    #listeners = new Map();
    #middlewares = [];

    constructor(initialState = {}) {
        this.#state = structuredClone(initialState);
    }

    getState() {
        return structuredClone(this.#state);
    }

    setState(updater) {
        const prevState = this.#state;
        const newState = typeof updater === 'function'
            ? updater(prevState)
            : updater;

        // Run the middleware
        for (const middleware of this.#middlewares) {
            middleware(prevState, newState);
        }

        this.#state = { ...prevState, ...newState };
        this.#notify();
    }

    subscribe(key, callback) {
        if (!this.#listeners.has(key)) {
            this.#listeners.set(key, new Set());
        }
        this.#listeners.get(key).add(callback);

        return () => this.#listeners.get(key).delete(callback);
    }

    #notify() {
        for (const [key, callbacks] of this.#listeners) {
            if (this.#state[key] !== undefined) {
                for (const callback of callbacks) {
                    callback(this.#state[key]);
                }
            }
        }
    }

    use(middleware) {
        this.#middlewares.push(middleware);
        return this;
    }
}

// Logger middleware
const logger = (prevState, nextState) => {
    console.log('State changed:', { from: prevState, to: nextState });
};

// Initialize the store
const store = new Store({
    student: [],
    schedule: [],
    grade: [],
    user: null
}).use(logger);

// Subscribe to changes
store.subscribe('student', (student) => {
    console.log(`Ada ${student.length} students`);
    renderStudentList(student);
});

// Update state
store.setState(prev => ({
    student: [...prev.student, { name: 'Mike', studentId: '001' }]
}));

Code Explanation

Features Summary

  • let/const - Block-scoped variables, const for values that don't change
  • Arrow functions - Concise syntax, lexical this
  • Template literals - String interpolation, multi-line strings
  • Destructuring - Extract values from arrays/objects
  • Spread/Rest - Expand/collect elements
  • Default parameters - Default values for function parameters
  • Classes - OOP syntax, inheritance, private fields
  • Modules - import/export for code organization
  • Promises/Async-Await - Handle asynchronous operations
  • for...of - Iterate over iterables
  • Symbol - Unique identifiers, well-known symbols

Conclusion

ES6+ features have changed the way we write JavaScript, from verbose to clean and expressive. By mastering these features, you can write code that is shorter, easier to read, and easier to maintain. For a campus project, applying ES6+ features such as arrow functions for callbacks, destructuring for extracting data, template literals for dynamic strings, and async/await for API calls will make the codebase much better. Start by refactoring old code incrementally to adopt these modern features.