编程范式(Programming Paradigms)是编程的一种风格或方法,它定义了代码的结构和组织方式。编程范式提供了不同的思考和解决问题的角度,影响着程序员如何编写代码。
常见的前端编程范式包括:
- 命令式编程(Imperative Programming)
- 声明式编程(Declarative Programming)
- 函数式编程(Functional Programming)
- 面向对象编程(Object-Oriented Programming)
命令式编程(Imperative Programming) - 关注"如何做"(How)
特点:关注"如何做"(How),核心思想是通过一系列明确的指令(如变量赋值、循环、条件判断)来改变程序的状态,逐步引导计算机完成任务。它强调 "过程" 和 "状态变化",就像给计算机一步步下达操作命令。
然后,看看下面的代码属于命令式编程么?
javascript
const isEven = (number) => number % 2 === 0;
const square = (number) => number * number;
const add = (a, b) => a + b;
function sumOfEvenSquares(numbers) {
// 声明变量保存中间状态(总和)
let sum = 0;
// 循环遍历数组,(明确的步骤:逐个检查元素,并进行处理)
numbers.forEach((number) => {
// 条件判断,筛选偶数
if (isEven(number)) {
// 修改状态(总和)
sum += square(number);
}
});
// 返回结果
return sum;
}
// 使用
console.log(sumOfEvenSquares([1, 2, 3, 4, 5, 6])); // 56
答案:上面是命令式编程。
以前我粗浅的以为,看到forEach就是声明式编程,看到提取函数以为是函数式编程,其实还是命令式编程,因为sum在被显示的修改。
其整个流程依然是 "初始化 sum → 循环数组 → 逐个判断 → 累加偶数 → 返回结果"
重新梳理下命令式编程的特点:
- 步骤化指令(关注步骤,明确每一步的执行过程)
- 依赖显式状态修改(状态在显示的修改,比如sum在显示的被修改)
- 控制流由开发者主导(开发者控制程序的执行流程,比如for循环、if判断等)
场景和优缺点:
- 适合场景:适合简单、步骤明确的逻辑的场景
- 优势:直观、可控性强
- 缺点:复杂逻辑下代码容易冗长(如多层嵌套循环)
类比炒西红柿炒鸡蛋
你手里有一份《番茄炒蛋盖浇饭步骤说明书》,严格按步骤执行:
- 拿出 2 个番茄,洗净,切成小块(明确 "拿、洗、切" 的动作);
- 拿出 2 个鸡蛋,打入碗中,用筷子搅拌均匀(明确 "打、搅" 的动作); 开火,锅烧热后倒入 2 勺油(明确 "开火、加热、倒油");
- 倒入鸡蛋液,翻炒至凝固,盛出备用(明确 "倒、炒、盛");
- 锅里再倒 1 勺油,放入番茄块,翻炒出汁(明确 "倒、放、炒");
- 倒入炒好的鸡蛋,加 1 勺盐、半勺糖,翻炒均匀(明确 "倒、加、炒");
- 拿出一碗米饭,盛在盘子里,把炒好的番茄炒蛋浇在米饭上(明确 "拿、盛、浇")。
你自己控制整个流程,自己把控每一步的执行过程。
但是注意,每个步骤有可能会涉及其他的范式。
声明式编程(Declarative Programming) - 关注"做什么"(What)
特点 :其实这个是相对于命令式编程来说的,命令式编程是关注"如何做"(How),描述具体的实现步骤,而声明式编程是关注"做什么"(What),开发者只需描述 "想要的结果 / 目标",而无需编写具体的执行步骤、控制流(循环、分支)或手动管理状态,具体的实现细节(如遍历、状态维护)由语言或框架自动完成。
看下同样的功能,使用声明式编程的代码:
javascript
const isEven = (n) => n % 2 === 0;
const square = (n) => n * n;
const sum = (acc, curr) => acc + curr;
const sumOfEvenSquares = (numbers) =>
numbers
.filter(isEven) // 描述:筛选偶数(不关心如何遍历)
.map(square) // 描述:计算平方(不关心如何处理)
.reduce(sum, 0); // 描述:累加求和(不关心如何维护状态)
// 使用
console.log(sumOfEvenSquares([1, 2, 3, 4, 5, 6])); // 56
看着好像也是一步步的,但这些 "步骤" 是 "目标的分解",而非 "执行的指令"。
第一步:我需要"筛选出偶数"这个结果,而不是具体的如何遍历、如何判断偶数。 第二步:我需要"计算平方"这个结果,而不是具体的如何计算平方。 第三步:我需要"累加求和"这个结果,而不是具体的如何累加求和。
每一步都是对 "中间结果" 的描述,而非 "如何实现这个中间结果" 的指令。你不需要告诉计算机 "如何筛选偶数"(是否用 for 循环、forEach 还是其他方式),filter 内部已经封装了遍历逻辑。 同样,你不需要手动维护 "筛选后的数组""平方后的数组" 这些中间状态,它们由函数自动生成并传递。也就是底层的遍历和状态维护完全封装。
看下sql的例子加深理解:
sql
-- 目标:查询"年龄大于18的用户姓名",不关心数据库如何检索
SELECT name FROM users WHERE age > 18;
-- 命令式思路会是:"打开数据库→遍历所有用户→判断年龄→收集姓名",而 SQL 直接描述结果。
场景和优缺点:
- 适合场景:数据查询、复杂计算逻辑
- 优势:可读性高、可维护性强、抽象程度高
- 缺点:可能产生中间数组,内存占用较大
注意,声明式是一个 "大范式",包含多个具体的子范式,覆盖不同场景,常见的子范式包括:
- 函数式编程(FP):用纯函数组合描述计算,无副作用、不可变数据(如 JavaScript 的 filter+map+reduce、Haskell)。
- 逻辑编程:通过 "事实 + 规则" 推导结果,而非步骤(如 Prolog)。
- 数据查询语言:描述需要的数据,而非获取数据的过程(如 SQL、GraphQL)。
- 响应式编程:基于数据流和变化传播处理异步(如 RxJS)。
- 标记语言:描述 "内容结构",而非 "渲染步骤"(如 HTML、XML)。
这些子范式的共性是 "声明目标,不写步骤",差异是 "目标类型不同"(计算、推理、数据、异步、结构)。
类比去餐馆点炒西红柿炒鸡蛋
你去餐馆,直接跟服务员说:"我要一份番茄炒蛋盖浇饭,米饭要软硬适中,番茄要炒出汁,少盐少糖。" 你完全不用管:厨师是先炒鸡蛋还是先炒番茄、用多少油、炒多久 ------ 只需要告诉 "最终需求",厨师会自己处理所有步骤。
函数式编程(Functional Programming)- 函数
特点 :一种以 "函数" 为核心的声明式编程范式,其核心思想是将计算过程视为 "纯函数的组合",强调无副作用、不可变数据、函数是 "第一公民" 等特性,通过函数的嵌套和组合 来解决问题,而非通过修改状态 或执行步骤。
注意,函数式编程是声明式编程的子范式,所以如果是函数式编程的,一定属于声明式编程。反过来不一定成立。
所以声明式编程里面的求和代码,也属于函数式编程。
函数式编程的核心原则:
- 纯函数(相同输入必产相同输出;不修改函数外部的任何状态(如全局变量、参数对象、DOM 等),也不依赖外部状态的变化)
- 不可变数据(数据一旦创建就不可修改,只能通过创建新的数据来修改)
- 函数是 "第一公民"(函数可以作为参数传递、可以作为返回值返回、可以作为变量赋值)
函数式编程的常见工具:
- 高阶函数(如 map、filter、reduce、flatMap等,用于组合数据处理逻辑)
- 柯里化(currying,将多参数函数转化为一系列单参数函数的过程,便于复用和组合)
- 函数组合(compose,将多个函数组合成一个函数,执行顺序从右到左或者从右到左)
场景和优缺点:
- 适合场景:适合数据处理、逻辑处理、函数组合的场景
- 优势:代码可测试性强、易于并行化、函数可复用、减少 bug
- 缺点:学习曲线陡峭、可能产生性能开销、不适合所有场景
拆解React的核心设计,加深函数式编程的理解
React并非 "纯函数式框架",而是选择性吸收了函数式编程(FP)的核心思想,并结合前端开发的实际场景(如 UI 状态管理、组件复用、副作用处理)进行落地。其核心目的是:通过 FP 的特性(纯函数、不可变数据、无副作用)解决前端开发的经典痛点(如状态混乱、组件复用复杂、调试困难),让代码更可预测、可维护。
从 React 的核心设计和实践出发,拆解它如何 "吸收 FP 思想",从而加深对函数式编程的理解。
用 "纯函数组件" 替代 "类组件"(React 16.8+ Hooks 时代)
函数式编程的核心是 "纯函数",React 把这个思想落地为 函数组件(Function Component)+ Hooks,替代了早期的类组件(Class Component)。
React 函数组件本质是一个 "输入 → 输出" 的纯函数:
- 输入:props(父组件传递的参数)+ state(组件内部状态,通过 Hooks 管理);
- 输出:UI 描述(JSX);
- 无副作用(理想状态):组件本身只负责 "根据输入渲染 UI",不直接操作 DOM、不修改外部状态、不发起网络请求(这些都属于副作用,交给专门的 Hooks 处理)。
jsx
// 纯函数组件:输入 props(name),输出 UI,无副作用
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// 调用时,相同输入必然产生相同输出(可预测)
<Greeting name="小明" /> // 输出 <h1>Hello, 小明!</h1>
<Greeting name="小红" /> // 输出 <h1>Hello, 小红!</h1>
之前的:
jsx
// 类组件:依赖可变的 this,状态修改隐含副作用
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>; // this.props 是可变的(父组件更新时变化)
}
}
相比之下,函数组件的优势很明显:
- 可预测性:纯函数组件 "输入定,输出定",无需担心内部隐藏状态导致的 UI 错乱,调试时只需关注 props 和 state;
- 简洁性:摆脱 this 绑定、生命周期钩子(如 componentDidMount)的复杂逻辑,代码更短、可读性更高;
- 复用性:纯函数组件更容易通过组合(而非继承)复用(如自定义 Hooks)。
"不可变数据" 管理状态(避免隐性副作用)
函数式编程强调 "不可变数据"(数据创建后不修改,而是返回新副本),React 把这个思想贯穿到 状态更新(setState/useState) 和 Props 传递 中。
React 的状态(state)本质是 "不可变的":你不能直接修改状态对象 / 数组,必须返回一个新的副本,React 才会感知到状态变化并重新渲染。
jsx
function TodoList() {
// 初始化状态:todo 列表(数组)
const [todos, setTodos] = React.useState([{ id: 1, text: '学习 FP' }]);
// 添加新 todo:返回新数组(不修改原数组)
const addTodo = (text) => {
const newTodo = { id: Date.now(), text };
// 正确:用扩展运算符创建新数组(不可变)
setTodos([...todos, newTodo]);
// 错误:直接修改原数组(React 无法感知变化)
// todos.push(newTodo);
};
return (
<div>
{todos.map((todo) => (
<p key={todo.id}>{todo.text}</p>
))}
<button onClick={() => addTodo('学习 React FP')}>添加</button>
</div>
);
}
这种设计的优势:
- 避免隐性副作用:直接修改原状态会导致 "状态变化不可追踪"(比如不知道什么时候、谁修改了状态),纯函数组件的 "输入 → 输出" 逻辑被打破;
- React 渲染优化:React 通过 "浅比较" 判断状态 / Props 是否变化(比如比较数组引用是否改变),如果直接修改原数据,引用不变,React 会误以为状态没变化,不重新渲染;
- 符合 FP 思想:不可变数据让状态变化 "可预测、可回溯",比如 Redux(React 生态的状态管理库)的核心就是 "不可变状态树"。
"副作用隔离"(用 Hooks 分离纯逻辑与副作用)
函数式编程强调 "纯函数无副作用",但前端开发无法避免副作用(如网络请求、DOM 操作、定时器)。React 没有禁止副作用,而是通过 Hooks(如 useEffect、useCallback) 将副作用与纯渲染逻辑 "隔离",让组件主体保持纯函数特性(组件主题(函数执行部分)只负责 "根据状态渲染 UI"(纯逻辑),副作用(如网络请求、DOM 操作、定时器)由专门的 Hooks 处理)。
jsx
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
// 副作用:发起网络请求(隔离在 useEffect 中)
React.useEffect(() => {
// 网络请求是副作用(依赖外部环境,有不确定性)
fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // 依赖项:只有 userId 变化时,才重新执行副作用
// 组件主体:纯逻辑(根据 user 状态渲染 UI)
if (!user) return <div>加载中...</div>;
return <div>姓名:{user.name}</div>;
}
这样设计的优势:
- 组件职责单一:组件主体只关心 "渲染 UI",副作用交给专门的 Hooks 处理,符合 "单一职责原则";
- 副作用可控制:useEffect 通过 "依赖项数组" 控制副作用的执行时机(如组件挂载时、依赖变化时),避免 "副作用泛滥";
- 符合 FP 思想:纯函数负责 "计算"(渲染 UI),副作用负责 "与外部交互",两者分离,代码更易维护。
"函数组合" 实现组件复用(替代继承)
函数式编程强调 "用函数组合替代继承",React 把这个思想落地为 组件组合(Composition) 和 自定义 Hooks,替代了类组件的继承(如 extends React.Component)。
组件组合(Composition)通过 "组件嵌套" 和 "Props 传递" 实现复用,而非继承:
jsx
// 通用组件:Button(纯函数组件)
function Button({ children, onClick }) {
return (
<button style={{ color: 'red' }} onClick={onClick}>
{children}
</button>
);
}
// 业务组件:LoginButton(组合 Button 实现复用)
function LoginButton() {
const handleLogin = () => console.log('登录');
// 组合 Button,传递 Props,无需继承
return <Button onClick={handleLogin}>登录</Button>;
}
// 业务组件:LogoutButton(组合 Button 实现复用)
function LogoutButton() {
const handleLogout = () => console.log('退出');
return <Button onClick={handleLogout}>退出</Button>;
}
把可复用的逻辑(纯逻辑 + 副作用)封装成自定义 Hooks,通过 "函数调用" 复用,而非组件继承:
jsx
// 自定义 Hooks:封装"获取用户数据"的复用逻辑(纯逻辑 + 副作用)
function useUser(userId) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
return user; // 返回结果,供组件使用
}
// 组件 A:复用 useUser 逻辑
function UserProfileA({ userId }) {
const user = useUser(userId); // 函数调用,复用逻辑
if (!user) return <div>加载中...</div>;
return <div>姓名:{user.name}</div>;
}
// 组件 B:复用 useUser 逻辑
function UserCard({ userId }) {
const user = useUser(userId); // 函数调用,复用逻辑
if (!user) return <div>加载中...</div>;
return <div>卡片:{user.name}</div>;
}
这样的设计优势:
- 灵活性更高:继承会导致 "组件耦合"(子类依赖父类的实现),而组合和自定义 Hooks 是 "松耦合"(组件 / 逻辑通过参数传递,不依赖内部实现);
- 符合 FP 思想:自定义 Hooks 本质是 "函数组合",把复杂逻辑拆解成小的、可复用的函数,与函数式编程 "用纯函数组合描述计算" 的思想一致。
总结:React 吸收 FP 思想的核心价值 React 采用函数式编程思想,不是为了 "赶潮流",而是为了解决前端开发的实际问题:
- 可预测性:纯函数组件 + 不可变数据,让 UI 渲染 "输入定,输出定",减少状态混乱;
- 可维护性:副作用隔离 + 函数组合,让代码职责清晰、易于复用和调试;
- 性能优化:不可变数据 + 浅比较,让 React 渲染优化更高效;
- 简洁性:摆脱类组件的 this 和生命周期,代码更短、学习成本更低。
面向对象编程(Object-Oriented Programming) - 对象
特点:一种以 "对象" 为核心的编程范式,核心思想是将现实世界中的实体抽象为 "对象"------ 每个对象包含 "描述实体的属性(数据)" 和 "实体能执行的行为(方法)",通过对象间的交互(调用方法、传递数据)完成复杂功能。
简单说,OOP 就像 "搭积木":把复杂系统拆分成一个个独立的 "积木块(对象)",每个积木块有自己的 "样子(属性)" 和 "功能(方法)",再通过积木块的组合拼接,搭建出完整的系统。
仍然是求和的例子,用面向对象编程的方式实现:
javascript
// 类方式
class NumberProcessor {
constructor(numbers) {
this.numbers = numbers;
}
filterEven() {
this.numbers = this.numbers.filter((n) => n % 2 === 0);
return this;
}
square() {
this.numbers = this.numbers.map((n) => n * n);
return this;
}
sum() {
return this.numbers.reduce((sum, n) => sum + n, 0);
}
sumOfEvenSquares() {
return new NumberProcessor(this.numbers).filterEven().square().sum();
}
}
// 使用
const processor = new NumberProcessor([1, 2, 3, 4, 5, 6]);
console.log(processor.sumOfEvenSquares()); // 56
// 或者使用对象字面量
const numberUtils = {
isEven: (n) => n % 2 === 0,
square: (n) => n * n,
sum: (a, b) => a + b,
sumOfEvenSquares(numbers) {
return numbers.filter(this.isEven).map(this.square).reduce(this.sum, 0);
},
};
console.log(numberUtils.sumOfEvenSquares([1, 2, 3, 4, 5, 6])); // 56
当然一般计算,不会想到用面向对象编程的方式实现,但这里就反应了面向对象编程的核心思想:把一切东西都抽象为对象。
OOP的四大核心特性
- 封装(Encapsulation):将数据(属性)和行为(方法)封装在一起,形成一个独立的 "对象",对外隐藏内部实现细节,只暴露必要的接口(方法);
- 继承(Inheritance):通过 "类" 和 "对象" 的继承关系,实现代码复用和扩展;
- 多态(Polymorphism):通过 "方法重载" 和 "接口抽象",实现 "同名不同实现" 的灵活性;
- 抽象(Abstraction):通过 "接口" 和 "抽象类",实现 "高内聚、低耦合" 的模块化设计。
js
// 父类:Animal
// 将getName方法封装在父类中,对外隐藏内部实现细节,只暴露必要的接口(方法),这就是封装的体现
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
// 父类方法(统一接口)
makeSound() {
console.log('动物发出声音');
}
}
// 子类:Dog 继承了父类的getName方法,这就是继承的体现
class Dog extends Animal {
// 重写父类的makeSound方法,这就是多态的体现
makeSound() {
console.log(`${this.name}:汪汪汪!`);
}
}
// 子类:Cat(重写 makeSound) 这就是多态的体现
class Cat extends Animal {
makeSound() {
console.log(`${this.name}:喵喵喵!`);
}
}
// 统一调用逻辑(不关心具体是哪个子类)
function animalSound(animal) {
// 这就是多态的体现,不同对象调用同一方法,结果不同
animal.makeSound(); // 同一方法调用,不同行为
}
// 多态体现:不同对象调用同一方法,结果不同
const dog = new Dog('旺财');
const cat = new Cat('咪宝');
animalSound(dog); // 旺财:汪汪汪!
animalSound(cat); // 咪宝:喵喵喵!
// 抽象就是你思考到核心的属性和方法,什么适合封装成一个父类,什么适合封装成一个子类,这就是抽象的体现
OOP的适用场景和优劣势
- 适用场景:模拟现实世界的复杂实体(如 "用户、订单、商品" 等业务对象);大型复杂系统(如管理系统、游戏、框架),需要清晰的模块划分和复用
- 优势:可读性高(代码结构贴近现实世界),易于维护和拓展(内部实现细节对外隐藏,继承和多态让代码更灵活),模块化清晰(高内聚、低耦合)
- 劣势:可能过度设计(如果一个系统很小,却要使用OOP,可能会过度设计),性能开销(继承和多态会有一定的性能开销),继承可能导致紧耦合(如果一个系统很大,却要使用OOP,可能会导致代码过于复杂,难以维护)
范式对比总结
| 范式 | 关注点 | 适用场景 | 代码风格 |
|---|---|---|---|
| 命令式 | 如何做 | 性能敏感、底层操作 | 循环、条件语句 |
| 声明式 | 做什么 | 数据处理、UI 构建 | 链式调用、表达式 |
| 函数式 | 函数组合 | 数据处理、数学计算 | 纯函数、不可变数据 |
| 面向对象 | 对象和类 | 大型系统、GUI 应用 | 类、继承、封装 |
实际应用建议
实际项目中通常混合使用多种范式,不同场景适合不同范式。
来一个综合使用的例子: 假设需实现一个 "电商订单处理流程",包含以下功能:
- 定义订单实体(含商品、金额、状态等)。
- 筛选符合条件的订单(如 "已付款且金额> 100 元")。
- 计算订单总金额(含折扣:满 200 减 20)。
- 输出处理结果。
javascript
// 1. 面向对象编程(OOP):定义订单实体(封装属性和行为)
class Order {
constructor(id, products, isPaid) {
this.id = id;
this.products = products; // 商品列表({name: string, price: number})
this.isPaid = isPaid; // 是否付款
}
// 计算订单原始总金额(封装行为)
getTotalPrice() {
return this.products.reduce((sum, p) => sum + p.price, 0);
}
}
// 2. 函数式编程(FP):纯函数处理数据(不可变、无副作用)
// 纯函数:计算折扣后金额
const calculateDiscount = (total) => (total >= 200 ? total - 20 : total);
// 纯函数:筛选符合条件的订单(已付款且原始金额>100)
const filterValidOrders = (orders) =>
orders.filter((order) => order.isPaid && order.getTotalPrice() > 100);
// 3. 声明式编程:描述目标(筛选→计算折扣→汇总),隐藏步骤
const processOrders = (orders) =>
filterValidOrders(orders)
.map((order) => ({
id: order.id,
originalPrice: order.getTotalPrice(),
discountedPrice: calculateDiscount(order.getTotalPrice()),
}))
.reduce(
(summary, item) => {
summary.totalOriginal += item.originalPrice;
summary.totalDiscounted += item.discountedPrice;
summary.details.push(item);
return summary;
},
{ totalOriginal: 0, totalDiscounted: 0, details: [] },
);
// 4. 命令式编程:执行流程并输出结果(显式步骤)
function main() {
// 初始化订单(命令式步骤:创建对象)
const orders = [
new Order(1, [{ name: '书', price: 50 }], true), // 金额50(不满足>100)
new Order(2, [{ name: '手机', price: 250 }], true), // 金额250(满足)
new Order(3, [{ name: '耳机', price: 150 }], false), // 未付款(不满足)
new Order(
4,
[
{ name: '键盘', price: 120 },
{ name: '鼠标', price: 90 },
],
true,
), // 金额210(满足)
];
// 处理订单(调用声明式/函数式逻辑)
const result = processOrders(orders);
// 命令式输出(显式步骤:循环打印)
console.log('有效订单处理结果:');
for (let i = 0; i < result.details.length; i++) {
// 命令式循环
const item = result.details[i];
console.log(`订单${item.id}:原价${item.originalPrice}元,折扣后${item.discountedPrice}元`);
}
console.log(`总计:原价${result.totalOriginal}元,折扣后${result.totalDiscounted}元`);
}
// 执行主函数
main();
main 函数的核心是 "流程控制",通过明确的步骤、显式的循环和指令,一步步引导计算机完成 "从初始化到输出" 的全流程,完全符合命令式编程 "关注如何做、步骤化执行" 的本质。
虽然内部调用了 processOrders(声明式 + 函数式),但这并不影响 main 本身是命令式 ------ 相当于 "命令式的流程" 中,调用了一个 "声明式的工具" 来完成某个子任务。