Giter VIP home page Giter VIP logo

blog's People

Watchers

 avatar  avatar

blog's Issues

ts programming (temp)

#3 TypeScript 介绍

通过上一节的介绍,相信读者已经对 Angular 的历史有了全局的认识,Angular 最终选择 TypeScript 作为其官方最主要的构建语言,对开发者来说,这意味着掌握 TypeScript 语言将更有利于高效地开发 Angualr 应用。本章将结合 Angular 的使用场景来介绍 TypeScript 这门语言,相信读完本章后读者应该具备可以用 TypeScript 开发 Angular 应用的能力了。

3.1 概述

3.1.1 介绍

TypeScript 是由 C# 语言之父 Anders Hejlsberg 主导开发的一门定位为 JavaScript 超集的编程语言。TypeScript 本质上是向 JavaScript 语言添加了可选的静态类型,它是基于类的面向对象编程语言,支持诸如类、继承、接口、命名空间等特性。关于 ES5、ES6、TypeScript 的关系,如下图所示:

Dialog

图 3-1 ES5、ES6 跟 TypeScript 关系

JavaScript 这种弱类型,简单自由(从另一种角度来说是随意散漫)的编写模式对开发者的技术水平要求较高,初学者跟资深开发者之间的代码质量可能差别很大,这不利于项目的维护。ES6 引入了变量增强、模板字符串、箭头函数、类、迭代器、生成器、模块和 Promises 等新特性,极大地增强 JavaScript 语言的开发能力。另一方面,2009 年开始设计的 TypeScript 语言,经历了几年的发展后,最终向 ECMAScript 靠拢,实现了其标准,并在此基础了上做了进一步增强,主要有类型校验、接口、装饰器等特性,这使得代码编写更规范化,本章后续将会介绍这些增强特性。

本章并不会刻意去突出 ES6 与 TypeScript 的异同,有 ES6 基础的读者学习成本会很低,甚至可以只关注接口、装饰器等部分内容即可。但 TypeScript 的核心是增强类型的处理,建议还是把所有知识点学习下,感受 TypeScript 带给我们的美妙编程体验。

3.1.2 安装 TypeScript

TypeScript 工具一般是通过 npm 进行安装,要查看 npm 是否已经安装,可以运行如下命令:

$ npm -v

这里我们将使用 TypeScript 2.0 版本,安装 TypeScript 命令如下:

$ npm install -g typescript

安装完成后 ,我们来编写第一个 TypeScript 程序,并保存到文件hello.ts 中,文件的代码如下所示:

console.log('Hello TypeScript!');

在浏览器中要运行 TypeScript 程序,必须先编译成浏览器能识别的 JavaScript 代码,我们可以通过 tsc 编译器来编译 TypeScript 文件,生成与之对应的 JavaScript 文件,编译过程如下:

$ tsc hello.ts

此时会在目录下面看到一个文件 hello.js,同时可以看到命令行的运行结果:

$ Hello TypeScript!

3.2 基本类型

TypeScript 提供了布尔值(boolean)、数字(number)、字符串(string)、数组(array)、元组(tuple)、枚举(enum)、任意值(any)、无类型值(void)、空值(null 和 undefined)和 never 这些基本类型。其中元组、枚举、无类型值和 never 是 TypeScript 有别于 ES6 的特有类型。

在 TypeScript 中声明变量,需要加上类型声明,如 boolean、number 或 string 等。通过静态类型约束,在编译时执行类型检查,这样可以避免一些低级的错误。下面将介绍这些基本类型。

3.2.1 布尔值

布尔值是最简单的数据类型,只有 true 和 false 两种值。下面定义了一个 boolean 类型的 flag 变量,并赋值为 true。由于 flag 被初始化为 boolean 类型,如果再赋值为非 boolean的其它类型值,编译时会抛出错误。

let flag: boolean = true;
flag = 1; // 报错,不能把 number 类型的值赋给 boolean 类型的变量。

3.2.2 数字

TypeScript 的数字都是浮点型,也支持 ES6 中的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

3.2.3 字符串

