50projects50days JavaScript模式:函数式与面向对象编程
你是否在开发Web应用时纠结于选择函数式编程(Functional Programming, FP)还是面向对象编程(Object-Oriented Programming, OOP)?50projects50days项目通过50个迷你Web应用,展示了JavaScript中两种范式的实战应用。本文将深入分析这些项目中的代码模式,帮你掌握何时选择函数式、何时选择面向对象,以及如何混合使用两种范式解决实际问题。
读完本文你将获得:
- 识别JS项目中函数式与面向对象模式的能力
- 3类典型场景的范式选择决策指南
- 从50个实战项目提炼的8个代码模板
- 混合编程模式的最佳实践
范式对比:核心差异与项目应用
本质区别
| 特性 | 函数式编程 | 面向对象编程 |
|---|---|---|
| 核心单元 | 纯函数 | 类与对象 |
| 状态管理 | 不可变数据 | 封装的可变状态 |
| 代码组织 | 函数组合 | 继承与多态 |
| 适用场景 | 数据处理、管道操作 | 复杂实体、UI组件 |
| 50projects占比 | 约65% | 约35% |
项目中的范式分布
函数式编程:项目实战解析
函数式编程在50projects中占比最高,尤其适合状态独立、逻辑复用的场景。其核心特点是:纯函数、不可变数据、函数组合和声明式代码。
1. 纯函数与数据转换
密码生成器项目展示了纯函数的典型应用,通过独立函数实现不同类型字符的生成:
// 纯函数:无副作用,相同输入始终返回相同输出
function getRandomLower() {
return String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
function getRandomUpper() {
return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}
function getRandomNumber() {
return String.fromCharCode(Math.floor(Math.random() * 10) + 48);
}
这些函数具有以下特点:
- 无外部依赖(不读取/修改全局变量)
- 输出仅由输入决定
- 无副作用(不修改参数或外部状态)
2. 函数组合与高阶函数
随机选择器项目展示了函数组合的威力,通过将多个简单函数组合成复杂逻辑:
// 高阶函数:接收函数作为参数或返回函数
function createTags(input) {
return input
.split(',')
.filter(tag => tag.trim() !== '')
.map(tag => `<span class="tag">${tag.trim()}</span>`)
.join('');
}
// 事件处理中的函数组合
textarea.addEventListener('keyup', (e) => {
const tags = createTags(e.target.value);
tagsEl.innerHTML = tags;
if (e.key === 'Enter') {
setTimeout(() => {
e.target.value = '';
}, 10);
randomSelect();
}
});
3. 不可变数据模式
在计数器项目中,通过替换而非修改实现状态更新:
// 不可变更新:创建新数组而非修改原数组
function updateCounter() {
const target = +counter.getAttribute('data-target');
const count = +counter.innerText;
const increment = target / 200;
if (count < target) {
counter.innerText = Math.ceil(count + increment);
setTimeout(updateCounter, 1);
} else {
counter.innerText = target; // 最终赋值确保精确性
}
}
// 批量处理多个计数器(函数式数据处理)
counters.forEach(counter => {
updateCounter(counter);
});
面向对象编程:类与实例实战
虽然函数式编程占比更高,但面向对象编程在特定场景中表现更优,特别是需要维护内部状态和提供复杂行为的组件。
1. 类与封装
虽然50projects中直接使用class语法的项目较少,但Todo List应用展示了典型的面向对象思想:
// 隐式的对象封装模式
function addTodo(todo) {
let todoText = input.value;
if (todo) {
todoText = todo.text; // 从对象中获取数据
}
if (todoText) {
const todoEl = document.createElement('li');
if (todo && todo.completed) {
todoEl.classList.add('completed'); // 维护完成状态
}
todoEl.innerText = todoText;
// 封装行为:点击切换完成状态
todoEl.addEventListener('click', () => {
todoEl.classList.toggle('completed');
updateLS(); // 状态变更持久化
});
// 右键删除功能
todoEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
todoEl.remove();
updateLS();
});
todosUL.appendChild(todoEl);
input.value = '';
updateLS();
}
}
这个实现虽然没有使用class关键字,但体现了OOP的核心思想:
- 数据(todo.text, completed状态)和行为(toggle, remove)的封装
- 通过方法(updateLS)维护内部状态
- 对象实例(每个todoEl)独立管理自己的状态
2. 原型与继承
测验应用展示了如何通过原型模式实现代码复用:
// 测验数据结构(可视为类的原型定义)
const quizData = [
{
question: "Which language runs in a web browser?",
a: "Java",
b: "C",
c: "Python",
d: "JavaScript",
correct: "d",
},
// 更多问题...
];
// 测验状态管理(类似类实例的状态)
let currentQuiz = 0;
let score = 0;
// 方法定义(类似类的方法)
function loadQuiz() {
deselectAnswers();
const currentQuizData = quizData[currentQuiz];
questionEl.innerText = currentQuizData.question;
a_text.innerText = currentQuizData.a;
// 设置其他选项...
}
function getSelected() {
let answer;
answerEls.forEach(answerEl => {
if(answerEl.checked) {
answer = answerEl.id;
}
});
return answer;
}
混合编程:最佳实践与项目案例
大多数复杂项目需要混合使用两种范式。50projects50days中的最佳实践是:用函数式处理数据,用面向对象管理状态。
1. 面向对象封装 + 函数式数据处理
密码生成器项目完美展示了这种混合模式:
// 函数式部分:纯函数与函数组合
const randomFunc = {
lower: getRandomLower,
upper: getRandomUpper,
number: getRandomNumber,
symbol: getRandomSymbol
};
function generatePassword(lower, upper, number, symbol, length) {
// 函数组合:过滤->映射->归约
let generatedPassword = '';
const typesCount = lower + upper + number + symbol;
const typesArr = [{lower}, {upper}, {number}, {symbol}]
.filter(item => Object.values(item)[0]);
// 函数式循环:避免直接修改变量
for(let i = 0; i < length; i += typesCount) {
typesArr.forEach(type => {
const funcName = Object.keys(type)[0];
generatedPassword += randomFunc[funcName]();
});
}
return generatedPassword.slice(0, length);
}
// 面向对象部分:事件监听与状态管理
generateEl.addEventListener('click', () => {
const length = +lengthEl.value;
const hasLower = lowercaseEl.checked;
const hasUpper = uppercaseEl.checked;
const hasNumber = numbersEl.checked;
const hasSymbol = symbolsEl.checked;
resultEl.innerText = generatePassword(
hasLower, hasUpper, hasNumber, hasSymbol, length
);
});
2. 函数式管道 + 类封装
复杂表单处理可以采用这种模式:
场景决策指南:何时选择哪种范式
选择函数式编程当:
-
数据转换管道:如密码生成、数据过滤和格式化
// 示例:数据处理管道 function processUserData(users) { return users .filter(user => user.active) .map(user => ({ id: user.id, fullName: `${user.firstName} ${user.lastName}`, age: calculateAge(user.birthdate) })) .sort((a, b) => a.age - b.age); } -
无状态工具函数:如DOM操作、数学计算
// 示例:纯工具函数 const scale = (num, in_min, in_max, out_min, out_max) => { return (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }; -
事件处理逻辑:如按钮点击、表单提交
// 示例:事件处理函数组合 next.addEventListener('click', () => { if (currentActive >= circles.length) return; currentActive++; update(); });
选择面向对象编程当:
-
复杂状态管理:如测验进度、游戏状态
// 伪代码:游戏状态管理 class Game { constructor() { this.score = 0; this.level = 1; this.enemies = []; this.player = { x: 0, y: 0, health: 100 }; } start() { /* ... */ } update() { /* ... */ } render() { /* ... */ } gameOver() { /* ... */ } } -
UI组件封装:如自定义表单控件、交互组件
// 伪代码:自定义组件 class Dropdown { constructor(element) { this.element = element; this.items = element.querySelectorAll('.dropdown-item'); this.isOpen = false; this.bindEvents(); } bindEvents() { this.element.addEventListener('click', () => this.toggle()); } toggle() { this.isOpen = !this.isOpen; this.element.classList.toggle('open', this.isOpen); } } -
可复用实体:如待办事项、用户账户
// 伪代码:实体类 class TodoItem { constructor(text) { this.id = Date.now(); this.text = text; this.completed = false; this.createdAt = new Date(); } toggleComplete() { this.completed = !this.completed; this.updatedAt = new Date(); } toJSON() { return { id: this.id, text: this.text, completed: this.completed, createdAt: this.createdAt, updatedAt: this.updatedAt }; } }
常见问题与解决方案
Q1: 如何在函数式项目中处理状态?
A: 使用不可变更新模式和状态容器:
// 不可变状态更新
function updateState(oldState, newData) {
return {
...oldState,
...newData,
// 嵌套对象也需要复制
user: {
...oldState.user,
...newData.user
}
};
}
// 状态管理容器
const appState = {
theme: 'light',
user: { name: '', preferences: {} },
todos: []
};
// 状态更新函数
function setTheme(theme) {
appState = updateState(appState, { theme });
renderUI(appState);
}
Q2: 如何避免OOP中的继承层级过深?
A: 使用组合优于继承原则:
// 继承方式(不推荐)
class SpecialButton extends Button {
constructor() {
super();
this.hasIcon = true;
this.hasDropdown = true;
}
// ...大量方法
}
// 组合方式(推荐)
class Button {
constructor(options = {}) {
this.elements = {};
this.components = [];
if (options.hasIcon) {
this.components.push(new IconComponent());
}
if (options.hasDropdown) {
this.components.push(new DropdownComponent());
}
}
render() {
const button = document.createElement('button');
this.components.forEach(component => {
button.appendChild(component.render());
});
return button;
}
}
Q3: 混合编程时如何保持代码清晰?
A: 遵循明确的边界划分:
// 清晰分离数据处理(函数式)和状态管理(OOP)
class UserManager {
constructor() {
this.users = [];
this.loadUsers();
}
// OOP: 状态管理方法
loadUsers() {
const data = localStorage.getItem('users');
this.users = data ? JSON.parse(data) : [];
}
saveUsers() {
localStorage.setItem('users', JSON.stringify(this.users));
}
// 混合: 使用函数式处理数据
getActiveUsers() {
return filterActiveUsers(this.users);
}
searchUsers(query) {
return searchUsers(this.users, query);
}
}
// 纯函数: 数据处理(函数式)
function filterActiveUsers(users) {
return users.filter(user => user.active && !user.deleted);
}
function searchUsers(users, query) {
const lowerQuery = query.toLowerCase();
return users.filter(user =>
user.name.toLowerCase().includes(lowerQuery) ||
user.email.toLowerCase().includes(lowerQuery)
);
}
总结与下一步
50projects50days项目展示了现代JavaScript开发的真实面貌:不再是纯粹的函数式或面向对象,而是根据问题选择合适的范式。优秀的JS开发者应当:
- 掌握两种范式的核心原则:纯函数、不可变性、封装和多态
- 根据场景灵活选择:数据处理用函数式,复杂实体用面向对象
- 混合编程时保持清晰边界:数据层与状态管理层分离
进阶学习路线
- 函数式进阶:学习Ramda或Lodash库,掌握curry、compose等高级概念
- 面向对象深入:学习TypeScript类、接口和设计模式
- 状态管理:研究Redux(函数式)和Vuex(OOP)的实现原理
实用工具推荐
- 函数式编程:Ramda, Lodash/fp
- 不可变数据:Immer, Immutable.js
- 类型检查:TypeScript(提升两种范式的代码质量)
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



