今天咱们来聊聊一个听起来挺高大上的话题——编程范式。这词儿听起来可能有点唬人,但其实它就是描述编程时组织代码的不同风格和方法。
我会尽量用简单的话给大家解释清楚,每种范式到底是怎么一回事。这样,当别人说起“面向对象”、“函数式”或者“声明式”这些词儿时,你就能心领神会了。
这篇文章主要是个简单的理论介绍,当然啦,咱们也会看一些伪代码和实际的代码示例。
咱们开始吧!
什么是编程范式?
所谓的编程范式,其实就是我们组织程序或者编程语言的不同方式和风格。每种范式都有自己的一套结构、特性,以及解决常见编程问题的方法和观点。
为啥会有这么多不同的编程范式呢?这问题其实和为啥有这么多编程语言差不多。不同的范式适合解决不同类型的问题,所以针对不同项目使用不同的范式是有意义的。
随着时间的推移,软件和硬件的进步也推动了不同方法的发展。再加上我们人类的创造力,我们总喜欢创造新东西,改进前人的成果,把工具调整成我们喜欢的样子,或者看起来更高效的方式。
所以,今天我们在编写和组织程序时,有很多选择。
编程范式不是什么
要明确一点,编程范式不是编程语言,也不是工具。你不能用范式来“构建”任何东西。它们更像是一套理念和指导原则,是很多人达成共识、遵循并不断发展的东西。
编程语言并不总是和某个特定的范式绑定在一起。有些语言在设计时就考虑了特定的范式,并且提供了更多促进该范式编程的特性(比如Haskell和函数式编程)。
但也有很多“多范式”的语言,意味着你可以根据自己的需要,让代码适应不同的范式(比如JavaScript和Python)。
同时,不同的编程范式并不是互斥的,你完全可以在同一个项目中同时使用来自不同范式的实践。
我为啥要关心这个?
简单来说,就是为了增加你的知识面。
详细点说,我觉得了解编程的多种方法很有趣。探索这些话题可以帮助你开阔思维,跳出你已经熟悉的工具和框架。
而且,这些术语在编程界经常被提及,所以有一个基本的了解,将有助于你更好地理解其他相关的主题。
好了,既然我们已经介绍了编程范式是什么和不是什么,接下来就让我们一起来看看最流行的几种范式,了解它们的主要特点,并进行比较。
要记住,这个列表并不全面。还有一些其他的编程范式没有在这里涵盖到,但我会介绍最流行和最广泛使用的几种。
命令式编程
命令式编程由一系列详细的指令组成,这些指令按给定的顺序提供给计算机执行。它之所以被称为“命令式”,是因为作为程序员,我们会非常具体地告诉计算机必须做什么。
命令式编程关注的是描述程序是如何一步步运作的。
假如你想烤一个蛋糕,你的命令式程序可能看起来像这样(我可不是个厉害的厨师,所以别太苛刻哦 😒):
1. 在一个碗里倒入面粉
2. 往同一个碗里打入几个鸡蛋
3. 往同一个碗里倒些牛奶
4. 混合这些食材
5. 将混合物倒入模具
6. 烤上35分钟
7. 让它冷却下来
用实际的代码示例来说,假设我们要过滤一个数字数组,只保留大于5的元素。我们的命令式代码可能长这样:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
const result = [];
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
console.log(result); // 输出:[6, 7, 8, 9]
我们告诉程序遍历数组中的每个元素,将元素的值与5进行比较,如果元素大于5,就把它加入到新数组中。
我们的指令非常详细具体,这就是命令式编程的核心。
过程式编程
过程式编程是命令式编程的一个延伸,它增加了函数(也称为“过程”或“子程序”)的特性。
在过程式编程中,鼓励用户将程序执行细分为函数,作为提高模块化和组织性的一种方式。
继续我们的蛋糕例子,过程式编程可能是这样的:
function pourIngredients() {
// 在一个碗里倒入面粉
// 往同一个碗里打入几个鸡蛋
// 往同一个碗里倒些牛奶
}
function mixAndTransferToMold() {
// 混合食材
// 将混合物倒入模具
}
function cookAndLetChill() {
// 烤上35分钟
// 让它冷却下来
}
pourIngredients();
mixAndTransferToMold();
cookAndLetChill();
你可以看到,通过实现函数,我们可以直接看文件末尾的三个函数调用,对我们的程序做什么就有了一个清晰的了解。
这种简化和抽象是过程式编程的好处之一。但在函数内部,我们仍然使用的是命令式代码。
函数式编程
函数式编程将函数的概念提升到了一个新的层次。
在函数式编程中,函数被视为一级公民,这意味着它们可以被赋值给变量,作为参数传递,也可以作为其他函数的返回值。
另一个关键概念是纯函数。一个纯函数只依赖于它的输入来生成结果。给定相同的输入,它总是产生相同的结果。此外,它不会产生任何副作用(即不会对函数外部的环境产生任何改变)。
有了这些概念,函数式编程鼓励我们用函数来编写大部分程序(惊讶吧😲)。它还主张代码的模块化和无副作用,这使得在代码库中更容易识别和分离责任,从而提高了代码的可维护性。
回到数组过滤的例子,我们可以看到,在命令式范式中,我们可能会使用一个外部变量来存储函数的结果,这可以被视为一个副作用。
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
const result = []; // 外部变量
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
console.log(result); // 输出:[6, 7, 8, 9]
要将其转换为函数式编程,我们可以这样做:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
function filterNums() {
const result = []; // 内部变量
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
return result;
}
console.log(filterNums()); // 输出:[6, 7, 8, 9]
代码几乎一样,但我们把迭代包装在了一个函数里,并且在函数内部也存储了结果数组。这样,我们可以确保函数不会修改它作用域之外的任何东西。它只创建了一个变量来处理它自己的信息,一旦执行完成,那个变量也就不存在了。
声明式编程
声明式编程的核心是隐藏复杂性,让编程语言更接近人类的语言和思维方式。它与命令式编程正好相反,因为程序员不需要给出关于计算机应该如何执行任务的指令,而是关于需要什么结果。
举个例子,用数组过滤的故事来说,声明式的方法可能是这样的:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
console.log(nums.filter(num => num > 5)); // 输出:[6, 7, 8, 9]
看到没,用filter函数时,我们并没有明确告诉计算机要遍历数组或者把值存储到另一个数组里。我们只是说出了我们想要什么("filter")以及满足的条件("num > 5")。
这样的好处是,代码更容易阅读和理解,通常也更简短。JavaScript中的filter、map、reduce和sort函数就是声明式代码的很好例子。
另一个好例子是现代的JS框架/库,比如React。看看这段代码:
<button onClick={() => console.log('你点击了我!')}>点击我</button>
这里我们有一个按钮元素,带有一个事件监听器,当按钮被点击时,会触发console.log函数。
React使用的JSX语法将HTML和JS混合在一起,这让编写应用程序变得更加简单快捷。但实际上,浏览器并不会直接读取和执行这样的代码。React代码最终会被转译成常规的HTML和JS,这才是浏览器真正运行的东西。
JSX是声明式的,因为它的目的是为开发者提供一个更友好、更高效的工作接口。
关于声明式编程,一个重要的事情是,计算机在背后实际上是将这些信息作为命令式代码来处理的。
以数组为例,计算机仍然会像在for循环中那样遍历数组,但作为程序员,我们不需要直接编写这些代码。声明式编程所做的,就是将那些复杂性从程序员的直接视野中隐藏起来。
面向对象编程
面向对象编程(OOP)是最流行的编程范式之一。
OOP的核心概念是将关注点分离到被编码为对象的实体中。每个实体都会组合一组特定的信息(属性)和可以由实体执行的操作(方法)。
OOP大量使用类(这是从程序员设置的蓝图或样板开始创建新对象的一种方式)。从类创建的对象称为实例。
继续我们的伪代码烹饪示例,假设在我们的面包店中,我们有一个主厨(叫Frank)和一个助理厨师(叫Anthony),他们每个人在烘焙过程中都有特定的责任。如果我们使用OOP,我们的程序可能看起来像这样:
// 创建对应每个实体的两个类
class Cook {
constructor(name) {
this.name = name;
}
mixAndBake() {
// 混合食材
// 将混合物倒入模具
// 烤35分钟
}
}
class AssistantCook {
constructor(name) {
this.name = name;
}
pourIngredients() {
// 在一个碗里倒入面粉
// 在同一个碗里打入几个鸡蛋
// 在同一个碗里倒些牛奶
}
chillTheCake() {
// 让其冷却下来
}
}
// 从每个类实例化一个对象
const Frank = new Cook('Frank');
const Anthony = new AssistantCook('Anthony');
// 调用每个实例对应的方法
Anthony.pourIngredients();
Frank.mixAndBake();
Anthony.chillTheCake();
OOP的好处是,它通过明确的责任和关注点分离,促进了对程序的理解。
总结
正如我们所看到的,编程范式是我们面对编程问题的不同方式,以及组织我们代码的方式。
命令式、过程式、函数式、声明式和面向对象范式是今天最受欢迎和广泛使用的范式之一。了解它们的基础知识对于一般知识和更好地理解编码世界的其他主题都有好处。