跟 JavaScript 中的字符串一样,用单引号(')或双引号(“)来表示。另外,TypeScript 还支持模板字符串,可以定义多行文本和内嵌表达式,放到反引号(`)内,以 ${ expr } 的形式嵌入变量,在处理拼接字符串的时候很有用。

let name: string = `Angular`;
let years: number = 5;
let words: string = `你好,今年是 ${ name } 成立 ${ years + 1 } 周年`;

3.2.4 数组

TypeScript 数组的操作类似于 JavaScript 中数组的操作,TypeScript 建议开发者最好只为数组元素赋一种类型的值。TypeScript 有两种数组定义方式,如下所示:

let array: number[] = [1,2]; 
// 或者使用数组泛型
let array: Array<number> = [1,2];

3.2.5 元组

元组类型用来表示一个已知元素数量和类型的数组,各元素的类型不必相同。下面定义了一对值分别为 string 和 number 类型的元组,代码如下:

let x: [string, number];
x = ['Angular', 25]; // 运行正常
x = [10, 'Angular']; // 报错
console.log(x[0]); //  'Angular'

3.2.6 枚举

枚举(enum)是一个可被命名的整型常数的集合,枚举类型为集合成员赋予有意义的名称,增强可读性。

enum Color {Red, Green, Blue};
let c: Color = Color.Blue; // 输出:2

枚举默认下标是 0,可以手动修改默认下标值,示例如下:

enum Color {Red = 2, Blue, Green = 6};
let c: Color = Color.Blue; // 输出:3 

3.2.7 任意值

任意值(any)是 TypeScript 针对编程时类型不明确的变量使用的一种数据类型,它常用于以下三种情况:

  1. 变量的值会动态变化时,比如来自用户输入或第三方代码库,any 可以让这些变量跳过编译阶段的类型检查。
let x: any = 1; // number 类型
x = "I am a string"; // string类型
x = false; // boolean类型
  1. 改写现有代码时,any 允许在编译时可选择地包含或移除类型检查。
let x: any = 4;
x.ifItExists(); // 正确
x.toFixed(); // 正确
  1. 定义存储各种类型数据的数组时
let arrayList: any[] = [1, false, "fine"];
arrayList[1] = 100;

3.2.8 无类型值

void 表示没有任何类型。当一个函数没有返回值时,意味着返回值类型是 void。

function hello(): void {
  alert("hello world");
}

3.2.9 Null 和 Undefined

默认情况下,null 跟 undefined 是其它类型的子类型,可以赋值给其它类型如 number 等,此时赋值后的类型会变成 null 或 undefined,致力于类型校验的 TypeScript 设计者们显然不希望这种类型变化给开发者带来额外的困扰。在 TypeScript 中启用 严格的空校验(--strictNullChecks) 特性,就可以使得 null 跟 undefined 只能被赋值给 void 或本身对应的类型,例子如下:

// 启用 --strictNullChecks
let x: number;
x = 1;  // 运行正确
x = undefined;  // 运行错误
x = null;  // 运行错误

上面例子中 x 只能是 number 类型。如果一个类型可能出现 null 或者 undefined,可以用 | 来支持多种类型,如下面例子:

// 启用 --strictNullChecks
let x: number;
let y: number | undefined;
let z: number | null | undefined;
x = 1;  // 运行正确
y = 1;  // 运行正确
z = 1;  // 运行正确
x = undefined;  // 运行错误
y = undefined;  // 运行正确
z = undefined;  // 运行正确
x = null;  // 运行错误
y = null;  // 运行错误
z = null;  // 运行正确
x = y;  // 运行错误
x = z;  // 运行错误
y = x;  // 运行正确
y = z;  // 运行错误
z = x;  // 运行正确
z = y;  // 运行正确

上面例子中 y 允许 number 跟 undefined 类型的数据值, 而 z 还额外支持 null。TypeScript 官方建议在新开发的应用中,都启用 --strictNullChecks 特性,有利于开发更健壮的代码。

3.2.10 Never

never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),如下面例子:

let x: never; 
let y: number;
x = 123; // 运行错误:数字类型不能转为never

// 运行正确:never 类型可以赋值给 never 类型
x = (() => { throw new Error('exception occur') })();

// 运行正确:never 类型可以赋值给 number 类型
y = (() => { throw new Error('exception occur') })();

// 返回 never 的函数必须抛出异常
function error(message: string): never {
  throw new Error(message);
}

// 返回 never 的函数必须有无法被执行到的终止点
function loop(): never {
  while (true) {
  }
}

3.3 声明命令

在 TypeScript 中,支持 let 和 const 的声明命令。let 与 var 相似,但可以避免以往开发中常见的一些问题,如变量的全局污染。const 能够声明一个常量,防止二次赋值。

3.3.1 let 声明

let 与 var 声明变量的写法类似,如:

let hello = "Hello!";

不同于 var,let 声明的变量只在块级作用域内有效,如:

function f(input: boolean) {
  let a = 100;
  if (input) {
  let b = a + 1; // 运行正确
    return b;
  }
  return b; // 错误: b 没有被定义
}

这里定义了 2 个变量 a 和 b,a 的作用域是在 f 函数体内,而 b 的作用域是在 if 语句块里。块级作用域还有一个问题,就是变量不能在它声明之前被读取或赋值,如下代码所示:

a++; // 在声明之前使用是不合法的
let a;

块级作用域变量的重复定义

在使用 var 声明里,不管声明几次,最后都只会得到最近一次声明的那个值,例如:

var x = 2;
console.log( x + 3 ) // 输出:5
var x= 3
console.log( x + 3 ) // 输出:6

上面的用法不会报错,let 声明对此做了限制,如下:

let x = 2;
let x = 3; // 不能在一个作用域里多次声明

注意下面两种情况的 let 声明的对比:

function f(x) {
  let x = 100; // 报错, x已经在函数参数处声明
}
function f(condition, x) {
  if (condition) {
    let x = 100;  // 运行正常
      return x;
    }
    return x;
}

f(false, 0); // return 0
f(true, 0);  // return 100

下面再看一个块级作用域变量的获取例子:

function funcCity() {
    let getCity;
    if (true) {
       let city = "Shenzhen";
       getCity = function() {
         return city;
       }
    }
    return getCity();
}

每次进入一个块级作用域,它就创建了一个变量的执行环境,就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。上例已经在 getCity 的环境里获取到了 city,所以就算 if 语句执行结束后仍然可以访问它。所以,当执行 funcCity() 的时候,将输出 “Shenzhen”。

3.3.2 const 声明

const 与 let 声明相似,它与 let 拥有相同的作用域规则,但 const 声明的是常量,不能重新赋值,否则将编译错误,请看下面的例子:

const CAT_LIVES_NUM = 9;
const kitty = {
  name: "Aurora",
  numLives: CAT_LIVES_NUM
}

// 错误
kitty = {
  name: "Danielle",
  numLives: CAT_LIVES_NUM
};

