目录
[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');
}
});
}
**优点:**真正的私有性(运行时不可访问)、灵活性强、兼容性好(支持旧浏览器)
**缺点:**内存占用(变量不会释放)、调试困难、性能考虑(每次创建新闭包)、无法继承私有成员