写在前面
作者并没有任何可以作为背书的履历来证明自己写作这份手册的分量。
其内容大都来自于TypeScript官方资料或者搜索引擎获得,期间掺杂少量作者的私见,并会标明。
大部分内容来自于http://www.infoq.com/minibooks/typescript-c-sharp-programmers
你甚至可以认为这就是对这本英文小册子的翻译,实际上80%如此。
写给那些已经有编程基础,尤其是掌握c语言、c#、java这一类型的静态类型语言的同好。
鸣谢
先谢国家,虽然并不知道要谢些什么。
感谢我的太太和我的猫,他们的陪伴让我沉溺于电脑世界却不孤独。
感谢那些上班时间奋战在QQ群里的程序斗士们,他们让我始终秉持一颗艺术家的心。
感谢微软,TypeScript是微软的注册商标哦。
真正的开篇文字
只要你是对html5这个话题感兴趣的程序员,我们就离不开JavaScript这个话题。而新兴的TypeScript想必你也会有极大的兴趣。JavaScript如今到处都是,web、服务器(通过NodeJS)、移动应用(通过各种框架),所有这些,TypeScript都可以使用,并且可以为JavaScript扩展出面向对象和静态类型的特征。
TypeScript最初的灵感来自ECMAScript6,也就是未来的JavaScript。
TypeScript能让我们提前使用未来的语言特性,甚至更多,例如泛型这种语言特性。
TypeScript代码,最终会编译为地道的JavaScript,兼容一切使用JavaScript的场合。
编译过程主要是编译时检查,一点改写,删除类型批注和接口。删除类型批注和接口这个过程称为类型擦除(Type Erasure)。
我从网上找到一张很好的图片用来说明类型擦除,如下。
TypeScript的创造者是微软的一个开发团队,该团队的领导者是C#之父 和 Delphi之父,Anders Hejlsberg。
2012年10月1日发布,完全开源。
当前版本已达1.8,已经拥有了很多生产力项目,TypeScript的主页在http://www.typescriptlang.org/
我们后面将详细介绍TypeScript并对比他和其他语言的异同,主要是C#。
关于TypeScript到底是Compiling 还是 Transpiling
这个话题很难说清楚,但是很有必要在提到TypeScript的时候讲一下,这两个词:编译Compiling,Transpiling有人译作转译,这是一个英文计算机术语。一般认为转译是一种特殊的编译,当将一种源代码语言编译成另外一种源代码语言时,就称为转译。
当编译一个c#程序时,是由源代码语言C#编译为IL,这就不能称为Transpiling,因为他们是完全不同的东西。
而编译TypeScript程序时,他变成了另外一种源代码JavaScript,这个就称为Transpiling(转译)。
但无论如何,Transpiling是Compiling的特例,Transpiling也属于Compiling。
所以TypeScript转译为Javascript,TypeScript编译为JavaScript,都是没有问题的讲法。
TypeScript语言特性
这里快速介绍一下TypeScript的关键语法,比如显示类型批注、类、接口。
虽然C# Java程序员都很熟悉面向对象,但TypeScript并不基于C#,所以还是有所区别的。
TypeScript是静态类型语言,需要编译,拥有编译时类型检查的特性。
编译时类型检查能够确保类型安全,并方便开发更智能的自动完成功能,实际上TypeScript的各种开发工具都做得很不错。
比如VisualStudio,编写TypeScript文件时,就比编写JavaScript要聪明的多。这就是静态类型带来的好处。
TypeScript文件
TypeScript文件的扩展名为".ts",你可以使用很多工具编写.ts文件,比如visualstudio。更多信息,请看官网http://www.typescriptlang.org/
官网还提供了一个在线编写测试.ts文件的环境http://www.typescriptlang.org/Playground/
一个TypeScript应用包含多个TypeScript代码文件,一个代码文件可以包含多个Class,Class也可以组成模块。
模块的概念和C#中组织类型的namespace比较接近。
运行时,TypeScript编译得到的JavaScript可以通过Html标签<Script>加入网页中,也可以使用其他的模块加载工具,比如NodeJS就内置了模块加载工具。
不使用模块组织依赖的时候,一个TypeScript文件依赖另一个TypeScript文件,应该加上引用注释
(这是可选的,一般也不使用命令行编译,大部分图形化工具不加也可以)。
当使用模块加载工具的时候(比如RequireJS,或者NodeJS内置加载工具),代码如下:
不知道啥时RequireJS的同学请自行补课。
当你使用import语句的时候,是有两种模式的,CommonJS 和 AMD模式,他们编译为JavaScript生成的代码不同,这个根据你使用不同的加载工具请自己设置TypeScript编译选项。
注:RequireJS使用CommonJS模式,NodeJS使用AMD模式。
类型
TypeScript的基本类型有 string,number,boolean,null 和undefined.
因为JavaScript没有整数小数这些区分,所以TypeScript也没有添加,统一使用number。
另外TypeScript添加了一个any类型,当需要动态类型时使用。这个与C#的动态关键字是类似的。
TypeScript支持数组类型,通过在类型后面添加方括号定义,比如string[].
也可以自定义类型,在讲到class的时候我们细说。
类型推断
当右侧表达式足以确定变量的类型时,TypeScript可以自动确定变量的类型,这个特性C#也有。
比如这个代码
如果在VisualStudio中,鼠标悬停,依然可以看到这三个变量被识别出来是各自不同的类型。
类型推断还能处理更复杂的类型信息,比如这个代码,无需类型标注即可获得静态类型的好处。
写完这个代码以后,之后再键入example,再敲入.,也可以获得自动完成提示,.name,.id.collection.
类型推断始终是有局限性的,基本上是依靠右侧表达式来推断。如果声明变量时没有推断出来,事后赋值也推断不出来了。
比如下面的代码。
当声明变量时无法推断出,变量即被标注为 any 类型,any类型完全没有任何类型安全方面的保证。当然自动完成功能也没有。
类型批注
TypeScript的类型是设计为批注,而不是定义,是可选的,所以和C#的类型写在前面不同,类型是以可选的形式写在后面的。
我们将上面的所有代码都加上类型标注
这些标注的添加和上面自动类型推断的结果是一样的。但是阅读代码的人就可以一眼看出。
模块、类型、接口
TypeScript的模块用于代码组织,类似C#的namespace。一个模块可以包含多个类和接口。可以将类和接口私有化或者导出,导出的意思就是公开,让其他模块可以访问他们。
TypeScript的class和C#的class意义相同。实际上TypeScript的一个亮点就是他隐藏了JavaScript的原型设计,而是采用了更流行的基于类型的面向对象方式。你可以扩展其他的class,实现多个接口,添加构造函数,公开属性和方法。这些都和c#的class很相似。
属性和方法可以使用public 或private访问修饰符 修饰。当在构造函数的参数上使用访问修饰符的时候,他们会自动为该类型添加同名的属性。请非常小心这个语法,这和很多语言都不同
如图中Point的构造函数参数x,y 使用了public 访问修饰符,所以会自动生成Point的成员变量x和y,这是TypeScript的特有语法。
图中的export 关键字使类和接口在模块外部可见。实现接口使用implements关键字,继承类使用extends关键字,这点和C#直接用一个冒号表达不同。
当你扩展一个类时,用super关键字调用基类的方法。用this关键字来调用当前类的属性和方法。重写基类的方法是可选的。构造函数必须要调用基类的构造函数,编译器会提醒你的。
模块名称包含点,一个模块可以定义在多个文件中。模块的名字别太长,访问的时候打字会比较累。下图是一个比较累的例子
函数(或者叫功能)的参数
TypeScript的函数参数拥有丰富的特性:可选参数、默认参数、不定参数。
有一些和其他语言的设计不太相同。下面将一一说明。
虽然在设计上,这些参数特性都不是必要的,但是TypeScript的这些地方都有些特殊性,你得了解一下,以备看懂。
可选参数的设计和C#基本一致,符号用? 表示可选参数,可选参数必须出现在必选参数之后。
默认参数只要在定义时给附上值就行了,并且和c#不同,TypeScript的默认参数不需要是常量,运行时可解释即可,后文会有说明
下图是特殊的默认参数
不定参数可以指定任意数量,可以为零,这个设计也和c#类似,只是要添加三个点。
下面就是不定参数的一个典型使用场景
函数重载
Javascript是不允许重名函数的,TypeScript实现的重载和c#有很大的不同。
TypeScript的重载是要把所有的函数签名写出来,写一份实现,并且最后一个函数签名要能包含上面所有的函数签名。
如下图:
和C#三个重载的函数就拥有三个函数体不同,TypeScript的重载其实全是 overloadedMethod(input:any)这最后一个签名的实现,上面的两个只是两个兼容的签名。但是配合可选参数还是能够表现出和c#的函数重载类似的调用方式。函数编写方式免不了要写很多的if了。
枚举
TypeScript的枚举非常类似C#,你可以指定值。
也可以反过来通过枚举值取到枚举的名字
这里面的细节就不再赘述了,你可以观察枚举编译为JavaScript以后是什么样子
泛型
对c#程序员来说,TypeScript的泛型很熟悉,基本上是一致的设计。
类型约束
C#使用where关键字标记类型约束,TypeScript在尖括号内使用extends关键字,效果相同。
下面的例子中IExample约束了泛型必须是IMyInterface和他的派生类。
如果像下图这样用的话,就能约束为同时继承ClassOne和ClassTwo的类型。很费解吧,请特别注意。
这是因为本质上TypeScript的类型系统并不那么严格,下面的章节会详细解释TypeScript的类型系统
你也可以使用泛型作为泛型的类型约束,如下
结构类型
C#的类型系统是强制标记的,对象的类型必须显示声明。即使两个类型拥有完全相同的结构,他们也不是相同的类型。
TypeScript的类型系统是结构上的,建筑结构,层次型的。结构相同的类型即可认为是同一类型。
下面是C#中的一个例子
ClassA 和 ClassB是完全不同的类型,他们之间是不同的,必须显示继承接口才能让他们兼容。
而在TypeScript中不是这样,我们用如下的例子来证明。ClassA ClassB ExampleC 拥有签名一致的函数,所以他们就可以兼容。
TypeScript的结构类型系统意味着你在c#中的观念不再成立,class name不是关键。这需要我们写代码的时候时刻注意。
这玩意会让代码千变万化,如果你熟悉C#或者JAVA,这可能会让你困惑。
看下面的例子,不需要class关键字,也会实实在在的产生类型。
在这个例子中,会产生一个匿名的类型
这个匿名的类型可以让开发工具提供自动完成功能,编译器也会检查。如果你尝试将objA的name赋值一个数值,编译器会检查到告诉你错误。编译器还会为数组推断类型。
访问修饰器
TypeScript的访问修饰器可能会给你一种弱小的感觉,的确如此。他仅仅是一个编译时功能。
模块中的一切均为私有,除非加上export关键字。没有export关键字的类型仅能在模块内实用。
类的内部,一切均为公开,除非加上private 关键字。public只是为了看起来意图明确。
TypeScript的访问修饰器就是这样而已,没有c#的 internal 和 protected这种修饰器,想要c#类似的功能就放弃吧。
TypeScript特性
内存管理
当你运行TypeScript 程序时,他会被编译为JavaScript程序来运行。JavaScript的内存管理和C#比较接近。内存在对象创建时分配,在对象不再使用时回收。不同的是垃圾回收机制在JavaScript的世界里没有标准统一的实现,这意味着你的JavaScript程序的内存性能相比C#难以预测。
比如说,在比较早的浏览器上,可能使用的是引用计数垃圾回收机制,当一个对象的引用计数达到0时,将回收内存。这种垃圾回收机制比较快速和即时。但是当发生循环引用时,引用计数将永远也无法达到零。
比较新的浏览器使用标记与清扫垃圾回收机制来找出不可访问到的对象,很大程度上避免了这个问题。这种垃圾回收机制比较缓慢,但他能避免循环引用导致的内存泄露。
关于两种垃圾回收机制有很多的资料可以研究,这里只是想告诉你,别相信你的直觉,浏览器会很不同。
资源释放
在TypeScript中,通常都不会使用到非托管的资源,Node.JS中多一点。大部分浏览器将底层交互API设计为回调控制,不向你暴露对象,不需要你自己管理非托管资源。比如下面接近传感器的API使用方法。
如你所见,使用回调并不需要管理任何引用。Node.js中有时会碰到需要自己释放的对象,你要保证释放这些对象,否则就会内存泄露。你可以使用 try finally 块,释放代码写在 finally 块中。这样就能保证就算发生任何错误,释放代码都会执行。
异常
在TypeScript中,你可以用throw关键字引发一个异常。
在JavaScript中,throw可以throw任何类型的东西。但是再TypeScript中,throw的必须是一个Error对象。
要自定义异常,可以继承Error类。当你需要一个特定的异常行为或者你希望catch块可以分辨异常类型时,自定义异常就会很有用。
处理异常需要使用try catch语句块。大体上和c#的使用方法是很接近的,但是c#支持多个catch块,TpyeScript不可以,你可以用error.name来区分异常类型。
如果需要一些即使发生异常也会调用的代码,你就需要finally语句块了,他们的执行顺序如下图
数组
TypeScript 从0.9开始,数组就是可以指定内容的准确类型。用法是,用类型批注加上方括号。TypeScript会检查加入到数组中的条目的类型,也会推断从数组中检索的条目的类型。
因为TypeScript没有自己的框架库,所以只能使用内置的JavaScript函数和浏览器接口,没有像C#的List<T> 这样的泛型库。但这并不能阻止我们自己创造一个。下面是一个泛型列表类的例子,只是个演示。
上面这个List类演示了许多TypeScript的语言特性和使用方法,就像C#的List<T>那样。下面是如何使用这个List类的例子。
日期与时间
TypeScript中的Date对象是基于JavaScript Date 对象的,他是由从1970年零点 utc时间开始的毫秒数。
Date对象能够接受各种精度的初始化,如下。
你也可以使用RFC和ISO格式的字符串初始化Date对象,当然,毫秒数也可以(从1970年1月1日开始)。
请注意,这不是时间戳,时间戳单位是秒,数字比这个少几个零。
Now
你可以访问现在的日期与时间,通过方法Date.Now();返回值是毫秒,你如果需要用Date对象去操作。需要用这个值去初始化一个Date对象
Date 的方法
当你有一个Date对象时,你可以用内部方法获取日期的一部分。有两套方法,一套本地,一套UTC。在下面的实例中,你看到我们把getUTCMonth的值加了1.因为返回值从零开始,,2 月是 1,等等。
,星期一为1。
也可以利用各种格式显示日期。如下。
事件
TypeScript中的事件是 DOM API(Document Object Model),DOM API事件是一套标准的鼠标和键盘交互和对象和窗体事件。下面的事件列表并非详尽无遗。
鼠标事件
事件 |
触发时机 |
onblur |
焦点离开目标组件 |
onclick |
目标组件检测到鼠标单击 |
ondblclick |
目标组件检测到鼠标双击 |
onfocus |
目标组件获得焦点 |
onmousedown |
目标组件检测到鼠标按下 |
onmousemove |
目标组件检测到鼠标指针移动 |
onmouseover |
鼠标指针经过目标组件 |
onmouseout |
鼠标指针离开目标组件 |
onmouseup |
目标组件检测到鼠标按钮松开 |
键盘事件
事件 |
触发时机 |
onkeydown |
目标组件键盘按下 |
onkeypress |
目标组件键盘按键按下并松开 |
onkeyup |
目标组件键盘松开 |
对象事件
事件 |
触发时机 |
onload |
对象加载(文档或图像等) |
onresize |
对象尺寸改变 |
onscroll |
一个文档滚动 |
onunload |
文档关闭 |
表单事件
事件 |
触发时机 |
onchange |
目标输入框的内容改变 |
onreset |
表单重置(清空) |
onsubmit |
表单提交 |
自定义事件
TypeScript中自定义事件和DOM内置事件采用相同的机制发布和侦听。任何事件都可以有多个侦听器和多个发布者。下面是一个侦听自定义事件的例子
下面的类发布自定义的事件:
执行顺序
事件按照他们被注册的顺序执行。这个因素很重要,因为事件处理程序是序列执行,不是同时,顺序会影响逻辑。
当你注册两个事件侦听器,并且第一个执行了5秒钟。那么第二个就不会执行,一直要等到第一个执行完毕之后才会被执行。如果第一个侦听器出错,第二个还是会执行。
你可以用setTimeOut时间参数为零的方式去执行你的长时间操作,这相当于多线程,就不会堵住其他的事件执行了。
框架
TypeScript捆绑了那些常用对象和方法。因为Web标准在不断的演变,你可能会经常发现一些新的东西还没有被包含在标准库中。你可以查看你计算机上的TypeScript标准库文件,他在SDK目录中,通常可能是:
C:\Program Files (86) \Microsoft SDKs\TypeScript\lib.d.ts
你永远也不应该修改这个文件,如果你需要新的定义,我们可以继续增加新的定义文件。
下面的是 ClientRectList 在TypeScript库文件中的当前定义。
如果要为 ClientRectList添加一个新的isOrdered属性,只需简单的在你自己的程序中添加以下接口扩展程序,即可立即使用。
当它被添加到标准库时,你自己的扩展会引发一个生成错误,到时把它删除了就行了。
当你创建ClientRectList的实例时,你将能够访问过去的方法,以及获取新的 isOrdered 属性。
除了这些TypeScript标准库的定义,TypeScript没有捆绑任何其他的框架。在前一章中,我们谈到了,你可以重建你的各种功能,比如像C#一样,因为TypeScript有着丰富的语言功能。你也可以访问所有现有的 JavaScript 框架,jQuery、Knockout、Angular和其他数以百计的框架。但是他们都用纯 JavaScript开发,你不会得到增强的开发体验,除非,你有一个匹配的定义文件。幸运的是,有一个项目,他们致力于为所有的JavaScript框架提供TypeScript定义。Definitely Typed 项目,Github地址是:
https://github.com/borisyankov/DefinitelyTyped
创建定义
有时你会想使用JavaScript的库、框架或者工具集。TypeScript编译器难以理解这些外部的代码,因为他们没有类型信息。
动态定义
在程序中使用外部代码的最简单方法是声明一个any变量。TypeScript编译器允许any对象调用任何方法于属性。这会让你通过编译,但是没有自动完成和类型检查。
在此示例中的特殊关键字是declare。这会告诉编译器,这个变量是外部的,这个仅仅是编译时的关键字,编译为JavaScript时会被擦除。
类型定义
为了使用外部JS代码能获取更佳的开发体验,你需要提供更全面的定义。可以声明变量、
模块、
类和函数,定义外部代码的类型信息。Declare关键字仅仅需要在定义的开头使用一次。
如果希望外部代码可以由TypeScript扩展,定义为class,否则,定义为interface。
这两种定义方法的唯一区别是能否继承扩展。这两种情况下,类型信息都仅为编译时使用,编译为JavaScript后都会进行类型擦除。
很多时候,定义会组成一系列接口,组合成很大的一幅图景。你甚至可以为TypeScript不能实现的JavaScript方法创建定义。
例如,它是如下图这样做:
在此示例中有一个名为move的不可思议的属性,他可以作为函数使用。这样一来,我们就可以声明几乎任何的JavaScript代码, jQuery、Knockout、Angular、RequireJS 和其他的老的JavaScript代码,让你能在TypeScript中使用它们而不必重写。
一些有用的小技巧
获取运行时类型
如果在运行时,想要得到类的名称,在C#中有反射这种方法,但TypeScript没有明显的内置方法。
静态方法 getType,检查编译后的 JavaScript 函数,然后提取它的名字,这就是TypeScript中的类的名称。
这种技术的限制是你不能获得包含类和模块的完整名称,这意味着:
MyModule.Example和 SomeOtherModule.Example 和没包装的叫做Example的类,它们所有的返回字符串均为Example。
扩展原生对象
TypeScript内置的定义文件描述了JavaScript原生对象和函数。你可以在lib.d.ts库文件中查看它们。你会注意到大部分都是使用interface定义的,不希望你继承它们。但我们还是可以扩展它们的。
例如,下面是给NodeList对象增加onclick事件。
但是这个代码会被TypeScript编译器警告,告诉你,NodeList对象没有onclick函数。
为了解决此为题,你需要在你自己的代码中添加定义代码。
这个接口声明可以写在任何引用的ts文件中。
完