kitty.name = "Kitty";  // 正确
kitty.numLives--;  // 正确

3.3.3 解构

解构是 ES6 的一个重要特性,TypeScript 在 1.5 版本中也开始增加了对解构的支持。所谓解构,就是将声明的一组变量与相同结构的数组或者对象的元素数值一一对应,并将变量相对应元素进行赋值。解构可以帮助开发者非常容易地实现多返回值的场景,这样不仅写法简洁,也增强代码的可读性。

TypeScript 支持以下形式的解构:

  • 数组解构

数组结构是最简单的解构类型,如下例所示:

let input = [1, 2];
let [first, second] = input;
console.log(first); // 相当于input[0]: 1
console.log(second); // 相当于input[1]: 2

也可作用于已声明的变量:

[first, second] = [second, first]; // 交换位置

或作用于函数参数:

function f([first, second]= [number, number]) {
  console.log(first);
  console.log(second);
}

f([1, 2]);

也可使用 “...name” 语法创建一个剩余变量列表,“...” 三个连续小点是展开操作符,用于创建可变长参数列表,使用起来非常方便,例子如下:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 输出:1
console.log(rest); // 输出:[ 2, 3, 4 ]

对象解构

对象解构有趣的地方是一些原本需要多行编写的代码,变得可以用一行完成,代码很简洁,可读性强,对象解构的例子如下:

var test = { x: 0, y: 10, width: 15, height: 20 };
var {x, y, width, height} = test;
console.log(x, y, width, height); // 输出:0,10,15,20

解构虽然很方便,但使用时还得多注意,特别是深层嵌套的场景,是比较容易出错的。

3.4 函数

不管什么编程语言,都少不了函数这个重要的概念,它用于定义一个特定的行为。TypeScript 为 JavaScript 函数添加了额外的功能,使函数变得更易用。

3.4.1 函数类型

TypeScript 提供了两种函数类型:命名函数和匿名函数。

命名函数

function max(x: number, y: number): number {
  return x > y ? x : y;
}

匿名函数

let myMax = function(x: number, y: number): number { return x > y ? x : y;};

在上例中,参数类型和返回值类型这两部分都是必须的。在调用时,只做参数类型和个数的匹配,不做参数名的校验。

3.4.2 可选参数和默认参数

JavaScript 里,被调函数的每个参数都是可选的。 TypeScript 却不一样,被调函数的每个参数都是必传的,在编译时,会检查函数每个参数是否传入了值。简而言之,传递给一个函数的参数个数必须和函数定义时的参数个数一致,例如:

function max(x: number, y: number): number {
  return x > y ? x : y;
}

let result1 =  max(2);  // 报错
let result2 =  max(2, 4, 7);  // 报错
let result3 =  max(2, 4);  //  运行正常 

但是经常会遇到根据实际需要来决定是否传入某个参数的情况,Typescript 提供了 “?”,即在参数名旁边加上 “?” 来使其变成可选参数,如:

function max(x: number, y?: number): number {
    if(y)
        return x > y ? x : y;
    else
        return x;
}

let result1 =  max(2); // 运行正常   
let result2 =  max(2, 4, 7);  // 报错
let result3 =  max(2, 4);  // 运行正常  

需要注意的是可选参数必须位于必要参数的后面

TypeScript 还支持初始化默认参数,如果函数的某个参数设置了默认值,当该函数被调用时,如果没给这个参数传值或者传的值为 undefined 时,此时这个参数的值就为设置的默认值。如代码所示:

function max(x: number, y = 4): number {
  return x > y ? x : y;
}

let result1 =  max(2); // 运行正常
let result2 =  max(2, undefined);  // 运行正常
let result3 =  max(2, 4, 7);  // 报错
let result4 =  max(2, 4);   // 运行正常   

带默认值的参数不必放在必要参数的后面,但如果默认值参数放到了必要参数的前面,用户必须显示的传入 undefined,例如:

function max(x=2, y: number): number {
  return x > y ? x:y;
}

let result1 =  max(2);      // 报错   
let result2 =  max( undefined, 4);  // 运行正常
let result3 =  max(2, 4, 7);  // 报错
let result4 =  max(2, 4);   // 运行正常

3.4.3 剩余参数

上面介绍了必要参数、默认参数和可选参数,它们的共同点是只能表示某一个参数,当需要同时操作多个参数,或者并不知道会有多少参数传递进来时,这就需要用到了 TypeScript 里的剩余参数。在 TypeScript 里,所有的可选参数都可以放到了一个变量里,如下代码所示:

function sum(x:number, ...restOfNumber:number[]): number {
  let result = x;
  for(let i = 0; i < restOfNumber.length; i++){
    result += restOfNumber[i];
  }
  return result;
}
let result = sum(1, 2, 3, 4, 5);

注意:剩余参数可以理解为个数不限的可选参数,即剩余参数包含的参数个数可以为零到多个。

3.4.4 重载

函数重载通过为同一个函数提供多个函数类型定义来达到实现多种功能的目的,TypeScript 支持函数的重载,示例如下:

function css(config: {});
function css(config: string, value: string);
function css(config: any, value?: any) {
    if (typeof config == 'string') {
        ...
    } else if (typeof config == 'object') {
        ...
    }
}

这个函数有三个重载,编译器会根据参数类型来判断该调用哪个函数。TypeScipt 重载通过查找重载列表来实现匹配,根据定义的优先顺序来依次匹配,所以在定义重载的时候,建议把最精确的定义放在最前面。

3.4.5 this 和箭头函数

