JavaScript Functions and Closures

Hiểu sâu về Function, Arrow Function và Closures trong JavaScript

12 phút

Functions là trái tim của JavaScript - chúng là first-class citizens, có thể được gán vào biến, truyền làm tham số và trả về từ functions khác. Trong bài viết này, chúng ta sẽ tìm hiểu sâu về functions, arrow functions và một trong những khái niệm quan trọng nhất: Closures.

Functions trong JavaScript

Function Declarations

Function declarations được hoisted hoàn toàn, có thể gọi trước khi khai báo.

// Có thể gọi trước khi khai báo
console.log(greet("World")); // "Hello, World!"

function greet(name) {
    return `Hello, ${name}!`;
}

// Function với nhiều tham số
function calculateArea(length, width, height = 1) {
    return length * width * height;
}

console.log(calculateArea(5, 3));    // 15 (2D)
console.log(calculateArea(5, 3, 2)); // 30 (3D)

Function Expressions

Function expressions không được hoisted, chỉ có thể gọi sau khi khai báo.

// ❌ Không thể gọi trước khi khai báo
// console.log(multiply(2, 3)); // TypeError

// Named function expression
const multiply = function multiplyNumbers(a, b) {
    return a * b;
};

// Anonymous function expression
const divide = function(a, b) {
    if (b === 0) {
        throw new Error("Cannot divide by zero");
    }
    return a / b;
};

console.log(multiply(4, 5)); // 20
console.log(divide(10, 2));  // 5

Immediately Invoked Function Expressions (IIFE)

// IIFE - chạy ngay lập tức
(function() {
    console.log("IIFE chạy ngay!");
})();

// IIFE với tham số
(function(name) {
    console.log(`Hello ${name}!`);
})("JavaScript");

// IIFE trả về giá trị
const result = (function(a, b) {
    return a + b;
})(10, 20);

console.log(result); // 30

Arrow Functions (ES6+)

Arrow functions cung cấp syntax ngắn gọn hơn và có đặc điểm quan trọng về this binding.

Syntax cơ bản

// Function declaration cũ
function add(a, b) {
    return a + b;
}

// Arrow function tương đương
const add = (a, b) => {
    return a + b;
};

// Arrow function rút gọn (implicit return)
const add = (a, b) => a + b;

// Một tham số - có thể bỏ ngoặc đơn
const square = x => x * x;

// Không có tham số - phải có ngoặc đơn
const getMessage = () => "Hello World";

// Trả về object - phải wrap trong ngoặc đơn
const createUser = (name, age) => ({ name, age });

Sự khác biệt quan trọng với regular functions

1. this binding

const person = {
    name: "John",
    age: 30,
    
    // Regular function - this refers to person object
    greetRegular: function() {
        console.log(`Hi, I'm ${this.name}`);
    },
    
    // Arrow function - this refers to lexical scope
    greetArrow: () => {
        console.log(`Hi, I'm ${this.name}`); // undefined
    },
    
    // Practical example with callbacks
    setupTimer: function() {
        // ❌ Vấn đề với regular function
        setTimeout(function() {
            console.log(`${this.name} timer expired`); // undefined
        }, 1000);
        
        // ✅ Arrow function giữ nguyên this
        setTimeout(() => {
            console.log(`${this.name} timer expired`); // "John timer expired"
        }, 2000);
    }
};

person.greetRegular(); // "Hi, I'm John"
person.greetArrow();   // "Hi, I'm undefined"
person.setupTimer();

2. arguments object

// Regular function có arguments object
function regularSum() {
    console.log(arguments); // Arguments object
    return Array.from(arguments).reduce((sum, num) => sum + num, 0);
}

// Arrow function không có arguments object
const arrowSum = (...numbers) => {
    // console.log(arguments); // ❌ ReferenceError
    return numbers.reduce((sum, num) => sum + num, 0);
};

console.log(regularSum(1, 2, 3, 4)); // 10
console.log(arrowSum(1, 2, 3, 4));   // 10

3. Constructor usage

// Regular function có thể dùng làm constructor
function Person(name) {
    this.name = name;
}

const john = new Person("John"); // ✅ OK

// Arrow function không thể dùng làm constructor
const PersonArrow = (name) => {
    this.name = name;
};

// const jane = new PersonArrow("Jane"); // ❌ TypeError

Khi nào dùng Arrow Functions?

// ✅ Tốt cho callbacks
const numbers = [1, 2, 3, 4, 5];

// Clean và ngắn gọn
const doubled = numbers.map(n => n * 2);
const evenNumbers = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);

// ✅ Tốt cho event handlers khi cần preserve this
class Component {
    constructor() {
        this.count = 0;
        this.element = document.getElementById('button');
        
        // Arrow function giữ nguyên this của class
        this.element.addEventListener('click', () => {
            this.count++;
            this.render();
        });
    }
    
