🌟 Spread and Rest Operators

ES6 ... Operator for Arrays and Objects

The ... Operator

The three dots (...) can be used as either the spread operator (expanding elements) or the rest operator (collecting elements), depending on context.

📤 Spread Operator

Array Spread

// Expand array into individual elements
let numbers = [1, 2, 3];
console.log(...numbers);  // 1 2 3 (separate arguments)
console.log(numbers);     // [1, 2, 3] (array)

// Copy array (shallow)
let original = [1, 2, 3];
let copy = [...original];
copy.push(4);
console.log(original);  // [1, 2, 3]
console.log(copy);      // [1, 2, 3, 4]

// Concatenate arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = [...arr1, ...arr2];
console.log(combined);  // [1, 2, 3, 4]

// Old way
let combined = arr1.concat(arr2);

// Insert elements
let middle = [3, 4];
let array = [1, 2, ...middle, 5, 6];
console.log(array);  // [1, 2, 3, 4, 5, 6]

// Add elements to copy
let numbers = [2, 3, 4];
let withBoundaries = [1, ...numbers, 5];
// [1, 2, 3, 4, 5]

Function Arguments

// Pass array as separate arguments
function sum(a, b, c) {
    return a + b + c;
}

let numbers = [1, 2, 3];
console.log(sum(...numbers));  // 6

// Old way
console.log(sum.apply(null, numbers));  // 6

// Math functions
let numbers = [5, 1, 9, 3, 7];
console.log(Math.max(...numbers));  // 9
console.log(Math.min(...numbers));  // 1

// Old way
console.log(Math.max.apply(null, numbers));

// Push multiple elements
let arr = [1, 2];
let toAdd = [3, 4, 5];
arr.push(...toAdd);
console.log(arr);  // [1, 2, 3, 4, 5]

// Old way
Array.prototype.push.apply(arr, toAdd);

Object Spread

// Copy object (shallow, ES2018)
let original = { a: 1, b: 2 };
let copy = { ...original };
copy.c = 3;
console.log(original);  // { a: 1, b: 2 }
console.log(copy);      // { a: 1, b: 2, c: 3 }

// Old way
let copy = Object.assign({}, original);

// Merge objects
let obj1 = { a: 1, b: 2 };
let obj2 = { c: 3, d: 4 };
let merged = { ...obj1, ...obj2 };
console.log(merged);  // { a: 1, b: 2, c: 3, d: 4 }

// Later properties override earlier ones
let obj1 = { a: 1, b: 2 };
let obj2 = { b: 3, c: 4 };
let merged = { ...obj1, ...obj2 };
console.log(merged);  // { a: 1, b: 3, c: 4 }

// Add/override properties
let user = { name: 'John', age: 30 };
let updated = { ...user, age: 31, city: 'NYC' };
console.log(updated);  // { name: 'John', age: 31, city: 'NYC' }

// Default properties
let defaults = { timeout: 3000, retries: 3 };
let config = { timeout: 5000 };
let settings = { ...defaults, ...config };
console.log(settings);  // { timeout: 5000, retries: 3 }

String Spread

// Spread string into characters
let str = 'hello';
let chars = [...str];
console.log(chars);  // ['h', 'e', 'l', 'l', 'o']

// Same as
let chars = str.split('');

// Unique characters
let unique = [...new Set('hello')];
console.log(unique);  // ['h', 'e', 'l', 'o']

// Spread in array literal
let arr = ['a', ...'hello', 'b'];
console.log(arr);  // ['a', 'h', 'e', 'l', 'l', 'o', 'b']

Spread with Iterables

// Set to array
let set = new Set([1, 2, 3]);
let arr = [...set];
console.log(arr);  // [1, 2, 3]

// Map to array
let map = new Map([['a', 1], ['b', 2]]);
let entries = [...map];
console.log(entries);  // [['a', 1], ['b', 2]]

// NodeList to array
let divs = document.querySelectorAll('div');
let divsArray = [...divs];
divsArray.forEach(div => console.log(div));

// Arguments to array
function test() {
    let args = [...arguments];
    console.log(args);
}
test(1, 2, 3);  // [1, 2, 3]

📥 Rest Operator

Function Parameters

// Collect remaining arguments
function sum(...numbers) {
    return numbers.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(1, 2, 3, 4, 5));  // 15

// Mix regular and rest parameters
function greet(greeting, ...names) {
    return `${greeting} ${names.join(' and ')}!`;
}

console.log(greet('Hello', 'John'));              // "Hello John!"
console.log(greet('Hi', 'John', 'Jane', 'Bob'));  // "Hi John and Jane and Bob!"

// Rest must be last parameter
function example(a, b, ...rest) {
    console.log(a);     // First arg
    console.log(b);     // Second arg
    console.log(rest);  // Remaining args
}

example(1, 2, 3, 4, 5);
// 1
// 2
// [3, 4, 5]