JavaScript 的 this 是个重要的概念,学习 TypeScrip 有必要弄清楚 this 工作机制,这能帮助我们避免一些隐蔽的 bug。

let gift = {
  gifts: ["teddy bear", "spiderman", "dinosaur", "Thomas loco",
  'toy bricks', 'Transformers'],

  giftPicker: function() {
  return function() {
    let pickedNumber = Math.floor(Math.random() *6);

    return this.gifts[pickedNumber];
  }
  }
}

let pickGift = gift.giftPicker();
console.log("you get a : " + pickGift());

运行程序,发现对话框并没有弹出来,这是因为 giftPicker 返回的函数里的 this 被设置成了 window 而不是 gift 对象。因为这里没有对 this 进行动态绑定,因此这里的 this 就指向了 window。

TypeScript 提供的箭头函数( => )很好地解决了这个问题,它在函数创建的时候就指定了 this 值,而不是在函数调用的时候。

let gift = {
  gifts: ["teddy bear", "spiderman", "dinosaur", "Thomas loco",'toy bricks','Transformers'],

  giftPicker: function() {
  return () => {
    let pickedNumber = Math.floor(Math.random() *6);

    return this.gifts[pickedNumber];
  }
  }
}

let pickGift = gift.giftPicker();
console.log("you get a : " + pickGift());

3.5 类

传统的 JavaScript 程序使用函数和基于原型的继承来创建可重用的类,这对于习惯了面向对象方式编程的程序员来说不是很友好。而在 TypeScript 中可以支持使用基于类的面向对象方法。

3.5.1 类的说明

下面看一个定义类的例子:

class Car {
  engine: string;
  constructor(engine: string) {
    this.engine = engine;
  }
  drive(distanceInMeters: number = 0) {
  console.log( `A car runs ${distanceInMeters}m  powered by` + this.engine);
  }
}

上面声明一个 Car 类,这个类有三个类成员: 类属性 engine、构造函数和 drive 方法,其中类属性 engine,可通过 this.engine 访问。下面实例化一个 Car 的新对象,并执行构造函数初始化。

let car = new Car("petrol");

调用成员方法并输出结果:

car.drive(100); // 输出:A car runs 100m powered by petrol

3.5.2 继承与多态

封装、继承、多态是面向对象的三大特性。上面的例子中把汽车的行为写到一个类中,即所谓的封装。在 TypeScript 中,用 extends 关键字即可方便实现继承,例子如下:

class MotoCar extends Car {
  constructor(engine: string) { super(engine); }
}

class Jeep extends Car {
  constructor(engine: string) { super(engine); }
  drive(distanceInMeters:number = 100) { 
    console.log("Jeep...");
    return super.drive(distanceInMeters);
  }
}

let tesla = new MotoCar("electricity"); 
let landRover: Car = new Jeep("petrol"); // 多态

tesla.drive(); // 调用父类的 drive 方法
landRover.drive(200); // 调用子类的 drive 方法

从上面的例子可以看到,MotoCar 和 Jeep 是基类 Car 的子类,通过 extends 来继承父类,子类可以访问父类的属性和方法,子类也可以重写父类的方法。Jeep 的 drive 方法重写了 Car 的 drive 方法,这样 drive 方法在不同的类中具有不同的功能,这就是所说的多态。

即使 landRover 被声明为 Car 类型,它依然是子类 Jeep,landRover.drive(200) 调用的是 Jeep 里的重写方法。派生类构造函数必须调用 super(),它会执行基类的构造方法。

3.5.3 修饰符和参数属性

在类中的修饰符可以分为公共(public)、私有(private)和受保护(protected)的类型。

public 修饰符

在 TypeScript 里,每个成员默认为 public,所以上面的例子中,我们可以自由的访问类里定义的成员。给 Car 类加上 public 后,如下所示:

class Car {
  public engine: string;
  public constructor(engine: string) {
    this.engine = engine;
  }
  public drive(distanceInMeters: number = 0) {
    console.log( `A car runs ${distanceInMeters}m  powered by` + this.engine);
  }
 }

private 修饰符

当成员被标记成 private 时,就不能在类的外部访问它,如代码所示:

class Car {
  private engine: string;
    constructor(engine: string) {
      this.engine = engine;
    }
}

new Car("petrol").engine; // 报错: 'engine' is private;

ES6 并没有提供对私有属性的语法支持,但是可以通过闭包来实现私有属性。

protected 修饰符

protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected 成员在派生类中仍然可以访问,例如:

class Car {
  protected engine: string;
  constructor(engine: string) {
    this.engine = engine;
  }
  drive(distanceInMeters: number = 0) {
    console.log( `A car runs ${distanceInMeters}m  powered by`+ this.engine);
  }
}

class MotoCar extends Car {
  constructor(engine: string) { super(engine); }
  drive(distanceInMeters: number = 50) {
    super.drive(distanceInMeters);  
  }
}


let tesla = new MotoCar("electricity");
console.log(tesla.drive()); // 运行正常
console.log(tesla.engine); //  报错

注意,由于 engine 被声明为 protected,所以不能在外部访问它,但是仍然可以通过它的继承类 MotoCar 来访问。

参数属性

参数属性是通过给构造函数参数添加一个访问限定符(public、protected 和 private)来声明。参数属性可以方便地让我们在一个地方定义并初始化一个成员。使用参数属性对 Car 类进行改造,如下所示:

class Car {
  constructor(protected engine: string) {}
  drive(distanceInMeters: number = 0) {
    console.log(`A car runs ${distanceInMeters}m  powered by` + this.engine);
  }
}

