Learning Objectives

  • Understand data privacy and why it matters
  • Create private variables using closures
  • Build public interfaces to private data
  • Implement the revealing module pattern

Why Data Privacy Matters

In JavaScript, object properties are public by default. Anyone can access and modify them:

const user = {
    name: "Alice",
    balance: 1000
};

// Anyone can modify the balance!
user.balance = 999999;
console.log(user.balance); // 999999 - Oops!

Closures allow us to create truly private variables that can only be accessed through controlled methods.

Creating Private Variables

The basic pattern for private variables:

function createUser(name, initialBalance) {
    // Private variables
    let balance = initialBalance;
    const accountNumber = Math.random().toString(36).substr(2, 9);
    
    // Public interface
    return {
        getName: function() {
            return name;
        },
        
        getBalance: function() {
            return balance;
        },
        
        deposit: function(amount) {
            if (amount > 0) {
                balance += amount;
                return true;
            }
            return false;
        },
        
        withdraw: function(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return true;
            }
            return false;
        },
        
        getAccountNumber: function() {
            // Return masked version
            return `***${accountNumber.slice(-4)}`;
        }
    };
}

const alice = createUser("Alice", 1000);
console.log(alice.getBalance()); // 1000
alice.deposit(500);
console.log(alice.getBalance()); // 1500

// Can't access private variables directly
console.log(alice.balance); // undefined
console.log(alice.accountNumber); // undefined

Practical Example: Configuration Manager

Use closures to create a configuration object with private settings:

function createConfig() {
    // Private configuration
    const config = {
        apiKey: "secret-key-12345",
        apiUrl: "https://api.example.com",
        timeout: 5000,
        retries: 3
    };
    
    // Private validation
    function isValidTimeout(value) {
        return typeof value === 'number' && value > 0 && value < 30000;
    }
    
    // Public interface
    return {
        getApiUrl: function() {
            return config.apiUrl;
        },
        
        getTimeout: function() {
            return config.timeout;
        },
        
        setTimeout: function(value) {
            if (isValidTimeout(value)) {
                config.timeout = value;
                return true;
            }
            return false;
        },
        
        getRetries: function() {
            return config.retries;
        },
        
        // Never expose the API key directly
        hasApiKey: function() {
            return config.apiKey !== null;
        }
    };
}

const appConfig = createConfig();
console.log(appConfig.getTimeout()); // 5000
appConfig.setTimeout(10000);
console.log(appConfig.getTimeout()); // 10000

// Can't access the API key!
console.log(appConfig.apiKey); // undefined

Practical Example: Password Manager

Create a secure password manager with private storage:

function createPasswordManager(masterPassword) {
    // Private storage
    const passwords = {};
    let isUnlocked = false;
    
    // Private helper
    function hash(str) {
        // Simplified hash (use proper hashing in production!)
        return btoa(str);
    }
    
    return {
        unlock: function(password) {
            if (hash(password) === hash(masterPassword)) {
                isUnlocked = true;
                return true;
            }
            return false;
        },
        
        lock: function() {
            isUnlocked = false;
        },
        
        addPassword: function(service, password) {
            if (!isUnlocked) {
                return "Manager is locked";
            }
            passwords[service] = hash(password);
            return "Password saved";
        },
        
        getPassword: function(service) {
            if (!isUnlocked) {
                return "Manager is locked";
            }
            return passwords[service] ? "Password exists" : "No password found";
        },
        
        isLocked: function() {
            return !isUnlocked;
        }
    };
}

const manager = createPasswordManager("master123");
console.log(manager.isLocked()); // true

manager.unlock("master123");
manager.addPassword("email", "mypass123");
console.log(manager.getPassword("email")); // "Password exists"

manager.lock();
console.log(manager.getPassword("email")); // "Manager is locked"

// Can't access passwords directly!
console.log(manager.passwords); // undefined

The Revealing Module Pattern

A popular pattern that defines all functions privately and reveals only what's needed:

const calculator = (function() {
    // Private variables and functions
    let result = 0;
    
    function validateNumber(n) {
        return typeof n === 'number' && !isNaN(n);
    }
    
    function add(n) {
        if (validateNumber(n)) {
            result += n;
        }
        return this;
    }
    
    function subtract(n) {
        if (validateNumber(n)) {
            result -= n;
        }
        return this;
    }
    
    function multiply(n) {
        if (validateNumber(n)) {
            result *= n;
        }
        return this;
    }
    
    function divide(n) {
        if (validateNumber(n) && n !== 0) {
            result /= n;
        }
        return this;
    }
    
    function getResult() {
        return result;
    }
    
    function reset() {
        result = 0;
        return this;
    }
    
    // Reveal public interface
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide,
        getResult: getResult,
        reset: reset
    };
})();

calculator.add(10).multiply(2).subtract(5);
console.log(calculator.getResult()); // 15

// Can't access private functions
console.log(calculator.validateNumber); // undefined
console.log(calculator.result); // undefined

Private Methods

You can also have private methods that are only used internally:

function createLogger(prefix) {
    // Private method
    function formatMessage(level, message) {
        const timestamp = new Date().toISOString();
        return `[${timestamp}] [${prefix}] [${level}] ${message}`;
    }
    
    // Public methods
    return {
        info: function(message) {
            console.log(formatMessage('INFO', message));
        },
        
        warn: function(message) {
            console.warn(formatMessage('WARN', message));
        },
        
        error: function(message) {
            console.error(formatMessage('ERROR', message));
        }
    };
}

const logger = createLogger('MyApp');
logger.info('Application started');
logger.warn('Low memory');
logger.error('Connection failed');

// Can't call formatMessage directly
// logger.formatMessage('DEBUG', 'test'); // Error!

Benefits of Data Privacy

  • Security: Sensitive data can't be accessed directly
  • Validation: All modifications go through controlled methods
  • Encapsulation: Implementation details are hidden
  • Maintainability: Internal changes don't affect public API
  • Predictability: Data can only change in expected ways

Key Takeaways

  • ✅ Closures enable true data privacy in JavaScript
  • ✅ Private variables can only be accessed through public methods
  • ✅ The revealing module pattern exposes only what's necessary
  • ✅ Private methods can be used for internal logic
  • ✅ Data privacy improves security and maintainability

Next Steps

Now that you understand data privacy with closures, in the next lesson we'll explore factory functions - a powerful pattern for creating multiple objects with private state.