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

目录

一、什么是封装?

二、为什么需要封装?

三、JavaScript封装的历史演进

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

3.2 命名空间模式

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

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

四、JavaScript封装利器 - 闭包

4.1 闭包的工作原理

4.2 使用闭包的时机

4.2.1 数据封装与私有变量

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

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

4.2.4 实现记忆化(Memoization)

4.2.5 实现部分应用和柯里化

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

4.2.7 React Hooks和现代框架

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


一、什么是封装?

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

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

二、为什么需要封装?

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

三、JavaScript封装的历史演进

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

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

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

function increment() {
    count++;
}

function setUserName(name) {
    userName = name;
}

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

3.2 命名空间模式

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

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

// 创建一个全局对象作为命名空间
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(立即执行函数表达式)创建私有作用域,通过闭包保护私有变量,并暴露公共接口。

// 使用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年)

// 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年左右)

// 定义模块
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年至今)

// 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 数据封装与私有变量

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

// ✅ 正确使用:创建具有私有状态的银行账户
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 函数工厂(创建定制化函数)

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

// ✅ 正确使用:创建各种数学运算函数
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 回调函数需要访问创建时上下文

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

// ✅ 正确使用: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)

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

// ✅ 正确使用:缓存计算昂贵的结果
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 实现部分应用和柯里化

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

// ✅ 正确使用:创建通用的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模块的环境中实现模块化(如旧项目)

// ✅ 正确使用:旧项目的模块封装
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函数组件中使用状态和副作用

// ✅ 正确使用: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 闭包使用中的性能注意事项

// 避免的问题:
// 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');
        }
    });
}

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

morning_judger

您的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值