在构造函数里通过 protected engine: string 来创建和初始化 engine 成员,从而把声明和赋值合并至一处。

3.5.4 静态属性

类的静态成员存在于类本身而不是类的实例上,类似在实例属性上使用 this. 来访问属性,我们使用 类名. 来访问静态属性。

class Grid {
  static origin = {x: 0, y: 0};
  calculateDistanceFromOrigin(point: {x: number; y: number;}) {
    let xDist = (point.x - Grid.origin.x);
    let yDist = (point.y - Grid.origin.y);
    return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
  }
  constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);
let grid2 = new Grid(5.0);

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

3.5.5 抽象类

TypeScript 有抽象类的概念,它是供其它类继承的基类,不能直接被实例化。不同于接口,抽象类必须包含一些抽象方法,同时也可以包含非抽象的成员。abstract 关键字用于定义抽象类和抽象方法。抽象类中的抽象方法不包含具体实现并且必须在派生类中实现,例子如下:

abstract class Person {
  abstract speak(): void; // 必须在派生类中实现
  walking(): void {
    console.log('Walking on the road');
  }
}

class Male extends Person {
  speak(): void {
    console.log('How are you?');
  }
}

let person: Person; // 创建一个抽象类引用
person = new Person(); // 报错:不能创建抽象类实例
person = new Male(); // 创建一个 `Male` 实例
person.speak();
person.walking();

在面向对象设计中,抽象类和接口是经常讨论的话题,TypeScript 中也一样,简单来说,接口更注重功能的设计,抽象类更注重结构内容的体现。

3.6 模块

ES6 引入了模块的概念,在 TypeScript 中也支持模块的使用。

3.6.1 介绍

模块是自声明的,两个模块之间的关系是通过在文件级别上使用 import 和 export 来建立的。TypeScript 与 ES6 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。

模块在其自身的作用域里执行,而不是在全局作用域里,这意味着定义在一个模块里的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们。类似的,如果想使用其它模块导出的变量、函数、类和接口时,必须先通过 import 导入它们。

模块使用模块加载器去导入它的依赖,模块加载器在代码运行时去查找并加载模块间的所有依赖,在 Angular 中,常用的模块加载器有 SystemJS 和 Webpack。

3.6.2 导出

模块可以通过导出的方式来提供变量、函数、类、类型别名和接口给外部模块调用,导出的方式分为如下三种:

导出声明

任何声明都能够通过 export 关键字来导出。

export const COMPANY = "GF"; // 导出变量

export interface IdentityValidate { // 导出接口
  isGfStaff(s: string): boolean;
}

export class ErpIdentityValide implements IdentityValidate { // 导出类
  isGfStaff(erp: string) {
    return erpService.contains(erp); // 判断是否为内部员工
  }
}

导出语句

当我们需要对导出的模块进行重命名时,就用到了导出语句,上面的例子改写如下:

class ErpIdentityValide implements IdentityValidate { // 导出类
  isGfStaff(erp: string) {
    return erpService.contains(erp);
  }
}

export { ErpIdentityValide };
export { ErpIdentityValide as gfIdentityValide };

模块包装

有时候需要修改、扩展已有的模块,并导出给其它模块使用,这时可以使用模块包装来再次导出,如:

// 导出原先的验证器,但做了重命名
export { ErpIdentityValide as RegExpBasedZipCodeValidator } from "./ErpIdentityValide";

有时一个模块可以包裹多个模块,并把新的内容以一个新的模块导出,如:

export * from "./IdentityValidate"; 
export * from "./ErpIdentityValide";  

3.6.3 导入

导入与导出相对应,可以使用 import 来导入当前模块中依赖的外部模块。导入有如下几种方式:

导入一个模块

import { ErpIdentityValide } from "./ErpIdentityValide";

let erpValide = new ErpIdentityValide();

别名导入

import { ErpIdentityValide as ERP } from "./ErpIdentityValide";
let erpValidator = new ERP();

将整个模块导入到一个变量,并通过它来访问模块的导出部分

import * as validator from "./ErpIdentityValide";
let myValidate = new validator.ErpIdentityValide();

3.6.4 默认导出

模块可以用 default 关键字实现默认导出的功能,每个模块可以有一个默认导出。类和函数声明可以直接省略导出名来实现默认导出。默认导出有利于使用方减少调用的层数,省去一些冗余的模块前缀书写,接下来看看几类默认导出的例子:

默认导出类

// erpIdentityValide.ts
export default class ErpIdentityValide implements IdentityValidate {  
  isGfStaff(erp: string) {
    return erpService.contains(erp);
  }
}

// test.ts      
import validator from "./erpIdentityValide";
let erp = new validator();

默认导出函数

// nameServiceValidate.ts
export default function (s: string) {
  return nameService.contains(s);
}

// test.ts
import validate from "./nameServiceValidate";
let name = “zhangsan”;

// 使用导出函数
console.log(`"${name}" ${validate(name) ? " matches" : " does not match"}`);

默认导出值

// constantService.ts
export default "Angular";

// test.ts  
import name from "./constantService";
console.log(name); // "Angular"

3.6.5 模块结构设计

在模块化开发中,共同遵循一些设计原则有利于代码更好地被使用、维护,下面列出几点模块化设计的原则:

尽可能的在顶层导出

顶层导出可以降低调用方使用的难度,过多的 "." 操作使得开发者要记住过多的细节,所以尽量使用默认导出,顶层导出;

