1. Why

我曾写过类似这样的Javascript代码:

dealService.fetch = function(ids, source, options = {}) {
  ......
}

编辑器马上显示了错误Parsing error: Unexpected token =, 然后我才反应过来, Javascript并不支持函数默认参数注1, 那段时间在开发一个服务的ruby和node.js的扩展包, 两份代码的实现基本是一致的, 我经常需要在2个语言环境中切换, 大脑对函数基本用法记忆的提取串掉了. 当时匆忙改完代码, 并未多想.

不过这种类似的事情在我和身边同事开发时不只出现过一次, 我所在的团队, 整个系统是一个较大的异构语言系统, 公司技术部门后端主要的语言平台有Ruby, Node.js, Java, Go 和少量的lua. 很多科技公司都有编程语言和技术栈的白名单, 语言平台过多对团队专业度和系统的稳定性都有很大的影响, 对于大多数优秀的程序员来说, 对新的语言和技术的充满好奇心, 不过对于一个团队而言, 语言平台泛滥带来的成本可能完全抵消掉引入新语言的好处. 我所在的团队中, 刚好以上5种语言平台都有涉及到, 如果想给一个公共服务发布客户端扩展包, 需要好几门语言都写一份. 对于一个团队来说, 语言白名单, 架构的统一, 是一个很必要的决定, 不过这是另一个话题了.

经常看到这样的说法, 一个有丰富编程经验的程序员, 要掌握一门新语言, 只需要XX天, 甚至X天, 这些话太过夸张, 而且「掌握」一词的程度, 也是深浅不一的. 不过这种说法, 也在一方面说明了语言的学习, 是有递进的捷径的, 语言之间的类比就是其中一种, 而且这种思维是程序员在语言使用过程中很自然形成的.

比如在ruby元编程中, 有一招叫做使用lambda来实现扁平化作用域, 虽然实现不一样, 但如果之前学习过javascript中的闭包, 就很容易理解这种技术.

又比如, 在Javascript中, 原型链是一个相对比较难的概念, 最简单的原型继承实现是这样:

Config.prototype = new BasicConfig()

除了上面的方式, Javascript还允许对特定对象指定它的直接原型:

const basicConfig = { level: 5 }

const config = {
  __proto__: basicConfig // 直接指定config的原型是basicConfig
}
console.log(config.level) // 5

而当我学习Lua的元表继承时, 我又找到了似曾相识的感觉:

BasicConfig = { level = 5 }
config = {}
setmetatable(config, {__index=BasicConfig}) -- 指定config的元表
print(config.level) -- 5

虽然实现不尽相同, 不过熟悉了Javascript的原型继承, 对理解Lua利用metatable实现原型继承的确是有帮助.

在语言的最佳实践上, 各种语言也有很多相通的地方, 比如很多语言都有各种形式的隐式类型转换, 而隐式类型转换往往是引起一些不易察觉bug的根源, 因此很多语言最佳实践都提倡避免使用隐式的类型转换, 如javascript的==/!=, 更年轻的Go语言直接禁止隐式类型转换; 大多语言提倡减少全局变量的使用, 原因和语言平台无关, 同样更年轻的Go语言没有提供顶层的全局变量.

但是类比思维有的时候也会办坏事, 比如我上面写的那个javascript函数, 不同语言的差异和个性始终是存在的, 来看一个隐式转换的例子:

// javascript
var x = '3';
var y = 3;
x + y // 结果是'33'
-- lua
local x = '3'
local y = 3
x + y -- 结果是6

如果你习惯在Javascript使用以上隐式转换, 切换到lua后如果不小心, 可能就会吃苦头. 隐式转换其实没有一个通用的标准, 完全靠语言的喜好决定, 要么你死记硬背, 要么更好的, 就是不要依赖隐式转换. 强类型语言直接避免了这个问题, 比如Go和Ruby注2:

# ruby
x = '3'
y = 3
x + y # TypeError: no implicit conversion of Fixnum into String

