在被运行之前,代码只是文字
当你写代码时,其实只是在往一个文件里敲文字——字母、数字和标点,和购物清单或一封邮件没什么两样。这个文件本身什么也不会做。无论里面的指令多么巧妙,它都静静地躺在你的磁盘上,一动不动。
“运行代码”就是那段文字变成行动的时刻。某个东西——一个内置在你电脑里的程序——会读取你的指令,并一步一步地执行。可以把你的代码想成写在纸上的菜谱:很有用,但在厨师拿起它、照着做之前,它做不出任何一道菜。
两条路:先全部翻译好,还是一行一行翻译
文字变成行动主要有两种方式,区别主要在于时机。这就是编译型与解释型语言之分,它决定了用起来是什么感觉。
编译型语言(比如 C 或 Rust)会一次性地把你的整个文件提前翻译成机器语言,产出一个成品程序,之后可以直接运行。这就像在任何人阅读之前先把整本书翻译完:准备起来慢一些,但一旦完成,运行就又快又独立。
解释型语言(比如 Python 或 JavaScript)则一行一行地读取并运行你的文件,每条指令在需要的那一刻才翻译。这就像有位口译员站在你身旁,你说一句他译一句:你可以改一行就立刻再试一次,不用先单独“把一切都构建好”。
真正运行它的引擎:运行时
无论你的语言走哪条路,你的代码都需要一个引擎来运行其中。这个引擎叫做运行时(runtime)——在你的程序运行期间,一直在场、一直在干活的那个东西。它提供各种实际的机器:读文件、在屏幕上绘制、与网络通信。
你其实已经在用运行时,只是没去叫它的名字。网页里的 JavaScript 运行在浏览器的运行时里;服务器上同样的 JavaScript 运行在 Node 里。Java 代码运行在一个叫 JVM 的东西里。同样的菜谱,不同的厨房——而厨房就是运行时。
这就是为什么你会经常听到“在运行时”(at runtime)这种说法。它的意思就是“在程序真正运行的时候”,与你写代码的时候相对。有些问题只会在运行时才暴露出来——当真实的输入到达、引擎真的去照着你的指令做的时候。
运行中的程序就是一个进程
一旦你的代码真正运行起来,操作系统就会把它包进一个进程(process)里——一个程序的一次活的实例,拥有自己私有的一块内存来存放数据。把同一个应用打开两次,你就得到两个进程,各管各的。
你的电脑会同时运行许多进程:一个浏览器、一个音乐应用、一个聊天窗口,外加几十个你从没见过的后台进程。操作系统就是那个杂耍者,让每个进程轮流用上处理器,并把它们的内存隔开,这样一个程序就不会踩坏另一个程序的数据。
为什么有些事得等,以及缓存如何加速
并不是每条指令都能瞬间完成。有些——比如抓取一个网页、读取一个大文件——需要时间,因为你的程序得等待它自身之外的某个东西。与其僵住、对着墙发呆,写得好的程序会用异步(async)代码:先把慢任务发出去,转头去做别的有用的活,等答案到了再回来处理。
想象点咖啡:你不会僵在柜台前——你拿个号、坐下,等做好了咖啡师会叫你。异步让程序保持响应,而不是每次要等慢东西时就卡死。
另一种避开等待的办法,是干脆不重复做那些慢活。缓存(cache)会把某个昂贵操作的结果就近存起来,下次再需要时,你直接拿那份存好的副本,而不必重新计算或重新获取。这就像冰箱里的剩菜:做一次,热很多次。
借来的代码:依赖与包管理器
几乎没有哪个真实程序是完全从零写出来的。你会借用别人已经写好并分享的代码——比如处理日期、加密,或者绘制图表。你的项目所依赖的每一块借来的代码,都是一个依赖(dependency):你的代码靠它存在才能正常工作。
你不用一个个手动去找这些代码。包管理器(package manager)会替你完成:你说出想要什么,它就把每个依赖——以及那些依赖自己又依赖的东西——下载到你的项目里。对 JavaScript 来说,这个工具通常是 npm;对 Python 来说,则是 pip。
npm install dayjs
在运行时,你的程序会像调用你自己写的代码一样,去取用这些已安装的依赖。所以“运行你的代码”通常意味着:运行你那寥寥几行,外加一大群安静的借来代码,全都在同一个进程里协同工作。
小结
你从文件里的一段文字出发,一路跟到了它变成行动。代码在被运行之前什么都不做;编译型语言会提前翻译它,而解释型语言一行一行地读它;运行时是真正干活的引擎;运行中的程序作为一个进程存在,拥有自己的内存;异步和缓存让程序不必无谓地等待;包管理器则把你代码所依赖的、借来的依赖拉进来。