
Angular是用一种类似于JavaScript的语言——TypeScript
——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。

图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript
5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。

从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur
与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN
(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了
,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();

我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();

在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the
prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!