不过令人欣喜的是, 语言在某些方面的发展出现了同质化的趋势, 在代码的表现力, 对易出错特性的限制, 执行效率提升等方面的改进上, 大家趋向一致. Javascript ES6 提供了函数默认参数, 还有非常方便字符串模板功能, 另外ES6还加入了Proxy, Reflect等元编程的特性, 语言的发展存在相互借鉴的现象.


学习新语言的乐趣在于总能发现新的规则, 新的模式. 从最初学习C系列语言, 变量必须有固定类型, 到学习类型可变的动态语言; 从基于Class的面向对象体系, 到javascript, Lua等基于原型的继承体系, 再到提倡Duck Type 的Go语言Interface; 还有Ruby的2.days.ago, 猴子补丁, 鸭子类型, Lua 万花筒般的table类型等等, 这些无不给我发现新大陆的感觉.

同时, 当我学习了更多的语言, 我也遇到了更多的困惑, 为什么同样都是值传递的Ruby和Go, 对数据影响的表现是不一致的? 为什么javascript通用风格提倡保留语句分号, 而很多其他语言提倡省略分号? 同样是项目依赖管理, 为什么Ruby Bundler 提供了依赖版本锁定文件Gemfile.lock, 而Node.js NPM却没有呢?注3

我的初衷, 是想总结一下在我们团队的常用的语言中, 哪些是容易混淆的, 哪些是相通可以互相借鉴的, 同时希望能找到上面提到的一些疑惑的原因. 最开始这些语言的类比是记录在团队的wiki里, 但是后面加入的内容比较多, 一页wiki看起来比较混乱, 我就把它迁移到了gitbook上.

通过比较, 我们能看到依赖隐式转换, 在各语言中可能造成的麻烦; 通过比较, 我们可以理解到一些司空见惯的概念的对立面真正的含义; 通过比较, 我们能发现, 即使都是「值传递」的语言, 实际情况的表现还是差异很大, 比较所产生的疑惑可以驱使我们去挖掘出额外的影响因素; 通过类比去了解语言相同的模式, 也能帮助我们未来更快的学习其他语言.

我另外的一个目的, 是希望通过类比, 总结一些通用的最佳实践, 以及可以跨语言借鉴的解决方案. 比如Error Handling in Node.js一文虽然主要讲的是Nodejs中的错误处理, 但是我觉得文章的精华在于教大家对错误进行分类, 对Operational Errors 和 Programmer Errors进行不同的处理. 而这部分最佳实践是和语言无关, 完全适用于在其他语言平台的错误处理.

书中的涉及的几门语言, 既有脚本型的动态语言也有需要编译的静态语言, 并非所有的特性都具有可比性, 不过正如之前所说, 学习中的类比是我们的天性, 归类可以让我们能更快的识别它们之间的相似之处; 同时我们也要始终认识到语言之间的区别, 编程语言之间的差别不会仅仅是语法上的, 语言的理念, 生态圈甚至社区, 都是决定它的使用场景的重要因素. 语言的设计充满了权衡, 不同的语言用不同的方式处理这些权衡, 一个好的程序员知道用不同的工具解决不同的问题.

这是一本小册子, 是一本学习笔记, 我的初衷以及我对这几门语言的理解, 都无法让这本册子变成一本大而全的参考手册, 这本册子中主要是关于语言相互涉及的领域中的不同的表现. 对于那些语言独有的特性, 不会有太多的涉及, 比如Ruby的元编程, Go语言的goroutine等, 这些特性往往是各语言能和其他语言区别开来的原因, 是值得深入学习的, 但是不会是这本书的重点.

在学习过程中我参考很多优秀文章, 某些章节下面会列出一些参考资料, 在这里对这些资料的作者表示感谢!


这本电子书还会有不定时的更新, 书的最后面有一个TODO list, 这是目前已知的补充计划.

这本书源码托管在github上, 如果你对这本书有任何建议, 可以给我提issue.

如果你喜欢, 请在github上对本书star点赞.


注1. 当时使用的node.js版本是node/0.10.X,还没有实现ES6
注2. Ruby的确是强类型语言
注3. NPM提供了npm-shrinkwrap.json, 但是默认并不启用, 社区接收度也不高.

results matching ""

    No results matching ""