因为你写一个允许传入动态类型的函数给别人用,就如同给一张签了名的空白支票别人一样危险,别人爱写什么数字上去都行。

举个例子,Facebook 有一种数据类型叫做 FBID,是一个 64 位的整数,用作任何对象的唯一 ID。在 Facebook 把无类型的 PHP 转化为有类型的 Hack 之前,很多人不知道 FBID 到底是整数还是字符串,于是有些函数用整数接收 FBID,另外一些函数用字符串。这时候函数之间传来传去的 FBID 到底是什么类型已经说不清楚。如果你写一个函数并接收一个 FBID 的数组,你永远不可能知道数字里的 FBID 是哪种类型,甚至可能是混合类型的。

尽管在语义上这绝对是有问题的,但神奇的 PHP 在做整数和字符串 == 比较时会自动把字符串隐式转换为整数,所以原本不应该相等的两种类型 FBID 可能会被判定为相等。不过如果用 === 的话,不同类型自然不相等。因为很多人不注意细节,使用 == 而非 ===,所以如果假设你接收了一个 FBID 的数组,然后调用别人的函数来去重,你猜 “123” 和 123 会不会被当作重复而去掉?你不可能猜到,因为你不知道别人用什么做对比。

这使得当时 Facebook 的 PHP 代码存在大量难以发现的 bug,而且没人敢去修。你说你这里 FBID 一定是整数,然后收到字符串就抛异常?你猜你这一改 Facebook 哪些功能会挂掉?你不可能知道……所以你不敢改。

最终 Facebook 把 PHP 逐步转化为 Hack,把 FBID 存储为整数,大家就可以放心地把 FBID 传来传去不用担心出问题了。如果你尝试拿 FBID 跟另外一个字符串或整数比较,你的代码在静态分析阶段就会报错。你一定要比较,就必须进行显式类型转换后再做比较。

同理,Facebook 把 JavaScript 改造为 Flow,但 FBID 在 JavaScript 里面必须存储为字符串,因为 JavaScript 存储不了 64 位整数,可能会转换为浮点数存储,然后丢失精度。在此之前,有人不知道 FBID 在 JavaScript 不能存为整数,然后引发各种难以复现的 bug。

如果你不懂 Hack 或 Flow,可以用现在主流的 TypeScript 做参考。我认为这是现在最好的折衷方案。语言本身是支持动态类型的,但通过类型注释来让类型稳定的变量和参数变为有类型,你不加类型注释的时候还能灵活使用动态类型。

为什么你需要保留使用动态类型的权力呢?因为你在开发新功能时,可能你只是在探索,具体数据结构还没有定下来,不需要声明完整的数据结构类型信息能为你节省不少改来改去的时间。与之对比的是那些 C++ 和 Java 操作 JSON 的代码,必须处处声明现在预期读取出来的变量是什么类型,你稍微改一下 JSON 的结构就要改对应的类型声明,这真的很麻烦。