转入 TypeScript 做开发有大半年了,想来分享一些自己的心得体会。
第一次学习并使用 JavaScript 大概是在07年,当时为了做一个 Web 自动化的项目,踏上了我的 JavaScript 之旅,最终以《Web 应用自动化生成工具的设计与实现》(大概是这么个名字)这篇论文来结束了我的研究生生涯。
那时候还没有全栈开发这一说,不过想想自己似乎10年前就开始了这种工作状态。我过往的技术经历比较复杂,在我们那个年代最主要的软件还是以 Windows C/S 架构为主,搞过 Socket 通信、写过 ARP 病毒,研究过 MFC 的 Thunk 技术;后来慢慢又接触了 C#、Java,还深深记得刚从 DAO 转入到 Hibernate 时的那种懵逼。
那时候觉得学什么都特别的开心,计算机专业学的还是“科学”与“技术”,也没有“互联网行业”这一说,我们这一行都还叫做“软件行业”,培训班都还在教你如何做网页。《晓松奇谈》里面有一集讲歌坛那个“白衣飘飘的年代”,我觉得2000年-2010年这10年就似乎是我的那个“白衣飘飘的年代”。那时候技术还比较神秘,大家都以研习 Windows Undocumented API 为荣,我记得我还打印了一本 Intel x86 汇编的小册子随身携带;那时候以为掌握了 Hook 就掌控了操作系统,以为会用 Ethereal 就监听了全世界。
我记得那是一个午后,我的导师花了两个小时给我讲解 JavaScript 的基础语法。这应该算是我从业多年来,最快速最密集学习的一门语言了。我还依稀记得那“22条军规”:
- 变量类型有 numer、string、bool......
- 定义变量用 var
- if & for 跟 C 语言 一样
- function 跟 C 语言 一样
- class 的语法是...
- 访问成员变量必须带上 this 指针
- 闭包是......
- ......
就这么从头到尾讲完了这10来条语法,然后导师对我说:“恩,差不多就这些了,你先开工吧,明天我来检查......”。WTF!!!就像驾校司机刚告诉了你如何启动汽车、如何操作刹车、油门,就对你说:“差不多了,明天你开车去趟......”。好吧,一个“老司机”就这么上路了,反正写啥都是在写 BUG。
作为那个年代的三大神兽语言 C++、C#、Java,我基本上都同时在写。不过他们都是 Class First 语言,以面向对象为核心,类是一等公民,定义一个类也是各种繁文缛节(相对于动态语言来说)。所以我在第一次见到 JavaScript 这种动态语言时,我的内心是震撼的,犹如从三维世界进入到四维世界的那种“宏大”,那种难以描述的“自由”。
为了防止有人喷我,特地做一下说明,不然一堆屁话就来了:
1、回头来看,我如此的震撼并不是因为 JavaScript 语言本身的精妙,而是因为动态语言相对于静态语言的自由程度;
2、我之前的技术经历主要是在 C++,并未接触过动态语言;
3、JavaScript 语言也有很多糟粕,不算“优秀”的语言,但并不影响我对它的喜爱;
4、Ruby 也是我喜爱的语言之一。
一个 JavaScript 的类(准确的讲是指模拟 C++ 里同一概念的类),它身上的属性、方法可以被动态的添加、删除、修改;一个函数可以作为变量到处传递,还有 lambda 表达式,可以直接写在函数的参数里面作为 Callback,这在当时的 C++/Java 里面简直不可想象。
在 C++ 里面,底层的很多网络库都是 C 语言实现的,所以一个 C++ 的成员函数是不能直接直接做为 Callback 函数传给底层 lib 的,而是要传递一个类的 static 方法,然后再想办法从这个 static 方法 跳回到成员方法。MFC 的 Thunk 技术就是在解决这一问题。直至今天在 Java 里面要实现一个 Callback 还需要写一大堆 Interface、Listener 什么的,真的是非常繁琐(Java 8 已经支持 lambda 表达式了,通过引入编译工具可以在 Android 开发里面使用,强烈推荐)。
而最最重要的一点就是 JavaScript 的世界观其实就是一张 Hash 表,至少我的理解是这样的。这就是一个 key-value 的世界,所有类、对象都是 Hash 表,数组也是。key 就是对象属性,value 可以是普通变量,也可以是函数,也可以是一个对象,等等。只是恰好如果 value 是一个变量,我们习惯称它为成员属性,如果 value 是一个函数,我们习惯称它为成员函数。而 Prototype 原型链也只不过是 Hash 表的一种存储结构,原型链查找也就是一种 Hash 查找算法而已。
10年前,用了一个下午爱上了 JavaScript,而10年后又遇上了 TypeScript。这10年中大部分时候还是在用 C++ 和 Java 来工作,所以 JavaScript 的一些痛苦,感受并没有像 “职业”开发者那样深,不过也还是遭遇到了不少的坑。变量不用定义就能用,偶尔代码写错一个字就成了一个新变量,为此却要付出大量的 debug 时间;早期调试也相对比较痛苦,这几年工具越来越完善了,不过做为一个从 Visual Studio 入门的开发者,看别的平台的调试方法总觉得不够傻瓜。
遇上 TypeScript 虽然没有10年前遇上 JavaScript 那样轰轰烈烈,但是也算喜爱有加。遇上 Javascript(遇上动态语言)我觉得解决的是精神生活的问题,而遇上 TypeScript 算是解决了物质生活的问题。以前开发 JavaScript 时的部分脏活累活都可以被消灭了。
这里才刚刚开始
先简单科普一下 TypeScript:
TypeScript 是由微软开发的一种基于 JavaScript 语法的语言,且已经支持了ES6、ES7语法。你可以理解它为 JavaScript 的超集,也可以理解为 JavaScript 的增强版。TypeScript 代码不能直接运行,需要通过编译器编译成 JavaScript 文件才能使用,所以依然可以在浏览器环境或者 node 环境下无缝使用。
好了,跑题了这么久,我终于要说为什么要从 JavaScript 转到 TypeScript 了,它到底解决了什么问题。
编译期类型检查
最最重要的能力就是:
- 编译期类型检查
- 编译期类型检查
- 编译期类型检查
重要的事情要说三遍。有了编译期类型检查再也不用担心变量写错名字、错误的类型赋值、写掉对象属性这些基本问题了。虽然无时无刻都需要书写类型信息,但是我相信这是值得的。部分 JavaScript 开发者可能会觉得有点繁琐,其实相信从静态语言转到 JavaScript 的开发者一定会感觉到非常亲切,所以主要还是一个先入为主的习惯问题。相对于这些付出,收益是巨大的。尤其在大型项目里面,后面接手的同事再也不用去“猜测”一个 Callback 返回的变量到底是什么类型了。
以下是 JavaScript 代码:
var name; //定义变量
function getName(type) { //定义函数
return ...
}
class Person { //ES7下的类定义
name = '';
age = 18;
getName() {
...
}
getAge() {
}
}
以下是 TypeScript 代码:
var name: string; //定义变量
function getName(type: string):string { //定义函数
return ...
}
class Person { //类定义
private name: string = '';
private age: number = 18;
public getName(): string {
...
}
public getAge(): number {
}
}
大家可以发现,最基本的语法其实就是在所有的变量或者函数的定义时,在变量名字的后面加多了一个类型信息。相较于传统静态语言的语法,基本上就是把类型信息从前面移到了后面而已。
我们再来看一下调用代码:
let name: string = obj.getName() //编译通过
let name: number = obj.getName() //编译失败
let name = obj.getName() //编译通过
let nameOther: number = name //编译失败
- 第一行编译通过,因为类型一致
- 第二行编译失败,因为类型不一致
- 第三行编译通过,因为 name 没有指定类型,TypeScript 编译器会自动推导,认为 name 变量也是 string 类型
- 第四行编译失败,因为 name 被推导为 string 类型,而 nameOther 是 number类型,类型检查失败。
所以其实我们可以看到,TypeScript 也并不是无时无刻都必须要写上类型信息,只要定义时类型信息足够丰富,我们也可以选择性的偷懒。
最近有看到一种言论说:
连 C# 都在拼命动态化,为何要让 JavaScript 变成一个静态语言?
其实我觉得这里是有一些误解的:“静态类型检查并不等于静态语言或者说静态化”。
动态语言虽然可以在运行期动态改变类型,但是一个变量类型的改变也是受“业务需求”影响的。我相信没有谁会因为喜欢炫技而在代码里面不断改变一个变量的类型信息。而大部分时候,是因为业务的需求再配合语言的动态优势,可以让一个变量在不同阶段存储不同类型的数据。
我们回忆一下,其实大部分时候一个变量在它生命周期内依然是同一种类型一直用到销毁。只有小部分时候有那么几个变量会偶尔存储number,偶尔存储 string;或者在服务器返回的数据里面,根据 errCode,在 json 里面取出余下不同的信息。
所以一言以蔽之就是说:
“即使是动态语言,一个变量在生命周期内,它的类型变化可能性是非常有限的”。
那么 TypeScript 非常好的解决了这个问题,那就是“联合类型(Union)”。相信有过 C 语言基础的同学,对 Union 一定不会陌生。Union 就是说一个变量可以时而为 A 类型,时而为 B 类型,到底是什么类型,这个由开发者自行判断。
这不正好和“服务器返回的 json 数据格式”的情况非常吻合么?我们通过检查 errCode (在 restful API 下,更提倡用 header 来返回错误信息) 来判断余下部分应该为 Data 域还是是 Error 域。
一个 Union 的变量定义:
let name: number | string;
name = 123; //编译通过
name = "danney"; //编译通过
name = true; //编译失败
上述代码编译成 JavaScript 的 ES5 版本为:
var name;
name = 123;
name = "danney";
name = true;
对比可以发现,其实 TypeScript 完全没有没有给你带来什么学习成本和使用不便,它真的是在帮你,帮你检查类型、帮你排除那些粗心大意的小错误。
除此之外,在 TypeScript 里面还有很多突破静态类型的方法,比如你可以把一个变量强转为 any 类型,这样就不受编译器类型检查限制了。所以再也不要误认为 TypeScript 把 JavaScript 静态化了。
代码智能提示
习惯了在 Visual Studio 上的番茄工具,以及 Android Studio 和 XCode 上的智能提示功能,切到 JavaScript 时,真是有一些不习惯,虽然目前几大主流的 Web 开发工具也有提示功能,但是我觉得还是不够完善和智能。
毕竟 TypeScript 语言天生是带有类型信息的,可以完美识别出一个对象的类型,它自身有哪些属性和方法,然后调用这些方法时,也能智能匹配出它的函数原型。
说到这里就不能不提 Visual Studio Code 了,简称 VSCode。VSCode是微软新一代的轻量级跨平台开发工具,虽然也叫Visual Studio,但是我觉得和传统的 Visual Studio 家族工具已经没有太大关系了。它更像 WebStorm 或则 Sublime Text 这样的 Web 开发工具,轻量级、跨平台,支持各种语言或者平台插件,Debug 也很方便。
用 TypeScript 做开发,基本上 VSCode 就是标配了。VSCode 自带 TypeScript 插件,完美智能提示,类型推导,让你的开发效率极大提升。有意思的是 VSCode 和 TypeScript 编译器也是用 TypeScript 语言开发的,这就像一个鸡生蛋蛋生鸡的问题。很多年前我就是在想 Java 编译器也是由 Java 语言开发的,那么最早的 Java 语言又是用什么来编译的呢?
其实答案很简单,现代编译器基本上都不是从零裸写的,而是先书写这门语言的“语法描述文件”,然后由“编译器生成器”来读取“语法描述文件”,从而生成一个“编译器”。所以无论是 Java 还是 TypeScript,都是先用别的语言来创建一个最基本的编译器,再用这门语言来开发自己的新版编译器。
JavaScript 混合编程
很多同学一定担心 TypeScript 的生态环境不够丰富,相关的 lib 不够多,这一点完全不用担心。因为 TypeScript 和 JavaScript 可以在一个工程里面混合编程,TypeScript 的文件后缀是 .ts ,JavaScript 的文件后缀是 .js 仅此而已。ts 文件里面依然用 import 或者 require 来引入一个 module。
唯一的问题就是因为引入的 JavaScript 模块因为没有类型信息,会导致 VSCode 的智能提示无法使用而已。有智能提示时算是一个补充,没有智能提示时我觉得也不算吃亏。
不过其实这个问题基本上已经解决了,那就是 TypeScript 在编译为 JavaScript 时,可以自动产生一个叫做 .d.ts 的文件,它就是 js 的头文件,和 C 语言的 .h 文件一个道理。所以当一个编译后的 TypeScript 作为一个 lib 发布时,会附上 .js 和 .d.ts 两种文件。这样别人在导入你的 lib 时,也会获得智能提示功能。
那么对于一个基于 JavaScript 开发的老牌 lib,有没有办法也能智能提示呢?答案是肯定的。那就是现在有大量的开发者在为老牌 lib 人肉编写 .d.ts 文件。并且微软提供了一个叫做 typings 的工具,可以为你在线查找并下载一个 lib 的 .d.ts文件。typings 和 npm 使用类似,最新的 typings 也已经统一到 npm 工具上了,即使用 npm 就可以下载 .d.ts 文件了。还有最新版的 VSCode 也能做到自动分析你的包依赖,然后后台自动下载 .d.ts 文件。不用担心 JavaScript lib 没有 .d.ts 文件,连微信小程序的头文件都已经有了。
编译质量
对于担心 TypeScript 编译质量不如人手写优化的同学,你们也完全不用怕。TypeScript 编译逻辑主要有两部分,一部分是去掉类型信息,回归正常 JavaScript 语法;另外一部分就是翻译 ES6 和 ES7 语法,我目测编译结果和 babel 基本上一致,所以 babel 的编译结果如果你都不担心,TypeScript 的编译结果也就没什么好担心的了。
但是假如你连整个编译过程都觉得不能忍受的话,那一定是你写的代码太少。你来开发 iOS 和 Android 试试 ?当年 Windows QQ 编译一次可是要40分钟啊,我们一般都是点一下编译按钮,然后就去吃饭了。记得以前有从 JavaScript 入门的同学转去做 Android 开发时,狂吐槽分号的问题,我想说:真的是你见识的东西太少。可以热爱 JavaScript,但是不要让它蒙了你的眼。
调试
VSCode 提供了完善了调试工具,非常方便。因为我基本上是在 node 环境下开发,Web 调试不好说,不过 node 下再也不用蹩脚的弹到 Chrome 里面去 debug 了。TypeScript 编译后会自动生成 souremap 文件,所以 debug 时是不会涉及到编译后的 js 源文件的。就好像 C 语言编译成汇编后,你 debug 时也不用关注汇编一样。
所以另外一个需要注意的就是团队开发时,如果要修复 bug,千万千万不要修改编译后的 js 源文件。因为之前在网上看到有人在吐槽他用 TypeScript 开发的代码,编译成 js 上线后,有同学吭哧吭哧拿 js 代码去修复 bug,还一个劲吐槽原作者。脑子是个好东西,希望他也有。所以一定要避免在一个项目里面有的人用 TypeScript 开发,有的人用 JavaScript 开发。要么全都用,要么就别用。
其他
相比于 CoffeeScript 等变种 JavaScript 语言,TypeScript 语法几乎保持不变,没有什么学习难度。说学习曲线陡峭或者觉得团队内部不好培训、不好推进的,我真心觉得这个行业的从业者素质可能确实需要提升。
其他几个不错的新增语法点:
- TypeScript 提供了 enum ,我个人超级喜欢
- TypeScript 提供了泛型,解决了不少问题
- TypeScript 支持 JSX 语法,做 ReactJS 和 React-Native 开发的同学应该会非常喜欢
差不多就这些了,拥抱 TypeScript 吧,希望你也会喜欢!