JavaScript封装演进史:从全局变量到闭包

目录

一、什么是封装?

二、为什么需要封装?

三、JavaScript封装的历史演进

[3.1 第一阶段:无封装时代(ES3之前)](#3.1 第一阶段:无封装时代(ES3之前))

[3.2 命名空间模式](#3.2 命名空间模式)

[3.3 函数作用域与闭包的出现](#3.3 函数作用域与闭包的出现)

[3.4 模块化标准(CommonJS、AMD、ES6 Modules)](#3.4 模块化标准(CommonJS、AMD、ES6 Modules))

[四、JavaScript封装利器 - 闭包](#四、JavaScript封装利器 - 闭包)

[4.1 闭包的工作原理](#4.1 闭包的工作原理)

[4.2 使用闭包的时机](#4.2 使用闭包的时机)

[4.2.1 数据封装与私有变量](#4.2.1 数据封装与私有变量)

[4.2.2 函数工厂(创建定制化函数)](#4.2.2 函数工厂(创建定制化函数))

[4.2.3 回调函数需要访问创建时上下文](#4.2.3 回调函数需要访问创建时上下文)

[4.2.4 实现记忆化(Memoization)](#4.2.4 实现记忆化(Memoization))

[4.2.5 实现部分应用和柯里化](#4.2.5 实现部分应用和柯里化)

[4.2.6 模块模式(ES6模块前的模块化)](#4.2.6 模块模式(ES6模块前的模块化))

[4.2.7 React Hooks和现代框架](#4.2.7 React Hooks和现代框架)

[4.3 闭包使用中的性能注意事项](#4.3 闭包使用中的性能注意事项)


一、什么是封装?

封装是指一种通过接口抽象将具体实现包装并隐藏起来的方法,具体来说,包括:

  • 限制对对象内部组件直接访问的机制;
  • 将数据和方法绑定起来,对外提供方法,从而改变对象状态的机制;

二、为什么需要封装?

  • 提高代码的安全性:通过限制对类内部数据的直接访问,防止外部代码随意修改关键数据。
  • 降低系统耦合:封装后,类的内部实现细节对外部透明。修改内容逻辑时,只要接口不变,外部代码无需调整。

三、JavaScript封装的历史演进

3.1 第一阶段:无封装时代(ES3之前)

这个阶段的特点是:通常以简单的脚本形式嵌入HTML,全局变量泛滥,容易造成命名冲突。

javascript 复制代码
// 直接在全局作用域定义变量和函数
var count = 0;
var userName = '';

function increment() {
    count++;
}

function setUserName(name) {
    userName = name;
}

// 多个脚本文件中的同名变量和函数会相互覆盖

3.2 命名空间模式

随着Web应用复杂度增加,为了避免全局变量污染,开发者开始使用对象来模拟命名空间。

这个阶段的特点是:将函数和变量封装到对象中,减少全局变量的数量。

javascript 复制代码
// 创建一个全局对象作为命名空间
var MyApp = MyApp || {};

// 在命名空间下定义变量和函数
MyApp.count = 0;
MyApp.userName = '';

MyApp.increment = function() {
    MyApp.count++;
};

MyApp.setUserName = function(name) {
    MyApp.userName = name;
};

// 使用
MyApp.setUserName('Alice');
MyApp.increment();

3.3 函数作用域与闭包的出现

到2006年,jQuery发布,开始广泛使用了闭包来封装代码。

这个阶段的特点是:利用IIFE(立即执行函数表达式)创建私有作用域,通过闭包保护私有变量,并暴露公共接口。

javascript 复制代码
// 使用IIFE和闭包创建模块
var Module = (function() {
    // 私有变量
    var privateVar = 0;
    
    // 私有函数
    function privateMethod() {
        return privateVar;
    }
    
    // 公共接口
    return {
        publicMethod: function() {
            // 可以访问私有变量和函数
            privateVar++;
            return privateMethod();
        },
        anotherPublicMethod: function() {
            return privateVar;
        }
    };
})();

// 使用
Module.publicMethod(); // 返回1
Module.anotherPublicMethod(); // 返回1

3.4 模块化标准(CommonJS、AMD、ES6 Modules)

随着JavaScript应用规模不断扩大,出现了多种模块化规范。典型的时间包括:

  • 2009年,Node.js采用CommonJS模块规范,使得服务器端JavaScript能够方便地组织代码。
  • 2011年左右,RequireJS实现了AMD规范,适用于浏览器端的异步模块加载。
  • 2015年,ECMAScript 2015(ES6)正式引入了模块系统,成为JavaScript语言的标准模块化方案。

典型的代码示例如下:

1. CommonJS(2009年)

javascript 复制代码
// math.js
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = {
    add: add,
    subtract: subtract
};

// app.js
var math = require('./math');
console.log(math.add(2, 3)); // 5

2. AMD(Asynchronous Module Definition,2011年左右)

javascript 复制代码
// 定义模块
define(['dependency1', 'dependency2'], function(dep1, dep2) {
    // 模块代码
    function add(a, b) {
        return a + b;
    }
    
    return {
        add: add
    };
});

// 使用模块
require(['math'], function(math) {
    console.log(math.add(2, 3));
});

3. ES6 Modules(2015年至今)

javascript 复制代码
// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

// app.js
import { add, subtract } from './math.js';
console.log(add(2, 3)); // 5

四、JavaScript封装利器 - 闭包

4.1 闭包的工作原理

函数在创建时记录下当前的词法环境([[Environment]]属性),当函数执行时,通过这个记录访问创建时的作用域,即使那个作用域的执行上下文已经销毁,但其词法环境被保留在内存中,从而实现了跨作用域的变量访问。

4.2 使用闭包的时机

4.2.1 数据封装与私有变量

时机:需要隐藏实现细节,只暴露必要接口时

javascript 复制代码
// ✅ 正确使用:创建具有私有状态的银行账户
function createBankAccount(initialBalance) {
    let balance = initialBalance;  // 真正私有的
    
    return {
        deposit: (amount) => {
            if (amount > 0) {
                balance += amount;
                return true;
            }
            return false;
        },
        
        withdraw: (amount) => {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return amount;
            }
            return 0;
        },
        
        getBalance: () => balance  // 只读访问
    };
}

// 使用
const account = createBankAccount(1000);
account.deposit(500);           // ✅ 允许
console.log(account.balance);   // ❌ undefined,无法直接访问
console.log(account.getBalance()); // ✅ 1500,通过接口访问

// ❌ 替代方案:使用类但没有真正的私有(ES2022前)
class BankAccount {
    constructor(balance) {
        this.balance = balance;  // 公开的!
    }
    // 任何人都可以修改 account.balance
}

4.2.2 函数工厂(创建定制化函数)

时机:需要基于不同参数创建功能相似但配置不同的函数

javascript 复制代码
// ✅ 正确使用:创建各种数学运算函数
function createMathOperation(operator) {
    const operations = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b,
        multiply: (a, b) => a * b,
        divide: (a, b) => a / b
    };
    
    // 返回定制化的函数
    return function(a, b) {
        const operation = operations[operator];
        if (!operation) throw new Error(`Unknown operator: ${operator}`);
        
        // 可以在这里添加通用逻辑
        console.log(`Performing ${operator} on ${a} and ${b}`);
        return operation(a, b);
    };
}

// 创建不同的函数实例
const add = createMathOperation('add');
const multiply = createMathOperation('multiply');

console.log(add(5, 3));       // 8
console.log(multiply(5, 3));  // 15

// 现实场景:创建不同级别的日志函数
function createLogger(level) {
    const timestamp = () => new Date().toISOString();
    
    return function(message, data = {}) {
        const logEntry = {
            level,
            timestamp: timestamp(),
            message,
            data
        };
        
        if (level === 'ERROR') {
            console.error(JSON.stringify(logEntry));
        } else if (level === 'WARN') {
            console.warn(JSON.stringify(logEntry));
        } else {
            console.log(JSON.stringify(logEntry));
        }
    };
}

const errorLog = createLogger('ERROR');
const debugLog = createLogger('DEBUG');
errorLog('Database connection failed', { code: 'DB_001' });

4.2.3 回调函数需要访问创建时上下文

时机:异步回调、事件处理器需要记住创建时的数据

javascript 复制代码
// ✅ 正确使用:DOM事件处理
function setupButtons() {
    const buttons = document.querySelectorAll('.action-btn');
    
    buttons.forEach((button, index) => {
        // 每个闭包记住自己的index和button
        button.addEventListener('click', function() {
            // 需要访问创建时的index和button
            console.log(`Button ${index} clicked`);
            console.log('Button text:', this.textContent);
            
            // 可以访问外部作用域的变量
            highlightButton(index);
        });
    });
    
    function highlightButton(idx) {
        buttons[idx].classList.add('highlight');
    }
}

// ✅ 正确使用:异步请求需要上下文
function fetchUserData(userId, apiKey) {
    // 闭包记住userId和apiKey
    return fetch(`/api/users/${userId}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
    })
    .then(response => response.json())
    .then(data => {
        // 回调中仍然可以访问userId
        console.log(`Data for user ${userId}:`, data);
        return { userId, data };
    });
}

// ❌ 不使用闭包的糟糕替代
let globalUserId;  // 污染全局
function fetchUserDataBad(userId) {
    globalUserId = userId;  // 临时存储
    return fetch(`/api/users/${userId}`)
        .then(response => response.json())
        .then(data => {
            console.log(`Data for user ${globalUserId}:`, data);
            // 如果多个请求并发,globalUserId会被覆盖
        });
}

4.2.4 实现记忆化(Memoization)

时机:需要缓存昂贵函数调用结果时

javascript 复制代码
// ✅ 正确使用:缓存计算昂贵的结果
function memoize(fn) {
    const cache = new Map();  // 闭包保存缓存
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log('Cache hit!');
            return cache.get(key);
        }
        
        console.log('Calculating...');
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// 使用:缓存斐波那契计算
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);

// 第一次计算
console.time('First');
console.log(memoizedFibonacci(40));  // 计算
console.timeEnd('First');            // ~800ms

// 第二次相同的调用
console.time('Second');
console.log(memoizedFibonacci(40));  // 从缓存返回
console.timeEnd('Second');           // ~0.1ms

4.2.5 实现部分应用和柯里化

时机:需要预先设置部分参数的函数

javascript 复制代码
// ✅ 正确使用:创建通用的HTTP请求函数
function createRequest(baseURL, defaultHeaders) {
    // 闭包记住基础配置
    return function(endpoint, options = {}) {
        const url = `${baseURL}${endpoint}`;
        const headers = { ...defaultHeaders, ...options.headers };
        
        return fetch(url, { ...options, headers })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                return response.json();
            });
    };
}

// 创建特定API的请求函数
const githubAPI = createRequest('https://api.github.com', {
    'Accept': 'application/vnd.github.v3+json'
});

const githubUserAPI = createRequest('https://api.github.com/users', {
    'Accept': 'application/vnd.github.v3+json'
});

// 使用
githubAPI('/repos/facebook/react')
    .then(data => console.log(data));

githubUserAPI('/octocat')
    .then(data => console.log(data));

4.2.6 模块模式(ES6模块前的模块化)

时机:在没有ES6模块的环境中实现模块化(如旧项目)

javascript 复制代码
// ✅ 正确使用:旧项目的模块封装
var MyApp = (function() {
    // 私有变量
    var privateData = [];
    var config = { debug: true };
    
    // 私有函数
    function privateHelper(data) {
        if (config.debug) {
            console.log('Processing:', data);
        }
        return data.toUpperCase();
    }
    
    // 公共接口
    return {
        addItem: function(item) {
            const processed = privateHelper(item);
            privateData.push(processed);
            return processed;
        },
        
        getCount: function() {
            return privateData.length;
        },
        
        // 配置方法
        setDebug: function(enabled) {
            config.debug = enabled;
        }
    };
})();

// 使用
MyApp.addItem('test');  // 处理并存储
console.log(MyApp.getCount());  // 1

4.2.7 React Hooks和现代框架

时机:在React函数组件中使用状态和副作用

javascript 复制代码
// ✅ 正确使用:React自定义Hook
function useLocalStorage(key, initialValue) {
    // 闭包记住key和初始值
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(error);
            return initialValue;
        }
    });
    
    // 返回的函数是闭包,可以访问key和setStoredValue
    const setValue = (value) => {
        try {
            const valueToStore = 
                value instanceof Function ? value(storedValue) : value;
            
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(error);
        }
    };
    
    return [storedValue, setValue];
}

// 在组件中使用
function MyComponent() {
    // 每个组件实例有自己的闭包
    const [name, setName] = useLocalStorage('name', '');
    
    return (
        <input 
            value={name}
            onChange={(e) => setName(e.target.value)}  // 闭包访问setName
        />
    );
}

4.3 闭包使用中的性能注意事项

javascript 复制代码
// 避免的问题:
// 1. 意外捕获大对象
function avoidLargeCapture() {
    const largeData = getLargeData();
    const smallPiece = largeData[0];
    
    // 错误:闭包捕获了整个largeData
    // return () => console.log(smallPiece);
    
    // 正确:只传递需要的部分
    return () => console.log(smallPiece);
}

// 2. 循环中创建大量闭包
function optimizeLoops() {
    const elements = document.querySelectorAll('.item');
    
    // 不佳:每个循环都创建新闭包
    // for (let i = 0; i < elements.length; i++) {
    //     elements[i].addEventListener('click', () => {
    //         console.log(`Item ${i} clicked`);
    //     });
    // }
    
    // 更好:使用事件委托
    document.addEventListener('click', (e) => {
        if (e.target.classList.contains('item')) {
            console.log('Item clicked');
        }
    });
}

**优点:**真正的私有性(运行时不可访问)、灵活性强、兼容性好(支持旧浏览器)

**缺点:**内存占用(变量不会释放)、调试困难、性能考虑(每次创建新闭包)、无法继承私有成员

相关推荐
在掘金801105 小时前
RequireJS 详解
前端·javascript
cindershade5 小时前
我对防抖(Debounce)的一点理解与实践:从基础到立即执行
javascript
Coder_Boy_5 小时前
【DDD领域驱动开发】基础概念和企业级项目规范入门简介
java·开发语言·人工智能·驱动开发
spencer_tseng5 小时前
jquery.min.js v1.12.4
javascript·jquery
CoderYanger5 小时前
A.每日一题——3606. 优惠券校验器
java·开发语言·数据结构·算法·leetcode
飛6795 小时前
玩转 Flutter 自定义 Painter:从零打造丝滑的仪表盘动效与可视化图表
开发语言·javascript·flutter
利剑 -~5 小时前
设计java高并安全类
java·开发语言
CoderYanger5 小时前
D.二分查找-基础——744. 寻找比目标字母大的最小字母
java·开发语言·数据结构·算法·leetcode·职场和发展
柯南二号5 小时前
【后端】【Java】一文详解Spring Boot 统一日志与链路追踪实践
java·开发语言·数据库