源自我在知乎对 你们的 JavaScript 学习开发之路是怎样的? 的回答,补充了一些对时下热点问题的看法。
三年前,我当时作为服务器端工程师,从未系统地了解过 JavaScript,只是在少数需要改前端页面时才写过几句 JavaScript,正是因为这种不了解,写起来觉得很烦,也不喜欢 JavaScript.
但因为工作需要开始学习 Node.js,项目是用 CoffeeScript 写的,显然这又是一个我没见过的东西,但时间很紧,只看了一天 CoffeeScript 就开始写代码了,的确 CoffeeScript 的宽容度要比 JavaScript 好很多,语法很直观,也避开了 JavaScript 的一些坑。
写了大半年 Node.js 之后我喜欢上了 Node.js 和 CoffeeScript,但我依然不太喜欢 JavaScript,但毕竟代码最后是要被编译到 JavaScript 的,所以我还是认真地读完了「JavaScript 语言精粹」和「JavaScript 权威指南」。权威指南虽然很厚,但其实通篇都是平铺直叙的介绍,读起来并不是很累,小节之间关联也比较少,很适合碎片时间阅读。什么,你说权威指南太重了?不过我看的是电子版。
后来继续写了两年 Node.js,此时我 JavaScript 的编码经验依然为零 —— 我全部的代码都是 CoffeeScript。经过这么长时间,我对 JavaScript 的了解也非常深入了,所以对 JavaScript 也就没什么抗拒的情绪了 —— 所谓抗拒有时候只是自己不够了解。
再后来换了一家公司,开始维护一些 JavaScript 的项目,在我写了两年多 Node.js 之后终于开始真正地写 JavaScript 了,虽然我已经读了很多书、写了大量编译到 JavaScript 的代码,但真正写起来还是遇到了很多麻烦 —— 这些在 CoffeeScript 中并不是问题:定义类、判等(两个等号和三个等号)、非空判断(a?.b?.c
)、满屏的中括号和花括号。一些人喜欢拿 JavaScript 的设计缺陷说事,但哪个语言没点历史问题呢?如果一个缺陷是任何有基本 JavaScript 经验的人都可以绕开的,或者可以通过自动化的工具发现和修复的,那我觉得它可能不算一个问题,而仅仅是设计不完美而已。
经过原生 JavaScript 的折腾,我不像以前那么固执地用 CoffeeScript 来写所有的代码了,也开始尝试 TypeScript、Babel 之类其他的预编译语言,无数的预编译方言是 JavaScript 的闪光点之一 ,切换一下语言也会变化一下思路。
前端面试有一个被问烂了的问题就是「你如何理解 JavaScript」的闭包,这的确是一个很难回答的问题。从本质上来看,闭包赋予了一个函数基于词法作用域去读取外层作用域的变量的能力 —— 即使外层作用域本来已经要被销毁了。从结果上来看,闭包赋予了函数拥有「内部状态」的能力,在实践上,我更多地会用闭包来实现轻量级的面向对象范式:通过一个函数来创建对象,这个对象中的方法可以访问到闭包中的一些内部状态。
所以其实我很少去和原型链打交道,个人感觉用闭包来实现面向对象要比用原型链简单、直观一些,尤其在 ES2015 之前并没有一个标准的定义类的方法。对于原型链我的理解就是对象可以有个「默认的属性来源」,当读取一个对象上不存在的属性时,就会到原型上查找,如果找不到就继续到原型的原型上查找,原型赋予了对象之间共享数据的能力。
很多 JavaScript 程序员吐槽异步回调的繁琐,但如果你了解过其他语言的多线程编程,会发现 JavaScript 是非常美好的 —— 同一时间只有一个线程在执行 JavaScript 代码,而且事件循环是以函数为单位的,在函数内你完全不需要考虑线程间同步的问题(在一些语言中多线程同时读写变量都是未定义行为),而且 JavaScript 中的异步任务并不会对应到操作系统中的线程,即使有大量并发任务也不会引入线程切换的开销,这也是大家说 Node.js 适合高并发场景的原因。从概念上来说事件模型肯定是要比手动同步的多线程要先进的,事件模型不可避免地会引入大量的回调,但它将运行时的不确定的线程安全问题转换到了编写代码时的一些「小麻烦」。而且社区中已经有很多方案去解决这些小麻烦,可以看到 Promise 和 async/await 其实只是表现层面的语法糖,并没有触及到核心的事件循环机
制,所以说异步回调的繁琐可能并不是一个不可解决的问题。
我觉得我在 JavaScript 学习上所走的最大的弯路就是比较晚才开始了解和使用 Promise。相比于编写 Callback 风格的异步代码,使用 Promise 意味着一种思路上的转变,虽然 Promise 的原理简单,但在具体的使用场景上还是需要自己做很多尝试的,例如具有分支的异步逻辑、循环地处理数据、逐级传递异常等。在使用 Promise 的过程中,也让我对「异常」有了更加深入的认识,异常是现代语言所提供的非常强大的流程控制机制,让本来唯一一条通常的、正确的执行路径变得可以从任何一处中断,并进入一个所谓的异常处理流程。
在基于命名空间的语言中,同一依赖的多版本并存问题一直是一个大坑,因为同一个库的多个版本拥有着相同的命名空间,不可避免地会出现冲突。而 JavaScript 是没有命名空间的,取而代之的是基于文件系统的模块机制,这给构建出复杂的依赖关系提供了可能,也让 JavaScript 的社区变得更加活跃,在 JavaScript 中我几乎从未操心过依赖的版本,只要安装自己需要的版本即可。
在熟悉了 JavaScript 之后,感觉其实前端并没有那么神秘了,大家都是 JavaScript,只不过调的 API 不同嘛,在架构上也无非是若干年前桌面编程已经踩过的那些坑。在此推荐一本「JavaScript Web Applications」,让我很快地对前端框架的实现有了一个概览性的了解。
最近几年前端的工具和框架更新极快,我觉得一方面是因为 JavaScript 语言本身在进化,所以构建工具方面变化很快了;另一方面的确前端,也就是 GUI 应用需要管理的状态远比服务器端要复杂 —— 来自服务器的状态和 GUI 上元素的状态需要相互地同步,而状态的变化也同时来自于服务器端和用户的操作。其实这是若干年前桌面应用已经走过的路,但因为之前浏览器端能力的限制,直到最近几年才涌现出这些新的尝试。
而我的选择是 React,作为服务器端工程师里前端水平尚可的人,我后来也用 React 写了一些内部站点(管理员后台之类的)。React 的渲染过程和后端的数据 API 很像,将数据和状态统一地存储于一处,每当数据变化时就进行一次完整的重新渲染,渲染过程是一个无状态也无副作用的纯函数,不需要在两次渲染之间维护状态,有一种函数风格的美感。