明确地列出导入的名字

在导入的时候尽可能明确指定导入的变量,这样只要接口不变,调用方式就可以不变,降低了导入跟导出模块的耦合度,做到面向接口编程;

使用命名空间模式导出,如下所示:

// MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
// Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

使用重新导出进行扩展

我们可能经常需要去扩展一个模块的功能,推荐的方案是不要去改变原来的对象,而是导出一个新的实体来提供新的功能。

3.7 接口

3.7.1 接口概述

接口在面向对象设计中具有极其重要的作用,在 Gof 的 23 种设计模式中,基本上都可见到接口的身影。长期以来,接口模式一直是 JavaScript 这类弱类型语言的软肋,虽然有类似“鸭式辨型”等各种伪实现,并有诸如 《JavaScript设计模式》等书籍的问世,但在使用起来还是略为繁琐。TypeScript接口的使用类似于 Java,甚至还增加了更灵活的接口类型,包括属性、函数、可索引和类类型。

3.7.2 属性类型

在 TypeScript 中,使用 interface 关键字来定义接口。下面通过一个简单示例来了解属性接口,代码如下所示:

interface FullName {
  firstName: string;
  secondName: string;
}

function printLabel(name: FullName) {
  console.log(name.firstName + " " + name.secondName);
}
let myObj = {age: 10, firstName: “Jim”, secondName: “Raynor”};
printLabel(myObj);

上例中接口 FullName 包含两个属性:firstName 和 secondName,且都为 string 类型。这里有两点需要注意:

  1. 传给 printLabel 的对象只要 “形式上” 满足接口的要求即可,例如上例中对象 myObj 必须包含一个 firstName 属性和 secondName 属性,且类型都为 string;
  2. 接口类型检查器不会去检查属性的顺序,但要确保相应的属性存在且类型匹配。

可选属性

可选属性对可能存在的属性进行预定义,并兼容不传值的情况。带有可选属性的接口与普通接口的定义方式差不多,区别是在定义的可选属性名后面加一个 ? 符号。如下例所示:

interface FullName {
  firstName: string;
  secondName?: string;
}

function printLabel(name:FullName) {
  console.log(name.firstName + " " + name.secondName);
}

let myObj = {firstName: ”Jim”, secondName: “Raynor”};
printLabel(myObj);

3.7.3 函数类型

接口除了描述带有属性的普通对象外,也能描述函数类型。定义函数类型接口时,需要明确定义函数的参数列表和返回值类型,且参数列表的每个参数都要有参数名和类型,如下例所示:

interface encrypt {
  (val:string, salt:string):string
}

定义好函数类型接口 encrypt 之后,接下来将通过一个例子来展示如何使用函数类型接口,代码如下所示:

let md5:encrypt;
md5 = function(val:string, salt:string){
  console.log("origin value:" + val);
  let encryptValue = doMd5(val,salt); // doMd5只是个 mock 方法
  console.log("encrypt value:" + encryptValue);
  return encryptValue;
}
let pwd = md5("password","loveGf");

对于函数类型的接口要注意下面两点:

  1. 函数的参数名:使用时参数个数需与接口定义的参数相同,对应位置的数据类型需保持一致,参数名可以不一样;
  2. 函数返回值:函数的返回值类型与接口定义的返回值类型要一致。

3.7.4 可索引类型

可索引类型接口包含一个索引签名和相应索引的返回类型,它表示通过特定的索引来得到指定类型的返回值,可索引类型接口的定义如下所示:

interface UserArray {
  [index: number]: string;
}

let users: UserArray;
users = ["张三", "李四"];

索引签名有两种数据类型,即 string 和 number,索引类型接口使用这两种类型的最终效果是一样的,即当使用 number 类型来索引时,JavaScript 最终也会将它转换成 string 类型后再去索引对象的。

3.7.5 类类型

类类型接口与 C#、Java 里接口类似,用来规范一个类的内容,示例如下所示:

interface Animal {
  name: string;
}

class Dog implements Animal {
  name: string;
  constructor(n: string) { }
}

可以在接口中描述一个方法,并在类里去具体实现它的功能,如同下面的 setName 方法一样:

interface Animal {
  name: string;
  setName(): string;
}

class Dog implements Animal {
  name: string;
  setName(n: string) {
    this.name = n;
  }
  constructor(n: string) { }
}

3.7.6 扩展接口

和类一样,接口也可以实现相互扩展,即能将成员从一个接口复制到另一个里面,这样可以更灵活地将接口拆分到可复用的模块里,示例代码如下:

interface Animal {
  eat(): void;
}

interface Person extends Animal {
  talk(): void;
}

class Programmer {
  coding():void {
    console.log('wow, TypeScript is the best language');
  }
}

class ITGirl extends Programmer implements Person{
  eat(){
    console.log('animal eat');
  }
  talk(){
    console.log('person talk');
  }
  coding():void {
    console.log('I am a girl, but i like coding.');
  }
}

let seky = new ITGirl(); // 通过组合集成类来实现接口扩展,可以更灵活复用模块
seky.coding();

3.8 装饰器

3.8.1 装饰器介绍

装饰器(Decorators) 是一种特殊类型的声明,它可以被附加到类声明、方法、属性、或参数上。 装饰器由@符号紧接一个函数名称,形如 @expression,expression 求值后必须为一个函数,在函数执行的时候装饰器的声明方法会被执行。正如名字所示,装饰器是用来给附着的主体进行装饰,添加额外的行为、校验等。