    render() {
        this.element.textContent = `Clicked ${this.count} times`;
    }
}

// ❌ Không tốt cho object methods
const calculator = {
    value: 0,
    
    // ❌ Arrow function - this không refer đến calculator
    add: (num) => {
        this.value += num; // this is undefined
        return this;
    },
    
    // ✅ Regular function
    subtract: function(num) {
        this.value -= num;
        return this;
    }
};

Execution Context và Call Stack

Để hiểu closures, trước tiên phải hiểu cách JavaScript quản lý execution context.

Execution Context

Mỗi khi function được gọi, JavaScript tạo một Execution Context mới:

// Global Execution Context
let globalVar = "I'm global";

function outerFunction(x) {
    // Outer Function Execution Context
    let outerVar = "I'm outer";
    
    console.log("Outer function execution context:");
    console.log("- x:", x);
    console.log("- outerVar:", outerVar);
    console.log("- globalVar:", globalVar);
    
    function innerFunction(y) {
        // Inner Function Execution Context
        let innerVar = "I'm inner";
        
        console.log("Inner function execution context:");
        console.log("- y:", y);
        console.log("- innerVar:", innerVar);
        console.log("- outerVar:", outerVar);  // Closure!
        console.log("- x:", x);                // Closure!
        console.log("- globalVar:", globalVar);
        
        return innerVar + outerVar + globalVar;
    }
    
    return innerFunction;
}

const myFunction = outerFunction("parameter");
const result = myFunction("inner param");

Lexical Environment

function createFunction() {
    const message = "Hello from lexical environment";
    let counter = 0;
    
    // Function này "nhớ" lexical environment của nó
    return function() {
        counter++;
        console.log(`${message} - Call #${counter}`);
        return counter;
    };
}

const fn1 = createFunction();
const fn2 = createFunction();

fn1(); // "Hello from lexical environment - Call #1"
fn1(); // "Hello from lexical environment - Call #2"
fn2(); // "Hello from lexical environment - Call #1" (riêng biệt)

Closures - Khái niệm cốt lõi

IMPORTANT

Closure là khi một function có thể truy cập và “nhớ” các biến từ lexical scope của nó, ngay cả khi function đó được thực thi bên ngoài scope đó.

Closure cơ bản

function outerFunction(x) {
    // Biến trong outer scope
    let outerVariable = x;
    
    // Inner function có thể truy cập outerVariable
    function innerFunction(y) {
        return outerVariable + y; // Closure!
    }
    
    return innerFunction;
}

// outerFunction đã kết thúc, nhưng innerFunction vẫn "nhớ" outerVariable
const addFive = outerFunction(5);
console.log(addFive(10)); // 15 - outerVariable (5) + y (10)

const addTen = outerFunction(10);
console.log(addTen(10)); // 20 - outerVariable (10) + y (10)

Ví dụ thực tế 1: Counter với Closure

// ✅ Counter với closure - private state
function createCounter(initialValue = 0) {
    let count = initialValue;
    
    return {
        increment() {
            count++;
            return count;
        },
        
        decrement() {
            count--;
            return count;
        },
        
        getValue() {
            return count;
        },
        
        reset() {
            count = initialValue;
            return count;
        }
    };
}

const counter1 = createCounter(0);
const counter2 = createCounter(100);

console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter1.getValue());  // 2

console.log(counter2.increment()); // 101
console.log(counter2.getValue());  // 101

// Không thể truy cập trực tiếp biến count
// console.log(counter1.count); // undefined - private!

Ví dụ thực tế 2: Event Handlers với Closure

// Tạo multiple buttons với closure
function setupButtons() {
    const buttons = [];
    
    for (let i = 0; i < 5; i++) {
        // Mỗi button "nhớ" giá trị i của riêng nó
        const button = document.createElement('button');
        button.textContent = `Button ${i}`;
        
        // Closure captures the current value of i
        button.addEventListener('click', function() {
            console.log(`Button ${i} was clicked!`);
            
            // Advanced: Closure với state
            let clickCount = 0;
            
            return function() {
                clickCount++;
                this.textContent = `Button ${i} (${clickCount} clicks)`;
            };
        }());
        
        buttons.push(button);
        document.body.appendChild(button);
    }
    
    return buttons;
}

// setupButtons();

Ví dụ thực tế 3: Module Pattern