// Replace arguments object
function oldWay() {
    let args = Array.from(arguments);
    return args.reduce((a, b) => a + b);
}

function newWay(...args) {
    return args.reduce((a, b) => a + b);
}

Array Destructuring

// Collect remaining elements
let [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first);  // 1
console.log(rest);   // [2, 3, 4, 5]

// Skip elements and collect
let [, second, ...remaining] = [1, 2, 3, 4];
console.log(second);     // 2
console.log(remaining);  // [3, 4]

// Head and tail pattern
let [head, ...tail] = [1, 2, 3];
console.log(head);  // 1
console.log(tail);  // [2, 3]

// Empty rest
let [a, b, ...rest] = [1, 2];
console.log(rest);  // []

// Rest in middle? NO! Must be last
// let [first, ...middle, last] = [1, 2, 3, 4];  // SyntaxError

Object Destructuring

// Collect remaining properties (ES2018)
let { name, age, ...rest } = {
    name: 'John',
    age: 30,
    city: 'NYC',
    job: 'Developer'
};

console.log(name);  // 'John'
console.log(age);   // 30
console.log(rest);  // { city: 'NYC', job: 'Developer' }

// Extract some properties, keep others
let { id, createdAt, ...userData } = {
    id: 1,
    name: 'John',
    email: 'john@example.com',
    createdAt: '2025-01-01'
};

console.log(userData);
// { name: 'John', email: 'john@example.com' }

// Nested with rest
let { name, address: { city }, ...other } = {
    name: 'John',
    age: 30,
    address: {
        city: 'NYC',
        zip: '10001'
    }
};

console.log(other);
// { age: 30, address: { city: 'NYC', zip: '10001' } }

🔄 Combining Spread and Rest

// Function that accepts rest, uses spread
function merge(...objects) {
    return Object.assign({}, ...objects);
}

let result = merge({ a: 1 }, { b: 2 }, { c: 3 });
console.log(result);  // { a: 1, b: 2, c: 3 }

// Or with spread
function merge(...objects) {
    return { ...objects.reduce((acc, obj) => ({ ...acc, ...obj }), {}) };
}

// Destructure then spread
function updateUser(updates) {
    let { id, ...userData } = updates;
    return { ...userData, updatedAt: new Date() };
}

// Collect args, process, spread result
function calculate(...numbers) {
    let doubled = numbers.map(n => n * 2);
    return Math.max(...doubled);
}

console.log(calculate(1, 5, 3, 7));  // 14

// Rest in parameters, spread in call
function wrapper(...args) {
    return originalFunction(...args);
}

// Spread to rest, rest to spread
function proxy(...args) {
    console.log('Calling with:', ...args);
    return target(...args);
}

💡 Practical Examples

Array Operations

// Remove duplicates
let numbers = [1, 2, 2, 3, 3, 4];
let unique = [...new Set(numbers)];
console.log(unique);  // [1, 2, 3, 4]

// Merge arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let arr3 = [5, 6];
let merged = [...arr1, ...arr2, ...arr3];
// [1, 2, 3, 4, 5, 6]

// Insert at specific position
let array = [1, 2, 5, 6];
let toInsert = [3, 4];
array = [...array.slice(0, 2), ...toInsert, ...array.slice(2)];
// [1, 2, 3, 4, 5, 6]

// Flatten one level
let nested = [[1, 2], [3, 4], [5, 6]];
let flat = [].concat(...nested);
// [1, 2, 3, 4, 5, 6]

// Clone and modify
let original = [1, 2, 3];
let modified = [...original, 4, 5];
console.log(original);  // [1, 2, 3]
console.log(modified);  // [1, 2, 3, 4, 5]

// Split array
let [first, second, ...others] = [1, 2, 3, 4, 5];
// first: 1, second: 2, others: [3, 4, 5]

Object Operations

// Update object immutably
let user = { name: 'John', age: 30, city: 'NYC' };
let updated = { ...user, age: 31 };

// Remove properties
let { password, ...safe } = user;
console.log(safe);  // user without password

// Conditional properties
let includeEmail = true;
let user = {
    name: 'John',
    age: 30,
    ...(includeEmail && { email: 'john@example.com' })
};

// Merge with priority
let defaults = { timeout: 3000, retries: 3, debug: false };
let userConfig = { timeout: 5000 };
let config = { ...defaults, ...userConfig };

// Nested update (careful - shallow copy)
let state = {
    user: { name: 'John', age: 30 },
    settings: { theme: 'dark' }
};

let newState = {
    ...state,
    user: { ...state.user, age: 31 }
};

// Clone deeply nested (shallow per level)
let original = {
    a: 1,
    b: { c: 2, d: { e: 3 } }
};

let copy = {
    ...original,
    b: {
        ...original.b,
        d: { ...original.b.d }
    }
};

Function Utilities

// Flexible sum function
function sum(...numbers) {
    return numbers.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2));           // 3