在 TypeScript 的源码中,可以看到官方提供了如下几种类型的装饰器:

// 方法装饰器
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

// 类装饰器
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

// 参数装饰器
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

// 属性装饰器
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

如上所示,每种装饰器类型传入的参数不大相同,下面将分别介绍。

3.8.2 方法装饰器

方法装饰器是在声明在一个方法之前被声明的(紧贴着方法声明),它会被应用到方法的属性描述符上,可以用来监视、修改或者替换方法定义。方法装饰器的声明如下:

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器表达式会在运行时当作函数被调用,传入下列三个参数:

  1. target:类的原型对象
  2. propertyKey:方法的名字
  3. descriptor:成员属性描述

其中,descriptor 的类型为 TypedPropertyDescriptor,在 TypeScript 中定义如下:

interface TypedPropertyDescriptor<T> {
  enumerable?: boolean; // 是否可遍历
  configurable?: boolean; // 属性描述是否可改变或属性是否可删除
  writable?: boolean; // 是否可修改
  value?: T; // 属性的值
  get?: () => T; // 属性的访问器函数(getter)
  set?: (value: T) => void; // 属性的设置器函数(setter)
}

想了解更多关于 descriptor 可以到 MDN 查看更多 Object.defineProperty() 的介绍

下面是一个方法装饰器的例子:

class TestClass {
  @log
  testMethod(arg: string) { 
    return "logMsg: " + arg;
  }
}

下面是装饰器 @log 的实现:

function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
  let origin = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log("args: " + JSON.stringify(args)); // 调用前
    let result = origin.apply(this, args);// 调用方法
    console.log("The result-" + result); // 调用后
    return result; // 返回结果
  };

  return descriptor;
}

测试代码如下:

new TestClass().testMethod("test method decorator");

输出如下:

args: ["test method decorator"]
The result-logMsg: test method decorator

当装饰器 @log 被调用时,它会打印 log 信息。

3.8.3 类装饰器

类装饰器是在声明一个类之前被声明的,它应用于类构造函数,可以用来监视、修改或替换类定义,在 TypeScript 中定义如下:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

如上所示,类的构造函数作为其唯一的参数。类装饰器在运行时会被当作函数的形式来调用。

假如类装饰器返回了一个值,那么它会使用它提供的构造函数来替换类的声明。下面是使用类装饰器 (@component) 的例子,应用到 Person 类:

@Component({
  selector: 'person',
  template: 'person.html'
})
class Person {
  constructor(
    public firstName:string,
    public secondName:string
  ){}
}

类装饰器Component的定义如下:

function Component(component) {
  return (target:any) => {
    return componentClass(target, component);
  }
}

// componentClass 的实现
function componentClass(target:any, component?:any):any {
  var original = target;
  function construct(constructor, args) { // 处理原型链
   let c:any = function () {
     return constructor.apply(this, args);
   };
   c.prototype = constructor.prototype;
   return new c;
 }

  let f:any = (...args) => { // 打印参数
    console.log("selector:" + component.selector);
    console.log("template:" + component.template);
    console.log(`Person: ${original.name}(${JSON.stringify(args)})`);
    return construct(original, args);
};

f.prototype = original.prototype;
  return f; // 返回构造函数
}

测试代码如下:

let p = new Person("GF", "Security");

结果输出如下:

selector:person
template:person.html
Person: Person(["GF","Security"])

如上,代码看起来有点繁琐,因为我们返回了一个新的构造函数,必须自己处理好原来的原型链。

3.8.4 参数装饰器

参数装饰器是在声明一个参数之前被声明的,它用于类构造函数或方法声明。参数装饰器在运行时会被当作函数的形式来调用,定义如下:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

如上所述,包含 3 个参数:

  1. target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. propertyKey:参数名称。
  3. parameterIndex:参数在函数参数列表中的索引。

下面是参数装饰器的一个简单例子:

class userService {
  login(@inject name: string) {}
}

// @inject 装饰器的实现
function inject(target: any, propertyKey: string | symbol, parameterIndex: number) {
  console.log(target);   // userService prototype
  console.log(propertyKey);  // "login"
  console.log(parameterIndex); // 0
}

输出如下:

Object 
login
0

参数装饰器在 Angular 中广泛被使用,特别是结合 reflect-metadata 库来支持实验性的 metadata API,读者可到官网了解更多相关知识。

3.8.5 属性装饰器

属性装饰器的定义如下:

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

如上所述,包含2个参数:

  1. target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. propertyKey:属性名字

属性装饰器是用来修饰类的属性,声明和被调用方式跟其它装饰器类似,具体内容不展开细讲。

3.8.6 装饰器组合

TypeScript 支持多个装饰器同时应用到一个声明上,实现多个装饰器的复合使用,语法可以从左到右的书写如下例所示:

@decoratorA @decoratorB param

或从上到下的书写:

@decoratorA
@decoratorB
functionA

在 TypeScript 里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 从左到右(从上到下)依次执行装饰器函数,得到返回结果
  2. 返回结果会被当作函数,从左到右(从上到下)依次调用

下面是两个类装饰器复合应用的例子,注意看输出结果所显示的执行顺序,代码如下:

function Component(component) {
  console.log("selector:" + component.selector);
  console.log("template:" + component.template);
  console.log('component init');
  return (target: any) => {
    console.log('component call');
    return target;
  }
}

function Directive(directive) {
  console.log("directive init");
  return (target: any) => {
    console.log('directive call');
    return target;
  }
} 

@Component({
  selector: 'person',
  template: 'person.html'
})
@Directive()
class Person {
}

