在所有的设计实践中,全局变量是使用起来最简单的,被坑到时也是最沉痛的。 但有没有发现很难解释为啥不用全局变量? 大概是全局变量对设计的副作用并非立竿见影,而是比较远,比较曲折。

Harttle 在一个年味很浓的晚上,试着推演全局变量问题,本文用来在日后说服别人。

隐藏的耦合

为了能够模块化地开发,你可能已经在构建工具和文件结构花了大功夫。 如果有一天突然发现,这一大坨代码根本不是模块化的!那全局变量一定是罪魁祸首。

模块间之间通过 include、import、require 等语法构造相互依赖, 这样的依赖是写清楚的,甚至可以用工具来分析,显式的依赖有助于复用既有逻辑。 因为一个模块和它的所有依赖构成了一个封装,这个封装在外部看来是一个独立的可复用单元。

但全局变量会破坏这个故事,让看起来独立的可复用单元变得不可复用, 比如在一个新模块开发中调用一个既有模块时,它莫名其妙地不工作了, 居然是因为它依赖的全局变量尚未建立! 没事,那我们就建立这些全局变量。但是在新的模块中,很难建立与旧环境一模一样的全局变量。 只能 Mock 一个环境让模块跑起来,这形成了一个隐患。

不可测试

为了能够 Show Off 你的代码质量,我们引入单元测试并贴出测试覆盖率。 但编写测试时才发现,这代码在 test.js 中根本跑不起来!环境不同啊。

cannot call history of undefined
undefined is not a function
...

怎么办?Mock 啊,从编写一个测试桩开始。 构建这个单元的所有依赖,其实这是编写单元测试前必须经过的步骤。

但如果测试一个功能,需要构建很多个复杂的全局变量(环境依赖), 并且对每项测试都需要合适地更改它们的值。 那么即使你有耐心让测试通过,你的代码也因为依赖环境太复杂而没有可靠性,当然也很难复用。

这是 全局变量让模块变得不可测试,本质还是耦合

命名空间污染

在 C 程序设计和 JavaScript 程序设计中,命名空间污染是很常见的提法。 如果不加限制全局变量的使用,它们很快就会占用你能想到的所有命名。

但它带来的问题不仅仅是 "不知道再起什么名字了"。 无意的拼写错误、无意的名字覆盖会各种 Lint 和编译工具失效,把错误带到运行时增加调试时间。

尝试使用对象、属性、闭包等概念来封装这些全局变量,并通过关注点分离的设计让每段代码的上下文都很简单。 毕竟人脑很弱,同时只能理清楚屈指可数的几个变量之间的关系。

时序和并发问题

C++手稿:静态和全局变量的作用域 中探讨过 C++ 全局变量的初始化时序问题, 大意是全局变量分散在众多的 .o 文件中,它们链接到一起时谁也不知道这些全局变量的初始换顺序。 全局变量间引用的结果就会变得不可预测

初始化时序问题不是 C++ 特有的。只要有多个全局变量,它们之间的顺序就会很难保证。 引入单例模式可以缓解这个问题,但值得注意的是单例也是全局状态,只是不再以全局变量的形式出现。

在 Web 开发中,脚本之间共享一个全局作用域。 全局变量分散在各个脚本中,尤其遇到 SPA 的时候脚本异步加载, 相信全局变量时序的 Bug 会让你忙一个通宵。

全局变量的并发问题主要在多线程共享时要注意加锁, 最好引入 RAII 模式来管理资源。 这个问题在 JavaScript 中就不那么明显了,毕竟 JavaScript 只有一个线程,搞清楚异步就没问题。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2018/02/09/avoid-global-variables.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。