const UserModule = (function() {
    // Private variables và functions
    let users = [];
    let nextId = 1;
    
    function validateUser(user) {
        return user.name && user.email && user.email.includes('@');
    }
    
    function generateId() {
        return nextId++;
    }
    
    // Public API được return
    return {
        addUser(name, email) {
            const user = { name, email };
            
            if (!validateUser(user)) {
                throw new Error('Invalid user data');
            }
            
            user.id = generateId();
            user.createdAt = new Date();
            users.push(user);
            
            return user;
        },
        
        getUsers() {
            // Return copy để prevent mutation
            return users.map(user => ({ ...user }));
        },
        
        getUserById(id) {
            const user = users.find(u => u.id === id);
            return user ? { ...user } : null;
        },
        
        removeUser(id) {
            const index = users.findIndex(u => u.id === id);
            if (index > -1) {
                return users.splice(index, 1)[0];
            }
            return null;
        },
        
        // Debug method
        _debug: {
            getUserCount: () => users.length,
            getNextId: () => nextId
        }
    };
})();

// Sử dụng module
const user1 = UserModule.addUser("John Doe", "john@example.com");
const user2 = UserModule.addUser("Jane Smith", "jane@example.com");

console.log(UserModule.getUsers());
console.log(UserModule._debug.getUserCount()); // 2

// Không thể truy cập private variables
// console.log(users); // ❌ ReferenceError
// console.log(nextId); // ❌ ReferenceError

Ví dụ thực tế 4: Memoization với Closure

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log(`Cache hit for ${key}`);
            return cache.get(key);
        }
        
        console.log(`Computing for ${key}`);
        const result = fn.apply(this, args);
        cache.set(key, result);
        
        return result;
    };
}

// Expensive function
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Memoized version
const memoizedFib = memoize(fibonacci);

console.log(memoizedFib(10)); // Computing... 55
console.log(memoizedFib(10)); // Cache hit! 55
console.log(memoizedFib(11)); // Computing for new input

Ví dụ thực tế 5: Debounce với Closure