let p = new Person();

控制台输出如下:

selector:person
template:person.html
component init
directive init:
component call
directive call

Angular 框架在依赖注入,组件等部分中有多个复合装饰器应用的场景,读者在本书后续的学习中可以进一步掌握这部分知识。

装饰器是ES7的草案标准,在 Angular 中,装饰器主要场景之一是使用 元数据(Metadata) 的来定义组件。。因此,读者掌握了装饰器的原理后可以实现类似 Angular 装饰器的语法糖。

3.9 泛型

在实际开发中,我们定义的 API 不仅仅需要考虑功能的健全,还需要考虑到它的复用性,更多的时候需要支持不特定的数据类型,泛型(Generic)就是用来实现这样的效果。

比如我们有个最小堆算法,需要同时支持 number 和 string,可以通过把集合类型改为 any 来实现,但是这样就等于放弃了类型检查,其实我们希望的是返回的类型需要和参数类型一致,接下来通过代码来说明一下:

class MinHeap<T> {
  list: T[] = [];
  add(element: T): void {
    ...
  }

  min(): T {
    return this.list.length ? this.list[0] : null;
  }
}

var heap1 = new MinHeap<number>();
heap1.add(3);
heap1.add(5);
console.log(heap1.min());

var heap2 = new MinHeap<string>();
heap2.add('a');
heap2.add('c');
console.log(heap2.min());

上面的例子中分别声明一个适用于 number 类型和一个适用于 string 类型的最小堆实例,给 MinHeap 类增加了类型变量 T,用于帮助捕获用户输入的数据类型以便于后边的跟踪和使用。

泛型也支持函数,下面实现一个 zip 函数用于把两个数组压缩到一起,其中声明了两个泛型类型 T1T2,具体的代码示例如下:

function zip<T1, T2>(l1: T1[], l2: T2[]): [T1, T2][] {
  var len = Math.min(l1.length, l2.length);
  var ret = [];
  for (let i = 0; i < len; i++) {
    ret.push([l1[i], l2[i]]);
  }
  return ret;
}

console.log(zip<number, string>([1,2,3], ['Jim', 'Sam', 'Tom']));

3.10 TypeScript 周边

3.10.1 tsconfig.json

tsc 编译器有很多命令行参数,都写在命令行上会十分繁琐。tsconfig.json 文件正是用来解决这个问题的,它使得编译参数能在文件中维护。

当运行 tsc 时,编译器从当前目录向上搜索 tsconfig.json 文件来加载配置,类似于 package.json 文件的搜索方式。

我们可以从一个空的 tsconfig.json 文件开始配置。

{}

tsc 有合理的默认设置,具体的配置可到官网上查看详解介绍。下面是一个更为复杂的 Angular 环境用的 tsconfig.json 配置。

{
"compilerOptions": {
   "target": "es5",
   "module": "commonjs",
   "declaration": false,
   "noImplicitAny": false,
   "removeComments": true,
   "noLib": false,
   "emitDecoratorMetadata": true,
   "experimentalDecorators": true,
   "sourceMap": true
},
"exclude": [
   "node_modules",
   "typings/browser.d.ts",
   "typings/browser/**"
],
"compileOnSave": false
}

3.10.2 DefinitelyTyped

因为 TypeScript 是强类型语言,在使用第三方非 TypeScript 开发的库的时候,会需要 .d.ts 外部接口描述文件。

幸运的是很多这样的描述已经被开发了,并开源在 https://github.com/DefinitelyTyped/DefinitelyTyped 上,可以使用 typings 工具对这些描述文件进行管理,类似 npmnuget,输入简单的命令行即可方便地安装 .d.ts 文件。

首先安装 typings 工具:

$ npm install typings --global

然后安装 d.ts 文件:

$ typings install ds-jQuery --save

3.10.3 编码工具

优秀的编码工具(IDE、编辑器)可以提高开发者的编程效率,编码工具基于 TypeScript 类型的特点可以很方便的实现智能提示,查找引用,查找定义等功能,下面介绍几款优秀的编码工具,以飨读者:

3.10.4 展望未来

提到 TypeScript,很容易跟另一个著名 JavaScript 的转译型语言 CoffeeScript 做对比,曾经的 CoffeeScript 也是风靡前端开发,随着 ES6 的崛起,CoffeeScript 已经有点过时,甚至成为那些使用 CoffeeScript 作为开发语言项目的累赘了,各种 CoffeeScript to ES6 的工具已经被频繁使用。

TypeScript 会不会成为下一个 CoffeeScript 呢?这也许是一直萦绕在 TypeScript 开发者心里的一个困扰,事实上,从微软对 TypeScript 的定位就看出来,TypeScript 拥抱 ECMAScript 标准并实现所有规范,这使得无论 JavaScript 的未来如何变化,开发者编写的 TypeScript 语言始终可以运行在 JavaScript 的环境里,不用担心兼容性问题,加上有大厂商微软做背书,可以预见 TypeScript 社区的繁荣。

小结

本章首先介绍了 TypeScript 的背景,接着介绍了 TypeScript 的主要特性,主要包括基本类型、函数、类、接口、装饰器、模块和泛型等内容,在本章的最后介绍了 TypeScript 涉及的其它知识点,包括编译、开发工具、Typings 和未来展望。

通过本章的学习我们基本上掌握了 TypeScript 的相关知识点,具备使用 TypeScript 开发 Angular 应用的能力,接下来,让我们开始快速入门 Angular 吧。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.