console.log(sum(1, 2, 3, 4, 5));  // 15

// Logger with prefix
function log(prefix, ...messages) {
    console.log(`[${prefix}]`, ...messages);
}

log('INFO', 'Server started', 'Port:', 3000);

// Apply function with context
function apply(fn, context, ...args) {
    return fn.apply(context, args);
}

// Curry with rest
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return (...nextArgs) => curried(...args, ...nextArgs);
    };
}

let add = (a, b, c) => a + b + c;
let curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));  // 6

// Compose functions
function compose(...fns) {
    return x => fns.reduceRight((v, fn) => fn(v), x);
}

let addOne = x => x + 1;
let double = x => x * 2;
let square = x => x * x;

let compute = compose(square, double, addOne);
console.log(compute(2));  // 36

React/State Management

// Update state immutably
let state = {
    users: [{ id: 1, name: 'John' }],
    settings: { theme: 'dark' }
};

// Add user
let newState = {
    ...state,
    users: [...state.users, { id: 2, name: 'Jane' }]
};

// Update user
let newState = {
    ...state,
    users: state.users.map(user =>
        user.id === 1 ? { ...user, name: 'John Doe' } : user
    )
};

// Remove user
let newState = {
    ...state,
    users: state.users.filter(user => user.id !== 1)
};

// Update nested property
let newState = {
    ...state,
    settings: { ...state.settings, theme: 'light' }
};

// Redux reducer pattern
function reducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                ...state,
                todos: [...state.todos, action.payload]
            };
        case 'UPDATE_TODO':
            return {
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.payload.id
                        ? { ...todo, ...action.payload }
                        : todo
                )
            };
        default:
            return state;
    }
}

API and Data Handling

// Merge API responses
async function loadDashboard() {
    let [users, posts, stats] = await Promise.all([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json()),
        fetch('/api/stats').then(r => r.json())
    ]);
    
    return { users, posts, stats };
}

// Combine pagination results
async function loadAllPages(baseUrl) {
    let allData = [];
    let page = 1;
    
    while (true) {
        let { data, hasMore } = await fetch(`${baseUrl}?page=${page}`)
            .then(r => r.json());
        
        allData = [...allData, ...data];
        
        if (!hasMore) break;
        page++;
    }
    
    return allData;
}

// Transform and filter data
function processUsers(users, filters) {
    let { minAge, ...otherFilters } = filters;
    
    return users
        .filter(user => user.age >= minAge)
        .map(({ password, ...safe }) => safe)
        .map(user => ({ ...user, ...otherFilters }));
}

// Partial update
function updateResource(id, updates) {
    return fetch(`/api/resources/${id}`, {
        method: 'PATCH',
        body: JSON.stringify(updates),
        headers: { 'Content-Type': 'application/json' }
    });
}

let { name, email } = formData;
updateResource(userId, { name, email });

DOM Manipulation

// NodeList to array
let elements = [...document.querySelectorAll('.item')];
elements.forEach(el => el.classList.add('active'));

// Convert HTMLCollection
let divs = [...document.getElementsByTagName('div')];

// Combine element arrays
let headers = [...document.querySelectorAll('h1, h2, h3')];

// Clone attributes
let element = document.querySelector('.original');
let clone = document.createElement('div');
[...element.attributes].forEach(attr => {
    clone.setAttribute(attr.name, attr.value);
});

// Get all text nodes
function getAllTextNodes(element) {
    let walker = document.createTreeWalker(
        element,
        NodeFilter.SHOW_TEXT
    );
    
    let nodes = [];
    while (walker.nextNode()) {
        nodes = [...nodes, walker.currentNode];
    }
    return nodes;
}

⚠️ Common Pitfalls

// Shallow copy issue
let original = { a: 1, b: { c: 2 } };
let copy = { ...original };
copy.b.c = 3;
console.log(original.b.c);  // 3 (mutated!)

// Solution: deep clone nested objects
let copy = {
    ...original,
    b: { ...original.b }
};

// Or use structuredClone (modern)
let copy = structuredClone(original);

// Spread order matters
let obj = { a: 1, b: 2, c: 3 };
let result1 = { ...obj, b: 99 };  // { a: 1, b: 99, c: 3 }
let result2 = { b: 99, ...obj };  // { a: 1, b: 2, c: 3 }

// Rest must be last
// function bad(...rest, last) {}  // SyntaxError
function good(first, ...rest) {}   // OK

// Can't spread non-iterables
// let obj = { ...42 };  // Error
let obj = { ...{ a: 1 } };  // OK
let arr = [...[1, 2, 3]];   // OK
let arr = [...'hello'];     // OK

// Spread in object only copies own properties
let obj = Object.create({ inherited: 1 });
obj.own = 2;
let copy = { ...obj };
console.log(copy);  // { own: 2 } (no inherited)

🎯 Key Takeaways