function debounce(func, delay) {
    let timeoutId;
    
    return function(...args) {
        // Clear previous timeout
        clearTimeout(timeoutId);
        
        // Set new timeout
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Usage with search input
const searchInput = document.getElementById('search');
const performSearch = (query) => {
    console.log(`Searching for: ${query}`);
    // API call logic here
};

const debouncedSearch = debounce((event) => {
    performSearch(event.target.value);
}, 300);

// searchInput.addEventListener('input', debouncedSearch);

Advanced Closure Patterns

1. Closure với Classes

class Counter {
    constructor(initialValue = 0) {
        // Private field simulation với closure
        let count = initialValue;
        
        this.increment = () => ++count;
        this.decrement = () => --count;
        this.getValue = () => count;
        this.reset = () => count = initialValue;
    }
}

const counter = new Counter(5);
console.log(counter.increment()); // 6
console.log(counter.getValue());  // 6

// count vẫn private
console.log(counter.count); // undefined

2. Factory Functions với Closure

function createValidator(rules) {
    return function(data) {
        const errors = [];
        
        Object.keys(rules).forEach(field => {
            const rule = rules[field];
            const value = data[field];
            
            if (rule.required && !value) {
                errors.push(`${field} is required`);
            }
            
            if (rule.minLength && value && value.length < rule.minLength) {
                errors.push(`${field} must be at least ${rule.minLength} characters`);
            }
            
            if (rule.pattern && value && !rule.pattern.test(value)) {
                errors.push(`${field} format is invalid`);
            }
        });
        
        return {
            isValid: errors.length === 0,
            errors
        };
    };
}

// Tạo validators cho different forms
const userValidator = createValidator({
    name: { required: true, minLength: 2 },
    email: { 
        required: true, 
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 
    }
});

const productValidator = createValidator({
    title: { required: true, minLength: 3 },
    price: { required: true }
});

// Sử dụng
const result1 = userValidator({ name: "Jo", email: "invalid-email" });
console.log(result1); // { isValid: false, errors: [...] }

const result2 = userValidator({ name: "John", email: "john@example.com" });
console.log(result2); // { isValid: true, errors: [] }

3. Async Closures

function createAsyncCounter() {
    let count = 0;
    let pending = false;
    
    return {
        async increment() {
            if (pending) {
                console.log("Operation in progress...");
                return count;
            }
            
            pending = true;
            
            // Simulate async operation
            await new Promise(resolve => setTimeout(resolve, 1000));
            
            count++;
            pending = false;
            
            console.log(`Count incremented to: ${count}`);
            return count;
        },
        
        getValue() {
            return count;
        },
        
        isPending() {
            return pending;
        }
    };
}

const asyncCounter = createAsyncCounter();

// Multiple calls
asyncCounter.increment(); // Will increment after 1s
asyncCounter.increment(); // Will show "Operation in progress..."
asyncCounter.increment(); // Will show "Operation in progress..."

Common Mistakes và Best Practices

Mistake 1: Loops với Closures

// ❌ Common mistake
function createButtons() {
    for (var i = 0; i < 5; i++) {
        const button = document.createElement('button');
        button.textContent = `Button ${i}`;
        
        // Tất cả buttons sẽ log "Button 5"
        button.addEventListener('click', function() {
            console.log(`Button ${i} clicked`);
        });
        
        document.body.appendChild(button);
    }
}

// ✅ Fix 1: Use let instead of var
function createButtonsFixed1() {
    for (let i = 0; i < 5; i++) { // let creates new binding each iteration
        const button = document.createElement('button');
        button.textContent = `Button ${i}`;
        
        button.addEventListener('click', function() {
            console.log(`Button ${i} clicked`); // Correct value
        });
        
        document.body.appendChild(button);
    }
}

// ✅ Fix 2: IIFE closure
function createButtonsFixed2() {
    for (var i = 0; i < 5; i++) {
        (function(index) { // Create closure with parameter
            const button = document.createElement('button');
            button.textContent = `Button ${index}`;
            
            button.addEventListener('click', function() {
                console.log(`Button ${index} clicked`);
            });
            
            document.body.appendChild(button);
        })(i);
    }
}

Mistake 2: Memory Leaks

// ❌ Potential memory leak
function attachHandler() {
    const largeData = new Array(1000000).fill('data');
    
    document.getElementById('button').onclick = function() {
        // Closure keeps reference to largeData even if not used
        console.log('Button clicked');
    };
}

// ✅ Better approach
function attachHandler() {
    const largeData = new Array(1000000).fill('data');
    
    // Process data and extract only what's needed
    const summary = {
        length: largeData.length,
        firstItem: largeData[0]
    };
    
    document.getElementById('button').onclick = function() {
        console.log('Button clicked', summary);
        // largeData can be garbage collected
    };
}

Best Practices

  1. Minimize closure scope
// ❌ Unnecessarily large closure
function createHandler() {
    const config = getComplexConfig();
    const utils = getUtilityFunctions();
    const data = getLargeDataset();
    
    return function(input) {
        // Only uses config
        return config.processInput(input);
    };
}

// ✅ Minimal closure
function createHandler() {
    const config = getComplexConfig();
    
    return function(input) {
        return config.processInput(input);
    };
}
  1. Use descriptive names
// ❌ Unclear closure
function createFunc(x) {
    return function(y) {
        return x + y;
    };
}

// ✅ Clear purpose
function createAdder(baseValue) {
    return function addToBase(value) {
        return baseValue + value;
    };
}

const addFive = createAdder(5);
console.log(addFive(3)); // 8

Performance Considerations

Memory Usage

// Đo memory usage của closures
function measureClosureMemory() {
    const memBefore = process.memoryUsage().heapUsed;
    
    const closures = [];
    for (let i = 0; i < 10000; i++) {
        const data = new Array(1000).fill(i);
        
        closures.push(function() {
            return data[0]; // Closure keeps entire array
        });
    }
    
    const memAfter = process.memoryUsage().heapUsed;
    console.log(`Memory used: ${(memAfter - memBefore) / 1024 / 1024} MB`);
    
    return closures;
}

// measureClosureMemory();

Optimization Techniques

// ✅ Optimized closure pattern
function createOptimizedHandlers(items) {
    // Pre-process data outside closure
    const processedItems = items.map(item => ({
        id: item.id,
        name: item.name
        // Only keep what's needed
    }));
    
    return processedItems.map(item => {
        return function() {
            console.log(`Processing ${item.name}`);
            return item.id;
        };
    });
}

Kết luận

Functions và Closures là những khái niệm cốt lõi của JavaScript, mở ra sức mạnh thực sự của ngôn ngữ này:

🎯 Key Takeaways

  1. Functions are first-class citizens - Có thể assign, pass, return
  2. Arrow functions ≠ regular functions - Khác biệt về this, arguments, constructor
  3. Closures = Function + Lexical Environment - “Nhớ” scope nơi được tạo
  4. Practical applications - Module pattern, private variables, event handlers
  5. Performance matters - Cẩn thận với memory leaks

🚀 Advanced Applications

  • Module Pattern: Tạo private scope
  • Memoization: Cache kết quả tính toán
  • Debouncing/Throttling: Optimize performance
  • Factory Functions: Tạo objects với behavior tùy chỉnh
  • Functional Programming: Higher-order functions

Best Practices

  • Sử dụng arrow functions cho callbacks ngắn
  • Prefer const cho function expressions
  • Minimize closure scope để tránh memory leaks
  • Sử dụng descriptive names cho clarity
  • Hiểu khi nào cần preserve this context
TIP

Master Functions và Closures sẽ giúp bạn viết JavaScript code elegant, efficient và powerful hơn rất nhiều!

Những khái niệm này là foundation để hiểu các patterns nâng cao như Promises, async/await, và functional programming trong JavaScript.

Tài liệu tham khảo