mirror of
https://github.com/gnu4cn/ts-learnings.git
synced 2025-01-13 13:50:07 +08:00
Initial push.
This commit is contained in:
parent
3da1a3adc4
commit
721a40c3ca
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
dist/*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
246
01_basic_data_types.md
Normal file
246
01_basic_data_types.md
Normal file
@ -0,0 +1,246 @@
|
||||
# TypeScript基础数据类型
|
||||
|
||||
为令到程序有用,那么就需要能够处理一些最简单的数据单元:数字、字符串、数据结构、布尔值等等。TypeScript支持那些在JavaScript所期望的同样类型,并额外带有一种可将数值与字符串联系起来的枚举类型(For programs to be useful, we need to be able to work with some of the simplest units of data: numbers, strings, structures, boolean values, and the like. In TypeScript, we support much the same types as you would expect in JavaScript, with a convenient enumeration type thrown in to help things along)。
|
||||
|
||||
## 布尔值
|
||||
|
||||
最基本的数据类型就是简单的`true/false`, 在JavaScript与TypeScript中都叫做`boolean`(其它语言也一样)。
|
||||
|
||||
```typescript
|
||||
let isDone: boolean = false;
|
||||
```
|
||||
|
||||
|
||||
## 数字
|
||||
|
||||
与JavaScript一样,TypeScript中的 **所有数字,都是浮点数** ,类型为`number`。TypeScript支持十进制、十六进制字面量(literal),还有ECMAScript 2015中引入的二进制与八进制字面量。
|
||||
|
||||
```typescript
|
||||
let decLiteral: number = 6;
|
||||
let hexLiteral: number = 0xf00d;
|
||||
let binaryLiteral: number = 0b1010;
|
||||
let octalLiteral: number = 0o744;
|
||||
```
|
||||
|
||||
## 字符串
|
||||
|
||||
基于NodeJS的JavaScript服务端框架,或者众多的客户端框架,它们能够可处理客户端或服务器端的文本数据。与其它语言一样,TypeScript使用`string`来表示文本数据类型。与JavaScript一样,可使用`"`(双引号)或`'`(单引号)来将字符串包围起来。
|
||||
|
||||
```typescript
|
||||
let name: string = 'Bob';
|
||||
name = "Smith";
|
||||
```
|
||||
|
||||
此外,TypeScript或ES6中,还可以使用 *模板字符串(template string)* ,它可以 **定义多行文本** 与 **内嵌表达式** 这些字符串是用反引号字符(`\``)括起来,同时内嵌表达式的形式为`${ expr }`。
|
||||
|
||||
```typescript
|
||||
let name: string = `Gene`;
|
||||
let age: number = 37;
|
||||
let sentence: string = `Hello, my name is ${ name }.
|
||||
|
||||
I'll be ${ age+1 } years old next month`;
|
||||
```
|
||||
|
||||
这与下面定义`sentence`的方式,有相同效果:
|
||||
|
||||
```javascript
|
||||
let sentence: string = "Hello, my name is " + name + ".\n\n" +
|
||||
"I'll be " + ( age+1 ) + "years old next month.";
|
||||
```
|
||||
|
||||
## 数组
|
||||
|
||||
和JavaScript一样,TypeScript可以操作数组元素。定义数组的方式有两种,一是可以在类型后面接上`[]`,表示由此类型元素所组成的一个数组:
|
||||
|
||||
```typescript
|
||||
let list: number[] = [1, 2, 3, 4];
|
||||
```
|
||||
|
||||
第二种方式,是使用 **数组泛型(Array Generic)** (通用数组类型, a generic array type) ,`Array<type>`:
|
||||
|
||||
```typescript
|
||||
let list: Array<number> = [1, 2, 3, 4];
|
||||
```
|
||||
|
||||
> 与Python中清单的比较: Python清单中的元素,不要求类型一致,且因此认为Python在数据结构上更具灵活性。Python清单有`pop()`、`append()`等方法,TypeScript要求数组元素类型一致(比如强行将不一致的元素push到数组上,其编译器就会报错),则有`push()`与`pop()`方法。它们都是使用`[]`符号。
|
||||
|
||||
|
||||
## 元组(Tuple)
|
||||
|
||||
TypeScript中的元组,允许表示一个 **已知元素数量与类型** 的数组,这些元素的类型不要求一致。比如,可定义一对值分别为`string`与`number`类型的元组。
|
||||
|
||||
```typescript
|
||||
// 声明一个元组类型
|
||||
let x: [string, number];
|
||||
|
||||
// 对其进行初始化
|
||||
x = ['weight', 181];
|
||||
|
||||
// 错误的初始化
|
||||
x = [181, 'weight'];
|
||||
```
|
||||
|
||||
在访问某个索引已知的元素时,将得到正确的类型:
|
||||
|
||||
```typescript
|
||||
console.log(x[0].substr(1)); // 没有问题
|
||||
console.log(x[1].substr(1)); // 报错,'number' does not have 'substr'
|
||||
```
|
||||
|
||||
在访问元组的越界元素时,将使用 **联合类型** (Union Types,属于高级类型(Advanced Types)的一种)进行替代:
|
||||
|
||||
```typescript
|
||||
x[3] = 'fat'; // 没有问题,字符串可以赋值给(`string` | `number`)类型
|
||||
|
||||
console.log(x[5].toString()); // 没有问题,`string` 与 `number` 都有 toString 方法
|
||||
|
||||
x[6] = true; // 报错,布尔值不是(`string` | `number`)类型 (error TS2322: Type 'true' is not assignable to type 'string | number'.)
|
||||
```
|
||||
|
||||
> 与Python元组的比较:Python元组是不可修改的,访问速度较快。Python元组与Python清单一样可以包含不同类型的元素。Python元组使用`()`符号。
|
||||
|
||||
## 枚举(`enum`)
|
||||
|
||||
对JavaScript标准数据类型集的一个有帮助的补充,`enum`是TypeScript引入的新特性,作为JavaScript标准数据类型的补充。与像C#等其它语言一样,枚举类型是一种可以为某组数值带来更加友好名字的方式。
|
||||
|
||||
```typescript
|
||||
enum Color {Red, Green, Blue};
|
||||
let c: Color = Color.Green;
|
||||
```
|
||||
|
||||
枚举中的元素编号默认从`0`开始。也可手动指定元素编号数值。比如:
|
||||
|
||||
```typescript
|
||||
enum Color {Red=1, Green, Blue};
|
||||
let c: Color = Color.Green;
|
||||
```
|
||||
|
||||
或者全部采用手动的编号:
|
||||
|
||||
```typescript
|
||||
enum Color {Red = 1, Green = 2, Blue = 4};
|
||||
let c: Color = Color.Green;
|
||||
```
|
||||
|
||||
枚举的一个方便特性,在于还可以从数值,获取到枚举中其对应的名字。比如这里有个数值`2`,却不确定它是映射到上面枚举中的何种颜色,那么就可以查找那个相应的名称:
|
||||
|
||||
```typescript
|
||||
enum Color {Red = 1, Green, Blue}
|
||||
let colorName: string = Color[2];
|
||||
|
||||
console.log(colorName); // 将输出`Green`,因为上面的代码中`Green`的值为2
|
||||
```
|
||||
|
||||
> 枚举的深入理解:通过使用枚举特性,可以创建出定制的名称(字符串)-值(整数)映射的类型,随后就可以利用创建出的定制类型,来声明变量,从而加以使用。
|
||||
|
||||
## 任意值 (`any`)
|
||||
|
||||
可能会在编写应用时,为那些尚不知道类型的变量,进行类型描述。这些值可能来自用户、第三方库等动态内容。在这些情况下,就不希望TypeScript的类型检查器,对这些值进行检查,而是让它们直接通过编译阶段的检查。那么,就可以使用`any`类型来标记这些变量:
|
||||
|
||||
```typescript
|
||||
let notSure: any = 4;
|
||||
notSure = 'Maybe a string instead';
|
||||
notSure = false; // 布尔值也没有问题
|
||||
```
|
||||
|
||||
在对既有代码进行改写的时候,`any`类型就十分有用。`any`类型的使用,令到在编译时选择性的通过或跳过类型检查。你可能会认为与其它语言中也一样,`Object`类型也具有同样的作用。但`Object`类型的变量只是允许被赋予任意值,却不能在上面调用任意的方法,即使其真的有着这些方法:
|
||||
|
||||
```typescript
|
||||
let notSure: any = 4;
|
||||
notSure.ifItExists(); // 没有问题,因为在运行时可能存在这个一个`ifItExists`方法
|
||||
notSure.toFixed(); // 没有问题,因为`toFixed`方法确实存在(但编译器是不会加以检查的)
|
||||
|
||||
let prettySure: Object = 4;
|
||||
prettySure.toFixed(); // 报错,类型`Object`上没有`toFixed`属性 (error TS2339: Property 'toFixed' does not exist on type 'Object'.)
|
||||
```
|
||||
|
||||
就算只知道一部分数据的类型,`any`类型也是有用的。比如,有这么一个元组(数组?),其包含了不同类型的数据:
|
||||
|
||||
```typescript
|
||||
let list: any[] = [1, true, "free"];
|
||||
list[1] = 100;
|
||||
```
|
||||
|
||||
## 空值(`void`)
|
||||
|
||||
`void`有点像是`any`的反面,它表示没有任何类型。当某个函数没有返回值时,通常会看到其返回值类型为`void`:
|
||||
|
||||
```
|
||||
function warnUser(): void {
|
||||
alert('This is my warning message!');
|
||||
}
|
||||
```
|
||||
|
||||
仅仅声明一个`void`类型的变量是毫无意义的,因为只能为其赋予`undefined`和`null`值:
|
||||
|
||||
```typescript
|
||||
let unusable: void = undefined;
|
||||
```
|
||||
|
||||
> 那么`void` 类型,也就仅作为函数返回值类型了。
|
||||
|
||||
## `null` 与 `undefined`
|
||||
|
||||
TypeScript中的值`undefined`与`null`都有各自的类型,分别叫`undefined`与`null`。它们与`void`类似,各自用处都不大:
|
||||
|
||||
```typescript
|
||||
let u: undefined = undefined;
|
||||
let n: null = null;
|
||||
```
|
||||
|
||||
默认所有其它类型,都用着子类型`undefined`与`null`。也就是说,可将`null`与`undefined`赋值给`number`、`string`、`list`、`tuple`、`void`等类型。
|
||||
|
||||
但在指定了编译器(tsc, typescript compiler)选项`--strictNullChecks`时,`null`与`undefined`就只能赋值给`void`以及它们自己了。这能避免 **很多** 常见的问题。比如在某处计划传入一个`string`或`null`或`undefined`的参数,那么就可使用`string | null | undefined`的 **联合类型** 。
|
||||
|
||||
> 注意:TypeScript最佳实践是开启`--strictNullChecks`选项,但现阶段假设此选项是关闭的。
|
||||
|
||||
|
||||
## `never`类型
|
||||
|
||||
类型`never`表示一些永不存在的值的类型。比如,可将那些总是会抛出异常,或根本不会有返回值的函数表达式、箭头函数表达式的返回值设置为`never`类型;一些变量也可以是`never`类型,仅当它们受永不为真的 **类型保护** 约束时。
|
||||
|
||||
以下是一些返回`never`类型的函数:
|
||||
|
||||
```typescript
|
||||
// 返回`never`的函数,必须存在无法到达的终点(return?)
|
||||
function error(message: string): never {
|
||||
throw new Error (message);
|
||||
}
|
||||
|
||||
// 推断的返回值类型为never
|
||||
function fail () {
|
||||
return error('Somthing failed')
|
||||
}
|
||||
|
||||
// 返回`never`的函数,必须存在无法到达的终点
|
||||
|
||||
function infiniteLoop (): never {
|
||||
while(true) {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 类型的断言(Type Assertion)
|
||||
|
||||
可能会遇到这样的情况,相比TypeScript(编译器),Coder更有把握了解某个值的类型。也就是说Coder清楚地了解某个实体(entity, 与变量名称所对应的内存单元)有着比它现有类型(`any`/`undefined`/`null`等)更具体的类型。
|
||||
|
||||
那么此时就可以通过 **类型断言** ,告诉编译器“相信我,我知道自己在干什么”,从而对编译进行干预。类型断言相当于其它语言中的类型转换,只是不进行特殊的数据检查与结构(destructure)。其对运行时没有影响,尽在编译阶段起作用。TypeScript会假设Coder已进行了必要的检查。
|
||||
|
||||
```typescript
|
||||
let someValue: any = "This is a string";
|
||||
let strLength: number = (<string>someValue).length;
|
||||
```
|
||||
|
||||
类型断言的另一个`as`的写法:
|
||||
|
||||
```typescript
|
||||
let someValue: any = "This is a string";
|
||||
let strLength: number = (someValue as string).length;
|
||||
```
|
||||
|
||||
这两种形式是等价的。使用何种写法,仅凭个人喜好;但在结合JSX( [jsx.github.io](https://jsx.github.io/) )使用TypeScript时,就只能用`as`的写法。
|
||||
|
||||
## 深入理解`let`
|
||||
|
||||
在上面的示例中,TypeScript的`let`关键字取代了JavaScript中的`var`关键字。JavaScript版本ES6(ECMAScript 2015)带来了新的`let`关键字,TypeScript进行了实现。Javascript原来的很多问题,都可以通过使用`let`加以解决,所以尽可能的使用`let`来代替`var`了。
|
625
02_variables_declaration.md
Normal file
625
02_variables_declaration.md
Normal file
@ -0,0 +1,625 @@
|
||||
# 变量的声明,Variables Declaration
|
||||
|
||||
`let`与`const`是较新一版JavaScript(ES6)中变量声明的方式。前一部分提到,`let`在很多方面与`var`是相似的,但`let`却可以帮助大家解决JavaScript中的一些常见问题。`const`是对`let`的一个增强,可阻止对经其修饰的变量的再次赋值。
|
||||
|
||||
因为TypeScript是JavaScript的超集(super-set),所以自然有对JavaScript所有特性的支持,`let`与`const`关键字也不例外。以下将详细讨论这些全新的声明方式,以及为何要用它们来取代`var`的原因。
|
||||
|
||||
如果你还没有发现JavaScript中使用`var`所带来的问题,那么下面的内容将唤起你的记忆。
|
||||
|
||||
## 关于`var`式变量声明
|
||||
|
||||
JavaScript使用`var`关键字来声明变量,有着悠久的历史:
|
||||
|
||||
```javascript
|
||||
var a = 10;
|
||||
```
|
||||
|
||||
显而易见,这里定义出一个名为`a`的值为`10`的变量(指向某个内存单元地址)。
|
||||
|
||||
在函数内部,也可以进行变量的定义:
|
||||
|
||||
```javascript
|
||||
function f() {
|
||||
var msg = "Hello, World!"
|
||||
|
||||
return msg;
|
||||
}
|
||||
```
|
||||
|
||||
在其它函数内部,也可以访问相同变量:
|
||||
|
||||
```javascript
|
||||
function f() {
|
||||
var a = 10;
|
||||
|
||||
return function g() {
|
||||
var b = a+1;
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
var g = f();
|
||||
g();
|
||||
```
|
||||
|
||||
在上面的例子中,`g` 可以获取到函数`f`里定义的变量`a`。在`g`被调用时,它都可以访问到`f`里的变量`a`。 *即使`g`在`f`已经执行完毕后才被调用,其任可以访问并对`a`进行修改* 。
|
||||
|
||||
```javascript
|
||||
function f () {
|
||||
var a = 1;
|
||||
|
||||
a = 2;
|
||||
|
||||
var b = g();
|
||||
|
||||
a = 3;
|
||||
|
||||
return b;
|
||||
|
||||
function g () {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
f(); // 返回的是 2
|
||||
```
|
||||
|
||||
## 作用域规则(Scope Rules)
|
||||
|
||||
加入对其它严格的编程语言比较熟悉,那么对于JavaScript中`var`声明的作用域规则,将感到奇怪。比如:
|
||||
|
||||
```typescript
|
||||
function f(shouldInitialize: boolean) {
|
||||
if ( shouldInitialize ) {
|
||||
var x = 10;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
f(true); // 返回的是 `10`
|
||||
f(false); // 返回 `undefined`
|
||||
```
|
||||
|
||||
多看几遍这段代码就会发现,这里的变量`x`是定义在`if`语句里的,但却可以在该语句外面访问到它。究其原因,在于`var`声明可以在包含它的函数、模块、命名空间或全局作用域内的任何位置被访问到(后面将详细讨论这个问题),而所包含其的代码块却没什么影响。有人就直接叫这种作用域为 **var作用域** ,或 **函数作用域** 。对于函数参数,也适用函数作用域(函数参数也相当于`var`声明)。
|
||||
|
||||
此规则所涵盖到的作用域,将引发一些错误。比如多次声明同一个变量不会报错,就是其中之一:
|
||||
|
||||
```typescript
|
||||
function sumMatrix (matrix: number[][]) {
|
||||
var sum = 0;
|
||||
|
||||
for (var i = 0; i < matrix.length; i++){
|
||||
var currentRow = matrix[i];
|
||||
|
||||
for (var i = 0; i < currentRow.length; i++){
|
||||
sum += currentRow[i];
|
||||
}
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
```
|
||||
|
||||
显然,内层的`for`循环会覆盖变量`i`,因为所有`i`都引用相同的函数作用域内的变量。稍微有经验的Coder都知道,这些问题可能在代码审查时遗漏,从而引发麻烦。
|
||||
|
||||
## 捕获变量怪异之处
|
||||
|
||||
看看下面的代码,将有什么样的输出:
|
||||
|
||||
```javascript
|
||||
for (var i = 0; i < 10; i++){
|
||||
setTimeout(function (){
|
||||
console.log(i);
|
||||
}, 100 * i);
|
||||
}
|
||||
```
|
||||
|
||||
对于那些尚不熟悉的Coder,要知道这里的`setTimeout`会在若干毫秒的延时后尝试执行一个函数(因此要等待其它所有代码执行停止)。
|
||||
|
||||
结果就是:
|
||||
|
||||
```sh
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
10
|
||||
```
|
||||
|
||||
很多有经验的JavaScript程序员对此已经很熟悉了,但如被输出吓到了,也不是你一个人。大多数人都期望得到这样的结果:
|
||||
|
||||
```bash
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
```
|
||||
|
||||
参考上面提到的捕获变量(Capturing Variables),传递给`setTimeout`的每一个函数表达式,实际上都引用了相同作用域中的同一个`i`。
|
||||
|
||||
这里有必要花个一分钟来思考一下那意味着什么。`setTimeout`将在若干毫秒后运行一个函数, *但只是* 在`for`循环已停止执行后。随着`for`循环的停止执行,`i`的值就成为`10`。这就是作为`setTimeout`的第一个参数的函数在调用时,每次都输出`10`的原因。
|
||||
|
||||
作为解决此问题的一种方法,就是使用立即执行的函数表达式(Immediately Invoked Function Expression, IIFE, [参考链接](https://segmentfault.com/a/1190000003985390))。
|
||||
|
||||
```javascript
|
||||
for (var i = 0; i < 10; i++){
|
||||
// 这里要捕获到变量`i`的当前状态
|
||||
// 是通过触发带有其当前值的一个函数实现的
|
||||
(function(i){
|
||||
setTimeout(function (){
|
||||
console.log(i);
|
||||
}, 100 * i)
|
||||
})(i);
|
||||
}
|
||||
```
|
||||
|
||||
其实对于这种奇怪的形式,我们都已司空见惯了。立即执行函数中的参数`i`,会覆盖`for`循环中的`i`,但因为使用相同的名称`i`,所以都不用怎么修改`for`循环体内部的代码。
|
||||
|
||||
## 关于全新的`let`声明方式
|
||||
|
||||
现在已经知道使用`var`存在诸多问题,这也是要使用`let`的理由。除了拼写不一样外,`let`与`var`的写法一致。
|
||||
|
||||
```typescript
|
||||
let hello = 'Hello!';
|
||||
```
|
||||
|
||||
二者主要的区别,不在于语法上,而是语义的不同,下面会深入研究。
|
||||
|
||||
### 块作用域(Block Scoping)
|
||||
|
||||
在使用`let`来声明某个变量时,使用了 *词法作用域(Lexical Scope)* ,或 *块作用域(Block Scope)* 。与使用`var`声明的变量可在所包含的函数外部访问到不同,块作用域的变量在包含它们的块或`for`循环之外,是不能访问的。
|
||||
|
||||
```typescript
|
||||
function f (input: boolean) {
|
||||
let a = 100;
|
||||
|
||||
if (input) {
|
||||
// 这里仍然可以对`a`进行引用
|
||||
let b = a + 1;
|
||||
return b;
|
||||
}
|
||||
|
||||
// 这样写就会报错:`b` 在这里不存在(error TS2304: Cannot find name 'b'.)
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码中定义了两个变量`a`和`b`。`a`的作用域是函数体`f`内部。而`b`的作用域为`if`语句块里。
|
||||
|
||||
在`catch`语句里声明的变量也具有同样的作用域规则。比如:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
throw "oh no!"
|
||||
}
|
||||
|
||||
catch (e) {
|
||||
console.log("Oh well.")
|
||||
}
|
||||
|
||||
// 下面这样会报错:这里不存在`e`
|
||||
console.log(e);
|
||||
```
|
||||
|
||||
块级作用域变量的另一个特点,就是在其被声明之前,是不能访问的(尚未分配内存?)。虽然它们始终“存在”与它们所属的作用域里,但在声明它们的代码之前的部分,被成为 *暂时性死区(Temporal Dead Zone, TDZ)* ([参考链接](https://github.com/luqin/exploring-es6-cn/blob/master/md/9.4.md))。暂时性死区只是用来说明不能在变量的`let`语句之前,访问该变量,而TypeScript编译器可以给出这些信息。
|
||||
|
||||
```typescript
|
||||
a++; // error TS2448: Block-scoped variable 'a' used before its declaration. error TS2532: Object is possibly 'undefined'.
|
||||
let a;
|
||||
```
|
||||
|
||||
这里需要注意一点,在一个拥有块作用域的变量被声明之前,仍然可以 *获取(capture)* 到它。但要在变量被声明前就去调用那个其所属的函数,是不可行的。如编译目标代码是ECMAScript 2015(ES6),那么较新的运行时将抛出一个错误;不过目前的TypeScript编译器尚不能就此进行报错。
|
||||
|
||||
```typescript
|
||||
function foo () {
|
||||
// 这里要获取到`a`没有问题(okay to capture `a`)
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
// 但不能在`a`被声明前调用函数`foo`
|
||||
// 运行时(runtime)应该抛出错误
|
||||
|
||||
foo();
|
||||
|
||||
let a;
|
||||
```
|
||||
|
||||
关于 *暂时性死区* 的更多信息,请参考[Mozilla开发者网络](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#Temporal_dead_zone_and_errors_with_let)。
|
||||
|
||||
|
||||
## 重定义与屏蔽(Re-decalration and Shadowing)
|
||||
|
||||
在使用`var`进行变量声明时,注意到对变量进行多少次声明都没有关系;得到的变量仅有一个。
|
||||
|
||||
```javascript
|
||||
function f (x) {
|
||||
var x;
|
||||
var x;
|
||||
|
||||
if (true) {
|
||||
var x;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在上面的示例中,对`x`的所有声明,都是对同一个`x`的引用,且这样做也毫无问题。但这种做法通常将导致很多bug。`let`式的声明,终于不会这样任性了。
|
||||
|
||||
```typescript
|
||||
let x = 10;
|
||||
let x = 20; // error TS2451: Cannot redeclare block-scoped variable 'x'.
|
||||
```
|
||||
|
||||
并不是要重复声明的变量,都是块作用域,TypeScript编译器才会给出存在问题的信息。
|
||||
|
||||
```typescript
|
||||
function f (x) {
|
||||
let x = 100; // error TS2300: Duplicate identifier 'x'.
|
||||
}
|
||||
|
||||
function g () {
|
||||
let x = 100;
|
||||
var x = 100; // error TS2451: Cannot redeclare block-scoped variable 'x'.
|
||||
}
|
||||
```
|
||||
|
||||
这并非是说块作用域的变量决不能以某个函数作用域变量加以声明。而是说块作用域变量,只需要在某个明显不同的块中,加以声明。
|
||||
|
||||
```typescript
|
||||
function f(condition, x) {
|
||||
if (condition) {
|
||||
let x = 100;
|
||||
return x;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
f(false, 0); // 返回 `0`
|
||||
f(true, 0); // 返回 `100`
|
||||
```
|
||||
|
||||
这种在某个更深的嵌套块中引入新变量名的做法,就叫 *屏蔽(shadowing)* 。这样做看起来像是双刃剑,因为无意的屏蔽可能引入某些程序漏洞,同时也可能防止某些漏洞。比如,设想用现在的`let`变量来重写之前的`sumMatrix`。
|
||||
|
||||
```typescript
|
||||
function sumMatrix(matrix: number[][]) {
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < matrix.length; i++){
|
||||
var currentRow = matrix[i];
|
||||
|
||||
for (let i = 0; i < currentRow.length; i++){
|
||||
sum += currentRow[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
此版本的循环无疑将正确进行求和了,因为内层循环的`i`屏蔽了外层循环的`i`。
|
||||
|
||||
通常为了顾及编写出清爽的代码,应避免使用屏蔽(shadowing)。但在某些情况下使用屏蔽又能带来好处,因此用不用此特性就取决于你的判断了。
|
||||
|
||||
### 捕获块作用域变量(Block-scoped Variable Capturing)
|
||||
|
||||
前面在`var`式声明上,初次接触到 **变量捕获(variable capturing)** 这一概念,主要对所捕获到的变量的行为,有所了解。为了对此有更直观的认识,那么就说在某个作用域运行时,该作用域就创建出一个变量的“环境”。此环境及其所捕获到的变量,就算其作用域中的所有语句执行完毕,也仍将持续存在。
|
||||
|
||||
```typescript
|
||||
function theCityThatAlwaysSleeps () {
|
||||
let getCity;
|
||||
|
||||
if (true) {
|
||||
let city = "Seattle";
|
||||
|
||||
getCity = function () {
|
||||
return city;
|
||||
}
|
||||
}
|
||||
|
||||
return getCity;
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码中,因为在`city`所在的环境中对其进行了捕获,所以尽管`if`块完成了执行,却仍可以访问到它。
|
||||
|
||||
回顾之前的`setTimeout`示例,那里为了捕获`for`循环的每次迭代下某个变量的状态,而最终使用了一个IIFE。实际上为了所捕获的变量,而是建立了一个新的变量环境。那样做有点痛苦,但幸运的是,在TypeScript中再也无须那样做了。
|
||||
|
||||
在作为某个循环一部分使用`let`进行变量声明时,这些`let`声明有着显著不同的行为。与仅仅将一个新的环境引入到该循环相比,这些声明在某种程度上于每次遍历,都创建出一个新的作用域。因此这就跟使用IIFE有着异曲同工的效果,那么就可以仅使用`let`来改写旧版的`setTimeout`示例了。
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(function () { console.log(i); }, 100 * i);
|
||||
}
|
||||
```
|
||||
|
||||
将如预期的那样,输出以下结果:
|
||||
|
||||
```bash
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
```
|
||||
|
||||
## 关于`const`式的声明
|
||||
|
||||
`const`式声明是声明变量的另一种方式。
|
||||
|
||||
```typescript
|
||||
const numLivesForCat = 9;
|
||||
```
|
||||
|
||||
此类声明与`let`声明相似,但如同它们的名称一样,经由`const`修饰的变量的值,一旦被绑定,就不能加以改变了。也就是说,这些变量与`let`式声明有着相同的作用域,但不能对其进行再度赋值。
|
||||
|
||||
注意不要与所谓某些所引用的值 *不可修改(immutable)* 之概念搞混(经`const`修饰变量与那些不可修改值并不是一个东西)。
|
||||
|
||||
```typescript
|
||||
const numLivesForCat = 9;
|
||||
|
||||
const kitty = {
|
||||
name: "Aurora",
|
||||
numLives: numLivesForCat
|
||||
}
|
||||
|
||||
// 下面的代码将报错
|
||||
|
||||
kitty = {
|
||||
name: "Danielle",
|
||||
numLives: numLivesForCat
|
||||
};
|
||||
|
||||
// 但这些代码都没有问题
|
||||
kitty.name = "Rory";
|
||||
kitty.name = "Kitty";
|
||||
kitty.name = "Cat";
|
||||
kitty.numLives--;
|
||||
```
|
||||
|
||||
上面的示例表明,除非采取了特别措施加以避免,某个`const`变量的内部状态仍然是可改变的。不过恰好TypeScript提供了将对象成员指定为`readonly`的方法。[接口]()那一章对此进行了讨论。
|
||||
|
||||
|
||||
## `let`与`const`的比较
|
||||
|
||||
现在有了两种在作用域语义上类似的变量声明方式,那自然就要发出到底要使用哪种方式的疑问。与那些最为宽泛的问题一样,答案就是看具体情况。
|
||||
|
||||
适用[最小权限原则](https://en.wikipedia.org/wiki/Principle_of_least_privilege),除开那些将进行修改的变量,所有变量都应使用`const`加以声明。这么做的理论基础就是,在某个变量无需写入时,在同一代码基础上工作的其他人就不应自动地被赋予对该对象写的权力,同时将需要考虑他们是否真的需要对该变量进行重新赋值。使用`const`还可以在对数据流进行推演时,令到代码更可预测。
|
||||
|
||||
总之需要三思而后行,同时在可行的情况下,应就此与团队的其它人共商此事。
|
||||
|
||||
本手册主要使用`let`声明。
|
||||
|
||||
|
||||
## 解构(Destructuring)及新语法`...`
|
||||
|
||||
TypeScript从ECMAScript 2015(ES6)那里借鉴的另一特性,就是 **解构** 。可从[Mozilla开发者网络](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)对结构这一全新特性做完整了解。此小节将做简短的概览。
|
||||
|
||||
### 数组的解构
|
||||
|
||||
解构的最简单形式,就是数组结构式赋值(array destructuring assignment)了:
|
||||
|
||||
```typescript
|
||||
let input: number[] = [1, 2];
|
||||
let [first, second] = input;
|
||||
|
||||
console.log(first); // 输出 1
|
||||
console.log(second); // 输出 2
|
||||
```
|
||||
|
||||
上面的代码,创建了两个分别名为`first`及`second`的变量。这与使用索引效果一样,却更为方便:
|
||||
|
||||
```typescript
|
||||
let [first, second];
|
||||
first = input[0];
|
||||
second = input[1];
|
||||
```
|
||||
|
||||
对于那些已经声明的变量,解构也工作:
|
||||
|
||||
```typescript
|
||||
// 对变量的交换操作
|
||||
[first, second] = [second, first];
|
||||
```
|
||||
|
||||
以及对某个函数参数的解构:
|
||||
|
||||
```typescript
|
||||
function f ( [first, second]: [number, number] ) {
|
||||
console.log(first);
|
||||
console.log(second);
|
||||
}
|
||||
|
||||
f([1, 2]);
|
||||
```
|
||||
|
||||
使用 **语法`...`** ,可为某个清单(list, 也就是数组)中剩下的条目创建一个变量:
|
||||
|
||||
```typescript
|
||||
let [first, ...remain] = [1, 2, 3, 4];
|
||||
|
||||
console.log(first);
|
||||
console.log(remain);
|
||||
```
|
||||
|
||||
因为这是JavaScript, 所以当然可以将那些不在乎的后续元素,简单地忽视掉:
|
||||
|
||||
```typescript
|
||||
let [first] = [1, 2, 3, 4];
|
||||
console.log(first); // 输出 1
|
||||
```
|
||||
|
||||
或仅结构其它元素:
|
||||
|
||||
```typescript
|
||||
let [, second, , fourth] = [1, 2, 3, 4];
|
||||
```
|
||||
|
||||
### 对象的解构(Object destructuring)
|
||||
|
||||
还可以解构对象:
|
||||
|
||||
```typescript
|
||||
let o = {
|
||||
a: "foo",
|
||||
b: 12,
|
||||
c: "bar"
|
||||
};
|
||||
|
||||
let {a, b} = 0;
|
||||
```
|
||||
|
||||
这段代码将从`o.a`与`o.b`创建出两个新变量`a`与`b`。请注意在不需要`c`时可跳过它。
|
||||
|
||||
与数组解构一样,可不加声明地进行赋值:
|
||||
|
||||
```typescript
|
||||
({a, b} = {a: "baz", b: 101});
|
||||
```
|
||||
|
||||
请注意这里必须将该语句用括号(`()`)括起来。因为 **JavaScript会将`{`解析为代码块的开始** 。
|
||||
|
||||
使用`...`语法,可为某个对象中的剩余条目,创建一个变量:
|
||||
|
||||
```typescript
|
||||
let {a, ...passthrough} = o;
|
||||
let total = passthrough.length + passthrough.c.length;
|
||||
```
|
||||
|
||||
### 属性的重命名(新语法)
|
||||
|
||||
给属性赋予不同的名称,也是可以的:
|
||||
|
||||
```typescript
|
||||
let {a: newName1, b: newName2} = o;
|
||||
```
|
||||
|
||||
从这里开始,此新语法就有点令人迷惑了。建议将`a: newName1`读作`a`作为`newName1`("`a` as `newName1`")。其方向是左到右(left-to-right)的, 就如同以前写的:
|
||||
|
||||
```typescript
|
||||
let newName1 = o.a;
|
||||
let newName2 = o.b;
|
||||
```
|
||||
|
||||
此外,这里的冒号(`:`)也不是指的类型。如果要指定类型,仍然需要写道整个解构的后面:
|
||||
|
||||
```typescript
|
||||
let {a, b} : {a: string, b: number} = o;
|
||||
```
|
||||
|
||||
### 对象解构的默认值(Default values, 新语法)
|
||||
|
||||
默认值令到在某属性未被定义时,为其指派一个默认值成为可能:
|
||||
|
||||
```typescript
|
||||
function keepWholeObject ( wholeObject: {a: string, b?: number} ) {
|
||||
let {a, b = 1001} = wholeObject;
|
||||
|
||||
// do some stuff
|
||||
}
|
||||
```
|
||||
|
||||
就算`b`未被定义,上面的`keepWholeObject`函数也会有着一个`wholeObject`变量,以及属性`a`与`b`。
|
||||
|
||||
### 对象解构下的函数声明(Function declarations)
|
||||
|
||||
在函数声明中,解构也可运作。在简单场合,这是很明了的:
|
||||
|
||||
```typescript
|
||||
type C = { a: string, b?: number };
|
||||
|
||||
function f( {a, b}: C ) void {
|
||||
// do some stuffs
|
||||
}
|
||||
```
|
||||
|
||||
给参数指定默认值,是更为通常的做法,而通过解构来获取默认值,却可能是难以掌握的。首先需要记住在默认值前加上模式(`C`?):
|
||||
|
||||
```typescript
|
||||
function f ({a, b} = {a: "", b: 0}): avoid {
|
||||
// do some stuffs
|
||||
}
|
||||
|
||||
f(); // 编译通过, 默认值为: {a: "", b: 0}
|
||||
```
|
||||
|
||||
> 上面这段代码是类型推理(type inference)的一个示例,本手册后面后讲到。
|
||||
|
||||
此时,就要记住是要在被解构的属性上,而不是主初始化器上,给可选属性赋予一个默认值(Then, you need to remember to give a default for optional properties on the destructured property instead of the main initializer)。记住`C`的定义带有可选的`b`:
|
||||
|
||||
```typescript
|
||||
function f ({ a, b = 0 } = { a: "" }): void {
|
||||
//...
|
||||
}
|
||||
|
||||
f ({a: "yes"}); // 可通过编译,默认 b = 0
|
||||
f (); // 可通过编译,默认 {a: ""}, 此时默认这里b = 0
|
||||
f({}); // 报错,在提供了一个参数时,就需要提供`a`
|
||||
```
|
||||
|
||||
请小心谨慎地使用解构。如前面的示例所演示的那样,就算是最简单的解构表达式也不是那么容易理解。而在有着较深的嵌套解构时,即便不带有重命名、默认值及类型注释等操作,也难于掌握,那么就尤其容易搞混了。请尽量保持解构表达式在较小及简单的状态。可一致只写那些可以自己生成的赋值解构。
|
||||
|
||||
## 扩展(Spread, 新语法)
|
||||
|
||||
扩展操作符(The spread operator)与解构相反。经由扩展运算符,就可以将一个数组,展开到另一个中去,或者将一个对象展开到另一对象中去。比如:
|
||||
|
||||
```typescript
|
||||
let first = [1, 2],
|
||||
second = [3, 4];
|
||||
|
||||
let bothPlus = [0, ...first, ...second, 5];
|
||||
```
|
||||
|
||||
这段代码赋予`bothPlus`值`[0, 1, 2, 3, 4, 5]`。展开(spreading)创建出`first`与`second`变量的影子拷贝(a shadow copy)。而两个变量则并不会被展开操作所改变。
|
||||
|
||||
对于对象,也可以对其展开:
|
||||
|
||||
```typescript
|
||||
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
|
||||
|
||||
let search = { ...defaults, food: "rich" };
|
||||
```
|
||||
|
||||
现在`search`就成了`{ food: "rich", price: "$$", ambiance: "noisy" }`。比起数组展开,对象展开 **要复杂一些** 。与数组展开一样,对象展开将从左到右进行处理(proceeds from left-to-right),但结果仍是一个对象。这就是说在展开对象中后来的属性,将覆盖先来的属性。所以加入将上面的示例修改为在末尾才进行展开:
|
||||
|
||||
```
|
||||
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
|
||||
|
||||
let search = { food: "rich", ...defaults };
|
||||
```
|
||||
|
||||
此时`defaults`中的`food`属性就将覆盖`food: "rich"`,然而这并不是我们想要的。
|
||||
|
||||
对象的展开还有其它一些令人惊讶的限制。首先,它仅包含某对象[自己的、可枚举属性(own, enumerable properties)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties)。简单地说,这就意味着在展开某对象实例时,将丢失它的那些方法(Basically, that means you lose methods when you spread instances of an object):
|
||||
|
||||
```typescript
|
||||
class C {
|
||||
p = 12;
|
||||
m () {
|
||||
}
|
||||
}
|
||||
|
||||
let c = new C();
|
||||
let clone = { ...c };
|
||||
|
||||
clone.p; // 没有问题
|
||||
clone.m(); // 报错!error TS2339: Property 'm' does not exist on type '{ p: number; }'.
|
||||
```
|
||||
|
||||
此外,TypeScript编译器不支持一般函数的类型参数(the TypeScript compiler doesn't allow spreads of type parameters from generic functions)。此特性有望在该语言的后期发布中受到支持。
|
478
03_classes.md
Normal file
478
03_classes.md
Normal file
@ -0,0 +1,478 @@
|
||||
## 类(Classes)
|
||||
|
||||
## 简介
|
||||
|
||||
传统的JavaScript使用函数与基于原型的继承(prototype-based inheritance),来建立可重用的组件。但这种处理会令到那些习惯于面向对象方法的程序员不自在,面向对象方法有着功能继承、对象建立自类等特性。从ECMAScript 2015, 也就是ES6开始,JavaScript程序员就可以使用面向对象的、基于类的方法,来构建他们的应用了。在TypeScript中,现在就可以用上这些技术,并将其向下编译到可工作于所有主流浏览器与平台的JavaScript,而无需等待下一版的JavaScript。
|
||||
|
||||
## 关于类
|
||||
|
||||
让我们来看一个简单的基于类的实例吧:
|
||||
|
||||
```typescript
|
||||
class Greeter {
|
||||
greeting: string;
|
||||
|
||||
constructor ( message: string ) {
|
||||
this.greeting = message;
|
||||
}
|
||||
|
||||
greet () {
|
||||
return "Hello, " + this.greeting;
|
||||
}
|
||||
}
|
||||
|
||||
let greeter = new Greeter ("world");
|
||||
```
|
||||
|
||||
如你之前曾使用过C#或Java, 那么就应该对这段代码的语法比较熟悉了。这里声明了一个新的类`Greeter`(declare a new class `Greeter`)。此类有三个成员:一个名为`greeting`的属性,一个构建器,以及一个方法`greet`。
|
||||
|
||||
在类中,将注意到当对该类的某个成员进行引用时,在该成员前加上了`this.`。这就表名那是一个成员访问(a member access)。
|
||||
|
||||
上面代码的最后一行使用`new`关键字构建出该`Greeter`类的一个实例(construct an instance of the `Greeter` class by using `new`)。这调用了先前所定义的构建函数(constructor, 构建器),从而以该`Greeter`为外形(shape),进行新对象的创建,并运行该构造函数对其进行初始化。
|
||||
|
||||
## 继承(Inheritance)
|
||||
|
||||
在TypeScript中可使用通常的面向对象模式(common object-oriented patterns)。而基于类编程的最为基础模式之一,就是具备运用继承,对既有类加以扩展,从而创建出新类的能力了。
|
||||
|
||||
看看这个示例:
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
move ( distanceInMeters: number = 0 ) {
|
||||
console.log(`Animal moved ${distanceInMeters}m.`);
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
bark () {
|
||||
console.log ('Woof! Woof!');
|
||||
}
|
||||
}
|
||||
|
||||
const dog = new Dog ();
|
||||
|
||||
dog.bark();
|
||||
dog.move(10);
|
||||
dog.bark();
|
||||
```
|
||||
|
||||
此实例给出了最基本的继承特性:类自基类继承属性及方法(classes inherit properties and methods from base classes)。这里的`Dog`类是一个使用`extends`关键字,派生自`Animal`这个 *基类(base class)* 的 *派生(derived)* 类。派生类(derived classes)通常被称作 *子类(subclass)* ,同时基类又通常被叫做 *超类(superclass)* 。
|
||||
|
||||
因为`Dog`扩展了来自`Animal`的功能,所以这里就能创建一个可同时`bark()`及`move()`的`Dog`的实例。
|
||||
|
||||
再来看一个更复杂的示例:
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
name: string;
|
||||
|
||||
constructor (theName: string) { this.name = theName; }
|
||||
|
||||
move ( distanceInMeters: number = 0 ) {
|
||||
console.log(`${this.name} moved ${distanceInMeters}m.`);
|
||||
}
|
||||
}
|
||||
|
||||
class Snake extends Animal {
|
||||
constructor (name: string) { super(name); }
|
||||
|
||||
move ( distanceInMeters = 5 ) {
|
||||
console.log( "Slithering..." );
|
||||
super.move(distanceInMeters);
|
||||
}
|
||||
}
|
||||
|
||||
class Horse extends Animal {
|
||||
constructor (name: string) { super(name); }
|
||||
|
||||
move (distanceInMeters = 45) {
|
||||
console.log("Galloping...");
|
||||
super.move(distanceInMeters);
|
||||
}
|
||||
}
|
||||
|
||||
let sam = new Snake("Sammy the Python");
|
||||
let tom: Animal = new Horse("Tommy the Palomino");
|
||||
|
||||
sam.move();
|
||||
tom.move(34);
|
||||
```
|
||||
|
||||
这个示例涵盖了一些前面没有提到的其它特性。再度看到使用了`extends`关键字建立了`Animal`的两个新子类:`Horse`与`Snake`。
|
||||
|
||||
与前一示例的一点不同,就是每个含有构建器的派生类,都 **必须** 调用`super()`这个方法,以执行到基类的构造函数,否则编译器将报错(`error TS2377: Constructors for derived classes must contain a 'super' call.`, 及`error TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class`)。此外,在构造函数体中,于访问`this`上的某个属性之前, **必须** 先调用`super()`方法。TypeScript编译器将强制执行此一规则。
|
||||
|
||||
该示例还展示了怎样以特定于子类的方法,覆写基类中方法。这里的`Snake`与`Horse`都创建了一个覆写`Animal`中的`move()`方法的`move()`方法,从而赋予其针对不同类的特定功能。请注意就算`tom`是作为一个`Animal`加以声明的,其值还是一个`Horse`, 对`tom.move(34)`的调用,将调用到`Horse`中所覆写的方法:
|
||||
|
||||
```bash
|
||||
Slithering...
|
||||
Sammy the Python moved 5m.
|
||||
Galloping...
|
||||
Tommy the Palomino moved 34m.
|
||||
```
|
||||
|
||||
## 公共属性、私有属性与受保护的修改器(Public, Private and protected modifiers)
|
||||
|
||||
### 属性默认是公共的(Public by default)
|
||||
|
||||
在上面这些示例中,可在整个程序中自由地访问到所声明的那些成员。如你熟悉其它语言中的类,那么就可能已经注意到上面的示例中,不必使用`public`关键字来达到此目的;比如,C#就要求显式地给成员打上`public`标签,以令到其对外部可见。而在TypeScript中,默认各成员都是公共的。
|
||||
|
||||
当然也可以将某个成员显式地标记为`public`。可以下面的形式编写上一小节中的`Animal`类:
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
public name: string;
|
||||
|
||||
public constructor ( theName: string ) { this.name = theName; }
|
||||
|
||||
public move ( distanceInMeters: number ) {
|
||||
console.log(`${this.name} moved ${distanceInMeters}m.`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 掌握`private`
|
||||
|
||||
当某个成员被标记为`private`时,其就不能从包含它的类的外部访问到了。比如:
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
private name: string;
|
||||
|
||||
constructor ( theName: string ) { this.name = theName; }
|
||||
}
|
||||
|
||||
new Animal("Cat").name(); // 报错:`name` 是私有的, error TS2341: Property 'name' is private and only accessible within class 'Creature'.
|
||||
```
|
||||
|
||||
TypeScript是一个结构化的类型系统。在比较两个不同的类型时,无论它们来自何处,自要所有成员是相容的,那么就说两个类型本身也是相容的(TypeScript is a structural type system. When we compare two different types, regardless of where they come from, if the types of all members are compatible, then we say the types themselves are compatible)。
|
||||
|
||||
但在比较两个有着`private`及`protected`成员的类型时,将加以不同的对待。对于两个被认为是相容的类型,如其中之一有一个`private`成员,那么另一个就必须要有一个源自同样声明的`private`成员。同样的规则也适用于那些`protected`成员(For two types to be considered compatible, if one of them has a `private` member, then the other must have a `private` member that originated in the same declaration. The same applies to `protected` members)。
|
||||
|
||||
为搞清楚这一规则在实践中如何发挥作用,让我们看看下面的示例:
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
private name: string;
|
||||
|
||||
constructor ( theName: string ) { this.name = theName; }
|
||||
}
|
||||
|
||||
Class Rhino extends Animal {
|
||||
constructor () { super ('Rhino'); }
|
||||
}
|
||||
|
||||
Class Employee {
|
||||
private name: string;
|
||||
|
||||
constructor ( theName: string ) { this.name = theName; }
|
||||
}
|
||||
|
||||
let animal = new Animal ("Goat");
|
||||
let rhino = new Rhino();
|
||||
let employee = new Employee('Bob');
|
||||
|
||||
animal = rhino;
|
||||
animal = employee; // 报错: `Animal` 与 `Employee` 并不相容, error TS2322: Type 'Employee' is not assignable to type 'Creature'. Types have separate declarations of a private property 'name'.
|
||||
```
|
||||
|
||||
此示例有着一个`Animal`与`Rhino`, 其中`Rhino`是`Animal`的一个子类。同时还有一个新的`Employee`类,它在形状上看起来与`Animal`一致。示例中又创建了几个这些类的实例,并尝试进行相互之间的赋值,以看看会发生什么。因为`Animal`与`Rhino`共享了来自`Animal`中的同一声明`private name: string`的它们形状的`private`侧,因此它们是相容的(Because `Animal` and `Rhino` share the `private` side of their shape from the same declaration of `private name: string` in `Animal`, they are compatible)。但对于`Employee`却不是这样了。在尝试将一个`Employee`赋值给`Animal`时,就得到一个这些类型不相容的错误。就算`Employee`也有着一个名为`name`的`private`成员,但该成员也并不是那个在`Animal`中所声明的。
|
||||
|
||||
### 掌握`protected`
|
||||
|
||||
除了经由`protected`关键字声明的成员仍可以被派生类的实例所访问外,`protected`修改器(the `protected` modifier)与`private`修改器有着相似的行为。比如:
|
||||
|
||||
```typescript
|
||||
class Person {
|
||||
protected name: string;
|
||||
|
||||
constructor ( name: string ) { this.name = name; }
|
||||
}
|
||||
|
||||
class Employee extends Person {
|
||||
private department: string;
|
||||
|
||||
constructor ( name: string, department: string ) {
|
||||
super(name);
|
||||
this.department = department;
|
||||
}
|
||||
|
||||
public getElevatorPitch () {
|
||||
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
|
||||
}
|
||||
}
|
||||
|
||||
let howard = new Employee ("Howard", "Sales");
|
||||
console.log(howard.getElevatorPitch());
|
||||
console.log(howard.name); // 报错: error TS2445: Property 'name' is protected and only accessible within class 'Person' and its subclasses.
|
||||
```
|
||||
|
||||
## 关于只读修改器(Readonly modifier)
|
||||
|
||||
使用`readonly`关键字,可令到属性只读。只读的属性 **必须在其声明处或构造函数里进行初始化** 。
|
||||
|
||||
```typescript
|
||||
class Octopus {
|
||||
readonly name: string;
|
||||
readonly numberOfLegs = 8;
|
||||
|
||||
constructor (theName: string) {
|
||||
this.name = theName;
|
||||
}
|
||||
}
|
||||
|
||||
let dad = new Octopus ("Man with the 8 strong legs");
|
||||
dad.name = "Man with the 3-piece suit"; // 报错,`name` 是只读的。error TS2540: Cannot assign to 'name' because it is a constant or a read-only property.
|
||||
```
|
||||
|
||||
### 参数式属性(Parameter properties)
|
||||
|
||||
上一个示例不得不在`Octopus`这个类中,声明一个只读成员`name`,以及一个构建器参数`theName`,且随后要立即将`name`设置为`theName`。这种做法被证明是一种十分常见的做法。通过 *参数式属性(parameter properties)* 可在一处就完成成员的创建与初始化。下面是使用参数式属性方法,对上一个`Octopus`类的更进一步修订:
|
||||
|
||||
```typescript
|
||||
class Octopus {
|
||||
readonly numberOfLegs: number = 8;
|
||||
|
||||
constructor (readonly: name: string) {}
|
||||
}
|
||||
```
|
||||
|
||||
请注意这里完全丢弃了`theName`,而仅使用构建器上简化的`readonly name: string`参数,进行`name`成员的创建与初始化。从而实现了将声明与赋值强固到一个地方。
|
||||
|
||||
参数式属性是通过在构造函数参数前,加上可访问性修改器(`public/private/protected`)或`readonly`,抑或同时加上可访问性修改器与`readonly`,得以声明的。对于一个声明并初始化私有成员的参数化属性,就使用`private`做前缀;对于`public`、`protected`及`readonly`亦然。
|
||||
|
||||
|
||||
## 访问器(Accessors)
|
||||
|
||||
TypeScript支持以`getters/setters`方式,来拦截对某对象成员的访问。此特性赋予对各个对象成员的访问以一种更为精良的控制(TypeScript supports getters/setters as a way of intercepting accesses to a member of an object. This gives you a way of having finer-grained control over how a member is accessed on each object)。
|
||||
|
||||
下面将一个简单的类,转换成使用`get`及`set`的形式。首先,从没有获取器与设置器(getter and setter)开始:
|
||||
|
||||
```typescript
|
||||
class Employee {
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
let employee = new Employee ();
|
||||
|
||||
employee.fullName = "Bob Smith";
|
||||
|
||||
if (employee.fullName) {
|
||||
console.log(employee.fullName);
|
||||
}
|
||||
```
|
||||
|
||||
尽管允许人为随机对`fullName`进行直接设置相当方便,但如果某人可以突发奇想地修改名字,那么这样做就可能带来麻烦(while allowing people to randomly set `fullName` directly is pretty handy, this might get us in trouble if people can change names on a whim)。
|
||||
|
||||
下面一版中,将在允许用户修改`employee`对象之前,先检查用户是否有一个可用的密码。这是通过把对`fullName`的直接访问,替换为一个将检查密码的`set`方法来实现的。同时还加入了一个相应的`get`方法,以允许这个示例可以无缝地继续工作。
|
||||
|
||||
```typescript
|
||||
let passcode = "secret passcode";
|
||||
|
||||
class Employer {
|
||||
private _fullName: string;
|
||||
|
||||
get fullName(): string {
|
||||
return this._fullName;
|
||||
}
|
||||
|
||||
set fullName(newName: string) {
|
||||
if (passcode && passcode === "secret passcode") {
|
||||
this._fullName = newName;
|
||||
}
|
||||
else {
|
||||
console.log("Error: Unauthenticated update of employer!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let employer = new Employer ();
|
||||
|
||||
employer.fullName = "Bob Smith";
|
||||
|
||||
if (employer.fullName) {
|
||||
console.log(employer.fullName);
|
||||
}
|
||||
```
|
||||
|
||||
为了证实这里的访问器有对密码进行检查,可修改一下那个密码,看看在其不匹配时,将得到警告没有更新`employer`权限的消息。
|
||||
|
||||
有关访问器需要注意以下几点:
|
||||
|
||||
首先,访问器特性要求将TypeScript编译器设置到输出为ECMAScript 5或更高版本。降级到ECMAScript 3是不支持的。其次,带有`get`却没有`set`的访问器,将自动推理到是`readonly`成员。这样做在从代码生成到`.d.ts`文件时是有帮助的,因为用到该属性的人可以明白他们不能修改该属性。
|
||||
|
||||
|
||||
## 关于静态属性(Static Properties)
|
||||
|
||||
到目前为止,都讨论的是类的 *实例(instance)* 成员,这些成员都是在对象被实例化了后才出现在对象上的(Up to this point, we've only talked about the *instance* members of the class, those that show up on the object when it's instantiated)。其实还可以给类创建 *静态(static)* 成员,所谓静态成员,就是在类本身,而不是示例上可见的成员。下面的示例在`origin`上使用了`static`关键字,因为`origin`是所有`Grid`的通用值。各个实例通过在`origin`前加上该类的名字,来访问此值。与在访问实例时在前面加上`this.`类似,在访问静态成员时,前面加的是`Grid.`。
|
||||
|
||||
```typescript
|
||||
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(2.0);
|
||||
|
||||
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
|
||||
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
|
||||
```
|
||||
|
||||
## 关于抽象类(Abstract Classes)
|
||||
|
||||
抽象类是一些可以派生出其它类的基类。抽象类不可以被直接实例化。与接口的不同之处在于,某个抽象类可以包含其成员实现的细节。抽象类及某个抽象类中的抽象方法的定义,是使用`abstract`关键字完成的(Unlike an interface, an abstract class may contain implementation details for its members. The `abstract` keyword is used to define abstract classes as well as abstract methods within an abstract class)。
|
||||
|
||||
```typescript
|
||||
abstract class Animal {
|
||||
abstract makeSound(): void;
|
||||
|
||||
move(): void {
|
||||
console.log("roaming the earth...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
抽象类中被标记为`abstract`的方法,不包含其具体实现,而必须要在派生类中加以实现。抽象方法与接口方法有着类似的语法。二者都定义了不带有方法体的某个方法的签名。但抽象方法必须带有`abstract`关键字,同时可以包含访问修改器(Abstract methods share a similar syntax to interface methods. Both define the signature of a method without including a method body. However, abstract methods must include the `abstract` keyword and may optionally include access modifiers)。
|
||||
|
||||
```typescript
|
||||
abstract class Department {
|
||||
constructor ( public name: string ) {}
|
||||
|
||||
printName (): void {
|
||||
console.log("Department name: " + this.name);
|
||||
}
|
||||
|
||||
abstract printMeeting (): void; // 在派生类中必须实现此方法
|
||||
}
|
||||
|
||||
class AccountingDepartment extends Department {
|
||||
constructor () {
|
||||
super ("Accounting and Auditing"); // 派生类中的构建器必须调用 `super()` 方法
|
||||
}
|
||||
|
||||
printMeeting (): void {
|
||||
console.log ("The Accounting Department meets each Monday @10am.");
|
||||
}
|
||||
|
||||
generateReports (): void {
|
||||
console.log ("Generating accounting reports...");
|
||||
}
|
||||
}
|
||||
|
||||
let department: Department; // 创建一个到抽象类型的引用是没有问题的
|
||||
department = new Department (); // 报错: 无法创建某个抽象类的实例 error TS2511: Cannot create an instance of the abstract class 'Department'.
|
||||
department = new AccountingDepartment(); // 创建非抽象子类的实例并为其赋值,没有问题
|
||||
department.printName();
|
||||
department.printMeeting();
|
||||
department.generateReports(); // 报错:该方法并不存在与所声明的抽象类型上 error TS2339: Property 'generateReports' does not exist on type 'Department'.
|
||||
```
|
||||
|
||||
## 一些高级技巧(Advanced Techniques)
|
||||
|
||||
### 关于构建器函数
|
||||
|
||||
当在TypeScript中声明类的时候,实际上就是同时创建出了多个的声明。首先是该类的 *实例(instance)* 的类型。
|
||||
|
||||
```typescript
|
||||
class Greeter {
|
||||
greeting: string;
|
||||
|
||||
construtor (msg: string) {
|
||||
this.greeting = msg;
|
||||
}
|
||||
|
||||
greet () {
|
||||
return `Hello, ${this.greeting}`;
|
||||
}
|
||||
}
|
||||
|
||||
let greeter: Greeter;
|
||||
|
||||
greeter = new Greeter("World");
|
||||
console.log(greeter.greet());
|
||||
```
|
||||
|
||||
这里在说到`let greeter: Greeter`时,就使用了`Greeter`作为类`Greeter`的实例的类型。这对于那些其它面向对象语言的程序员来说,几乎是第二天性了(This is almost second nature to programmers from other object-oriented languages)。
|
||||
|
||||
同时还创建出名为`构造函数(construtor function)`的另一个值。这就是在使用`new`关键字,建立该类的实例时,所调用的那个函数。为搞清楚该函数实际面貌,请看看下面由以上示例所生成的JavaScript(ES6):
|
||||
|
||||
```typescript
|
||||
let Greeter = (function (){
|
||||
function Greeter (msg) {
|
||||
this.greeting = msg;
|
||||
}
|
||||
|
||||
Greeter.prototype.greet = function () {
|
||||
return `Hello, ${this.greeting}`;
|
||||
}
|
||||
|
||||
return Greeter;
|
||||
})();
|
||||
|
||||
let greeter;
|
||||
|
||||
greeter = new Greeter("World")!
|
||||
console.log(greeter.greet());
|
||||
```
|
||||
|
||||
这里的`let Greeter` **即将** 被该构造函数赋值(Here, `let Greeter` is going to be assigned (by) the construtor function)。在调用`new`并允许此函数时,就得到一个该类的实例。构造函数还包含了该类的所有静态成员(`greet()`)。还可以把各个类想成是有着一个 *实例* 端与 *静态* 端(Another way to think of each class is that there is an *instance* side and *static* side)。
|
||||
|
||||
下面对该示例稍加修改,来展示这种区别:
|
||||
|
||||
```typescript
|
||||
class Greeter {
|
||||
static standardGreeting = "Hello, there";
|
||||
|
||||
greeting: string;
|
||||
|
||||
greet () {
|
||||
if (this.greeting) {
|
||||
return `Hello, ${this.greeting}`;
|
||||
}
|
||||
else {
|
||||
return Greeter.standardGreeting;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let greeter1 : Greeter;
|
||||
greeter1 = new Greeter();
|
||||
console.log (greeter1.greet());
|
||||
|
||||
let greeterMaker: typeof Greeter = Greeter;
|
||||
greeterMaker.standardGreeting = "Hey there!";
|
||||
|
||||
let greeter2: Greeter = new greeterMaker();
|
||||
console.log(greeter2.greet());
|
||||
```
|
||||
|
||||
本示例中,`greeter1`的运作与上面类似。对`Greeter`类进行了初始化,得到并使用了对象`greeter1`。这样所在前面有见过。
|
||||
|
||||
接下来就直接使用了类`Greeter`。于此创建了一个名为`greeterMaker`的新变量。此变量(注:实际上对应的内存单元)将保有类`Greeter`自身,换种说法就是类`Greeter`的构造函数(类实际上是构造函数?)。这里使用了`typeof Greeter`,从而达到“给我类`Greeter`本身的类型”,而非类示例类型的目的。或者更准确地说,“给我那个名叫`Greeter`符号的类型”,那就是`Greeter`类的构造函数的类型了。此类型将包含`Greeter`的所有静态成员,以及建立`Greeter`类实例的构造函数。后面通过在`greeterMaker`上使用`new`关键字,创建`Greeter`的新实例,并如之前那样运行它们,就就证实了这一点。
|
||||
|
||||
|
||||
### 将类用作接口(Using a class as an interface)
|
||||
|
||||
正如上一小节所说,一个类的声明,创建出两个东西:该类实例的类型,以及构造函数(a class declaration creates two things: a type representing instances of the class and a constructor function)。因为类创建了类型,所以就可以在那些可使用接口地方使用类。
|
||||
|
||||
```typescript
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Point3d extends Point {
|
||||
z: number;
|
||||
}
|
||||
|
||||
let point3d: Point3d = { x: 1, y: 2, z: 3 };
|
||||
```
|
488
04_interfaces.md
Normal file
488
04_interfaces.md
Normal file
@ -0,0 +1,488 @@
|
||||
# 接口(Interfaces)
|
||||
|
||||
## 简介
|
||||
|
||||
TypeScript语言的核心原则之一,就是类型检查着重于值所具有的 *形(shape)*(One of TypeScript's core principles is that type-checking focuses on the *shape* that values have)。这有时候被称为“[鸭子类型(duck typing)](https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B)” 或 “[结构化子类型(structural subtyping)](https://openhome.cc/Gossip/Scala/StructuralTyping.html)”。在TypeScript中,接口充当了这些类型名义上的角色,且是一种定义代码内的合约(约定),以及与项目外部代码的合约约定的强大方式(In TypeScript, interfaces fill the role of naming these types, and are a powerfull way of defining contracts within your code as well as contracts with code outside of your project)。
|
||||
|
||||
|
||||
## 接口初步(Our First Interface)
|
||||
|
||||
理解接口的最容易方式,就是从一个简单的示例开始:
|
||||
|
||||
```typescript
|
||||
function printLable (labelledObj: { label: string }) {
|
||||
console.log(labelledObj.label);
|
||||
}
|
||||
|
||||
let myObj = {size: 10, label: "Size 10 Object"};
|
||||
|
||||
printLable(myObj);
|
||||
```
|
||||
|
||||
类型检查器对`printLable`的调用进行检查。函数`printLable`有着一个单独参数,该参数要求所传入的对象要有一个名为`label`、类型为字符串的属性。请注意这里的`myObj`实际上有着更多的属性,但编译器对传入的参数只检查其 *至少* 有着列出的属性,且要匹配要求的类型。当然也存在TypeScript编译器不那么宽容的情形,这一点在后面会讲到。
|
||||
|
||||
可以再次编写此示例,这次使用接口来描述需要具备`label`属性这一要求:
|
||||
|
||||
```typescript
|
||||
interface LabelledValue {
|
||||
label: string;
|
||||
}
|
||||
|
||||
function printLable ( labelledObj: LabelledValue ) {
|
||||
console.log(labelledObj.label);
|
||||
}
|
||||
|
||||
let myObj = { size: 10, label: "Size 10 Object" };
|
||||
printLable (myObj);
|
||||
```
|
||||
|
||||
这里的`LabelledValue`接口,是一个立即可用于描述前一示例中的要求的名称。它仍然表示有着一个名为`label`、类型为字符串的属性。请注意这里并没有像在其它语言中一样,必须显式地说传递给`printLable`的对象应用该接口。这里只是那个 *形(shape)* 才是关键的。如果传递给该函数的对象满足了列出的要求,那么就是允许的。
|
||||
|
||||
这里需要指出的是,类型检查器不要求这些属性以何种顺序进入,只要有接口所要求的属性及类型即可。
|
||||
|
||||
|
||||
## 可选属性(Optional Properties)
|
||||
|
||||
接口可以包含并不需要的属性。在特定条件下某些属性存在,或根本不存在(Not all properties of an interface may be required. Some exist under certain conditions or may not be there at all)。在建立像是那种将某个仅有少数属性的对象,传递给某个函数的“选项包(option bags)”的模式时,这些可选属性用得比较普遍。
|
||||
|
||||
下面是此种模式的一个示例:
|
||||
|
||||
```typescript
|
||||
interface SquareConfig {
|
||||
color?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function createSquare ( config: SquareConfig ): {color: string; area: number} {
|
||||
let newSquare = {color: "white", area: 100};
|
||||
|
||||
if (config.color) {
|
||||
newSquare.area = config.with * config.width;
|
||||
}
|
||||
|
||||
return newSquare;
|
||||
}
|
||||
|
||||
let mySquare = createSquare({color: "black"});
|
||||
```
|
||||
|
||||
带有可选属性的接口,其写法与其它接口相似,只需在各个可选属性的声明中,在属性名字的末尾,以`?`加以表示即可。
|
||||
|
||||
使用可选属性的优势在于,在对可能存在的属性进行描述的同时,仍然可以阻止那些不是该接口组成部分的属性的使用。比如在将`createSquare`中的`color`属性错误拼写的情况下,就会收到提醒的错误消息:
|
||||
|
||||
```typescript
|
||||
interface SquareConfig {
|
||||
color?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function createSquare ( config: SquareConfig ): { color: string; area: number } {
|
||||
let newSquare = { color: "white", area: 100 };
|
||||
//Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'? (2551)
|
||||
if (config.color) {
|
||||
newSquare.color = config.clor;
|
||||
}
|
||||
|
||||
if ( config.width ) {
|
||||
newSquare.area = config.width * config.width;
|
||||
}
|
||||
|
||||
return newSquare;
|
||||
}
|
||||
|
||||
let mySquare = createSquare({color: "black"});
|
||||
```
|
||||
|
||||
## 只读属性(Readonly properties)
|
||||
|
||||
一些属性只应在对象刚被创建时是可修改的。那么可通过将`readonly`关键字放在该属性名称前,对这些属性加以指定。
|
||||
|
||||
```typescript
|
||||
interface Point {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
```
|
||||
|
||||
就可以通过指派一个对象文字(an object literal),构建出一个`Point`出来。在赋值过后,`x`与`y`就再也不能修改了。
|
||||
|
||||
```typescript
|
||||
let p1: Point = { x: 10, y: 20 };
|
||||
p1.x = 5; //Cannot assign to 'x' because it is a constant or a read-only property. (2540)
|
||||
```
|
||||
|
||||
TypeScript 有着一个`ReadonlyArray<T>`类型,该类型与`Array<T>`一致,只是移除了所有变异方法(with all mutating methods removed),因此向下面这样就可以确保在某个数组创建出后,不会被修改:
|
||||
|
||||
```typescript
|
||||
let a: number[] = [1, 2, 3, 4];
|
||||
let ro: ReadonlyArray<number> = a;
|
||||
ro[0] = 12; //Index signature in type 'ReadonlyArray<number>' only permits reading. (2542)
|
||||
ro.push(5); //Property 'push' does not exist on type 'ReadonlyArray<number>'. (2339)
|
||||
ro.length = 100;//Cannot assign to 'length' because it is a constant or a read-only property. (2540)
|
||||
a = ro;//Type 'ReadonlyArray<number>' is not assignable to type 'number[]'
|
||||
```
|
||||
|
||||
上面这段代码中最后一行可以看出,将整个`ReadonlyArray`往回赋值给正常数组,也是非法的。但仍然可以使用一个类型断言(a type assertion),以消除此错误:
|
||||
|
||||
```typescript
|
||||
a = ro as number[];
|
||||
```
|
||||
|
||||
### `readonly` 与 `const`的区别
|
||||
|
||||
对于要使用`readonly`或`const`,最简单的办法就是区分是要在变量上,还是属性上使用。对于变量,当然就用`const`,属性则用`readonly`。
|
||||
|
||||
## 关于多余属性检查(Excess Property Checks)
|
||||
|
||||
在采用了接口的第一个示例中,TypeScript令到可将`{size: number; label: string;}`传递给某些仅期望一个`{label: string;}`的地方。后面还介绍了关于可选属性,以及可选属性在名为“选项包(option bags)”的地方如何发挥作用。
|
||||
|
||||
但是,如像在JavaScript中那样,将这两个特性单纯地结合在一起,就足以杀死你自己,下面就用最后一个示例使用`createSquare`来说明一下:
|
||||
|
||||
```typescript
|
||||
interface SquareConfig {
|
||||
color?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function createSquare ( config: SquareConfig ): { color: string; area: number } {
|
||||
// ...
|
||||
}
|
||||
|
||||
let mySquare = createSquare ({ colour: "red", width: 100 });
|
||||
```
|
||||
|
||||
注意这里给予`createSquare`的参数被写成了`colour`,而不是`color`。在普通的JavaScript中,这类错误将不会报错。
|
||||
|
||||
对于这个诚实,你可能会说没有错误拼写,因为`width`属性是兼容的,没有`color`属性出现,同时这里额外的`colour`属性是不重要的。
|
||||
|
||||
不过,TypeScript会认为在这段代码中存在问题。对象字面值会受到特别对待,同时在将对象字面值赋予给其它变量,或者将它们作为参数加以传递时,而收到 *多余属性检查*。如某个对象字面值有着任何目标对象不具有的属性时,就会报出错误。
|
||||
|
||||
```typescript
|
||||
// Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
|
||||
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'? (2345)
|
||||
let mySquare = createSquare({colour: "red", width: 100});
|
||||
```
|
||||
|
||||
绕过此类检查实际上相当简单。最容易的做法就是使用一个类型断言(a type assertion):
|
||||
|
||||
```typescript
|
||||
let mySquare = createSquare({width: 100, opacity: 0.5} as SquareConfig);
|
||||
```
|
||||
|
||||
不过,在确定对象可能有某些在特别情况下会用到额外属性时,一种更好的方式就是为其添加一个字符串的索引签名(a string index signature)。比如在这里的`SquareConfig`们就可以有着上面`color`与`width`属性,但也可以具有任意数量的其它属性,那么就可以将其定义成下面这样:
|
||||
|
||||
```typescript
|
||||
interface SquareConfig {
|
||||
color?: string;
|
||||
width?: number;
|
||||
[propName: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
索引签名这个概念在后面会涉及,这里说的是`SquareConfig`可以有着任意数量的属性,而只要这些属性不是`color`或`width`就可以,它们的类型并不重要。
|
||||
|
||||
绕过这些检查的一种终极方式,可能有点意外,就是将该对象赋值给另一变量:因为`squareConfig`不会受多余属性检查,因此编译器也就不会给出错误。
|
||||
|
||||
```typescript
|
||||
let squareConfig = { colour: "red", width: 100 };
|
||||
let mySquare = createSquare(squareConfig);
|
||||
```
|
||||
|
||||
请记住对于像是上面的简单代码,一般不必尝试“绕过”这些检查。而对于更为复杂的、有着方法并存有状态的对象字面值(complex object literals that have methods and hold state),可能就要牢记这些技巧了,但大多数的多余属性错误,都是真实存在的bugs。那就意味着在使用诸如选项包(option bags)这类的特性,而出现多余属性检查类问题时,就应该对类型定义加以审视。在此实例中,如果允许将某个有着`color`或`colour`属性的对象传递给`createSquare`方法,那么就要修改`SquareConfig`的定义,来反应出这一点。
|
||||
|
||||
## 函数的类型(Function Types)
|
||||
|
||||
对于描述JavaScript的对象所能接受的范围宽广的形,接口都是可行的(Interfaces are capable of describing the wide range of shapes that JavaScript objects can take)。除了用于描述带有属性的对象,接口还可以描述函数类型。
|
||||
|
||||
要用接口来描述函数,就要给予该接口一个调用签名(a call signature)。这就像是一个仅有着参数清单与返回值类型的函数声明。参数清单中的各参数,都要求名称与类型。
|
||||
|
||||
```typescript
|
||||
interface SearchFunc {
|
||||
(source: string, subString: string): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
一旦定义好,就可以像使用其它接口一样,对此函数类型接口(this function type interface)进行使用了。这里展示了创建一个某种函数类型的变量,并把同一类型的函数值赋予给它的过程(create *a variable of a function type* and assign it *a function value* of the same type)。
|
||||
|
||||
```typescript
|
||||
let mySearch: SearchFunc;
|
||||
mySearch = function (source: string; subString: string) {
|
||||
let result = source.search(subString);
|
||||
return result > -1;
|
||||
}
|
||||
```
|
||||
|
||||
参数名称无需匹配,就可以对函数类型进行正确的类型检查。比如这里可以像下面这样编写上面的示例:
|
||||
|
||||
```typescript
|
||||
let mySearch: SearchFunc;
|
||||
mySearch = function (src: string, sub: string): boolean {
|
||||
let result = src.search(sub);
|
||||
return result > -1;
|
||||
}
|
||||
```
|
||||
|
||||
函数参数会逐一检查,以每个相应参数位置的类型,与对应的类型进行检查的方式进行(Function parameters are checked one at a time, with the type in each corresponding parameter position checked against each other)。如完全不打算指定类型,那么TypeScript的上下文类型系统就可以推断出参数类型,因为该函数值是直接赋予给`SearchFunc`类型的变量的。同时,这里函数表达式的返回值类型,是由其返回值(也就是`false`或`true`)隐式给出的。加入让该函数返回数字或字符串,那么类型检查器(the type-checker)就会发出返回值类型与`SearchFunc`接口中描述的返回值类型不符的警告。
|
||||
|
||||
```typescript
|
||||
let mySearch: SearchFunc;
|
||||
mySearch = function (src, sub) {
|
||||
let result = src.search(sub);
|
||||
return result > -1;
|
||||
}
|
||||
```
|
||||
|
||||
## 可索引的类型(Indexable Types)
|
||||
|
||||
与使用接口来描述函数类型类似,还可以使用接口类描述那些可以索引的类型(types that we can "index into"),比如`a[10]`,抑或`ageMap["daniel"]`这样的。可索引类型有着一个描述用于在该对象内部进行索引的类型的 *索引签名(index signature)*,以及在索引时返回值的类型。来看看这个示例:
|
||||
|
||||
```typescript
|
||||
interface StringArray {
|
||||
[index: number]: string;
|
||||
}
|
||||
|
||||
let myArray: StringArray;
|
||||
myArray = ["Bob", "Fred"];
|
||||
|
||||
let myStr: string = myArray[0];
|
||||
```
|
||||
|
||||
在上面的代码中,有着一个带有索引签名的`StringArray`接口。此索引签名指出在某个`StringArray`以某个`number`加以索引时,它将返回一个`string`。
|
||||
|
||||
TypeScript支持的索引签名有两种类型:字符串及数字。同时支持这两种类型的索引器是可能的,但从某个数字的索引器所返回的类型,则必须是从字符串索引器所返回类型的子类型(It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer)。这是因为在以某个`number`进行索引时,JavaScript实际上会在对某个对象进行索引前,将其转换成`string`。也就是说,在使用`100`(`number`)来进行索引时,实际上与使用`"100"`(`string`)效果是一样的,因此二者就需要一致(That means that indexing with `100` (a `number`) is the same thing as indexing with `"100"` (a `stirng`), so the two need to be consistent)。
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
breed: string;
|
||||
}
|
||||
|
||||
// Numeric index type 'Animal' is not assignable to string index type 'Dog'. (2413)
|
||||
interface NotOkay {
|
||||
[x: number]: Animal;
|
||||
[x: string]: Dog;
|
||||
}
|
||||
```
|
||||
|
||||
尽管字符串的索引签名是描述“字典”模式的一种强大方式,但它们同时强制了与它们的返回值类型匹配的属性值(While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return type)。这是因为字符串的索引申明了`obj.property`同时与`obj["property"]`可用。在下面的示例中,`name`的类型与该字符串索引器的类型并不匹配,那么类型检查器就会给出一个错误:
|
||||
|
||||
```typescript
|
||||
//Property 'name' of type 'string' is not assignable to string index type 'number'. (2411)
|
||||
interface NumberDictionary {
|
||||
[index: string]: number;
|
||||
length: number;
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
最后,为了阻止对指数的赋值,就可以将这些索引签名置为只读(Finally, you can make index signatures readonly in order to prevent assignment to their indices):
|
||||
|
||||
```typescript
|
||||
interface ReadonlyStringArray {
|
||||
readonly [index: number]: string;
|
||||
}
|
||||
|
||||
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
|
||||
//Index signature in type 'ReadonlyStringArray' only permits reading. (2542)
|
||||
myArray[2] = "Mallory";
|
||||
```
|
||||
|
||||
因为此处的索引签名是只读的,因此这里就不能设置`myArray[2]`了。
|
||||
|
||||
|
||||
## 类的类型(Class Types)
|
||||
|
||||
###应用某个接口(Implementing an interface)
|
||||
|
||||
在诸如C#及Java这样的语言中,接口的一种最常用方式,就是显式地强调某个类满足一种特定的合约,那么在TypeScript中,这样做也是可能的。
|
||||
|
||||
```typescript
|
||||
interface ClockInterface {
|
||||
currentTime: Date;
|
||||
}
|
||||
|
||||
class Clock implements ClockInterface {
|
||||
currentTime: Date;
|
||||
constructor (h: number, m: number) {}
|
||||
}
|
||||
```
|
||||
|
||||
在接口中还可以对将在类中应用到的方法进行描述,就像下面示例中对`setTime`所做的那样:
|
||||
|
||||
```typescript
|
||||
interface ClockInterface {
|
||||
currentTime: Date;
|
||||
setTime (d: Date);
|
||||
}
|
||||
|
||||
class Clock implements ClockInterface {
|
||||
currentTime: Date;
|
||||
|
||||
setTime (d: Date) {
|
||||
this.currentTime = d;
|
||||
}
|
||||
|
||||
constructor (h: number, m: number) {}
|
||||
}
|
||||
```
|
||||
|
||||
接口对类的公共侧进行了描述,而不是同时描述公共及私有侧。这就禁止对使用接口来对同时有着特定类型的该类实例的私有面的类,进行检查(Interfaces describe the public side of the class, rather than both the public and private side. This prohibits you from using them to check that a class also has particular types for the private side of the class instance)。
|
||||
|
||||
### 类的静态与实例侧(Difference between the static and instance sides of classes)
|
||||
|
||||
在与类一同使用接口是时,记住类有着两种类型:静态侧的类型与示例侧的类型(the type of the static side and the type of the instance side),是有帮助的。或许已经注意到在使用构建签名来建立一个接口,并尝试应用此接口来建立类的时候,将报出一个错误:
|
||||
|
||||
```typescript
|
||||
interface ClockInterface {
|
||||
new (hour: number, minute: number);
|
||||
}
|
||||
|
||||
class Clock implements ClockInterface {
|
||||
currentTime: Date;
|
||||
constructor (h: number, m: number) {}
|
||||
}
|
||||
```
|
||||
|
||||
这是因为在某个类应用某个接口时,仅有该类的实例侧被检查了。因为该构建器位处静态侧,所以其并不包含在此检查中。
|
||||
|
||||
那么就需要直接在该类的静态侧上动手了。在此实例中,定义了两个接口:用于构建器的`ClockConstrutor`与用于实例方法的`ClockInterface`。随后为便利起见,这里定义了一个构建器函数`createClock`,以创建出传递给它的该类型的实例。
|
||||
|
||||
```typescript
|
||||
interface ClockConstrutor {
|
||||
new (hour: number, minute: number): ClockInterface;
|
||||
}
|
||||
|
||||
interface ClockInterface {
|
||||
tick();
|
||||
}
|
||||
|
||||
function createClock (ctor: ClockConstrutor, hour: number, minute: number): ClockInterface {
|
||||
return new ctor (hour, minute);
|
||||
}
|
||||
|
||||
class DigitalClock implements ClockInterface {
|
||||
constructor (h: number, m: number) {}
|
||||
|
||||
tick () {
|
||||
console.log("beep beep");
|
||||
}
|
||||
}
|
||||
|
||||
class AnalogClock implements ClockInterface {
|
||||
constructor (h: number, m: number) {}
|
||||
|
||||
tick () {
|
||||
console.log("tick tock");
|
||||
}
|
||||
}
|
||||
|
||||
let digital = createClock (DigitalClock, 12, 17);
|
||||
let analog = createClock (AnalogClock, 7, 32);
|
||||
```
|
||||
|
||||
因为`createClock`第一个参数是`ClockConstrutor`, 那么在`createClock(AnalogClock, 7, 32)`中,它就对`AnalogClock`有着正确的构建签名进行检查。
|
||||
|
||||
## 扩展接口(Extending Interfaces)
|
||||
|
||||
与类一样,接口也可以相互扩展。此特性令到将某接口的成员拷贝到另一接口可行,这就在将接口分离为可重用组件时,提供更多的灵活性。
|
||||
|
||||
```typescript
|
||||
interface Shape {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Square extends Shape {
|
||||
sideLength: number;
|
||||
}
|
||||
|
||||
let square = <Square> {};
|
||||
square.color = "blue";
|
||||
square.sideLength = 10;
|
||||
```
|
||||
|
||||
一个接口还可以对多个接口进行扩展,从而创建出所有接口的一个联合(a combination of all of the interfaces):
|
||||
|
||||
```typescript
|
||||
interface Shape {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface PenStroke {
|
||||
penWidth: number;
|
||||
}
|
||||
|
||||
|
||||
interface Square extends Shape, PenStroke {
|
||||
sideLength: number;
|
||||
}
|
||||
|
||||
let square = <Square> {};
|
||||
square.color = "blue";
|
||||
square.sideLength = 10;
|
||||
square.penWidth = 5.0;
|
||||
```
|
||||
|
||||
## 混合类型(Hybrid Types)
|
||||
|
||||
正如早先所提到的那样,接口具备描述存在于真实世界JavaScript中的丰富类型(As we mentioned earlier, interfaces can describe the rich types present in real world JavaScript)。由于JavaScript的动态且灵活的天性,因此偶尔会遇到某个对象将以结合上述各种类型的方式运作的情况。
|
||||
|
||||
这类实例之一,就是某个对象同时以函数与对象,并带有一些属性方式行事:
|
||||
|
||||
```typescript
|
||||
interface Counter {
|
||||
(start: number): string;
|
||||
interval: number;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
function getCounter (): Counter {
|
||||
let counter = <Counter> function (start: number) {};
|
||||
counter.interval = 123;
|
||||
counter.reset = function () {};
|
||||
return counter;
|
||||
}
|
||||
|
||||
let c = getCounter();
|
||||
c(10);
|
||||
c.reset();
|
||||
c.interval = 5.0;
|
||||
```
|
||||
|
||||
在与第三方JavaScript(注:TypeScript, 你,别人的程序)交互时,就需要使用上面这样的模式,来充分描述类型的形状(When interacting with 3rd-party JavaScript, you may need to use patterns like the above to fully describe the shape of the type)。
|
||||
|
||||
## 对类进行扩展的接口(Interface Extending Classes)
|
||||
|
||||
当某个接口对类类型进行扩展时,它将继承该类的成员,却不继承这些成员的实现(When an interface type extends a class type, it inherits the members of the class but not their implementations)。这就如同接口已经对该类的所有成员进行了声明,而没有提供到其具体实现。接口甚至会继承到某个基类的私有及受保护成员。那就意味着在创建某个对带有私有及保护成员的类进行扩展的接口时,所建立的接口类型,就只能被被扩展的类所其子类所应用(实现,It is as if the interface had declared all of the members of the class without providing an implementation. Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it)。
|
||||
|
||||
在有着大的继承层次时,此特性是有用的,但需要指出的是,这只在代码中有着仅带有确定属性的子类时才有用(This is useful when you have a large inheritance hierarchy, but want to specify that your code works with only subclass that have certain properties)。这些子类除了继承自基类外,不必是有关联的。比如:
|
||||
|
||||
```typescript
|
||||
class Control {
|
||||
private state: any;
|
||||
}
|
||||
|
||||
interface SelectableControl extends Control {
|
||||
select (): void;
|
||||
}
|
||||
|
||||
class Button extends Control implements SelectableControl {
|
||||
select () {}
|
||||
}
|
||||
|
||||
class TextBox extends Control {}
|
||||
|
||||
//Class 'Image' incorrectly implements interface 'SelectableControl'.
|
||||
//Property 'state' is missing in type 'Image'. (2420)
|
||||
class Image implements SelectableControl {
|
||||
select () {}
|
||||
}
|
||||
|
||||
class Location {}
|
||||
```
|
||||
|
||||
在上面的示例中,`SelectableControl`包含了所有`Control`的成员,包括私有的`state`属性。因为`state`是一个私有成员,因此对于`Control`的后代,就只可能去应用`SelectableControl`这个接口了。这是因为只有`Control`的后代,才会有着这个源自同一声明的`state`私有成员,这也是私有成员可用的一个要求(Since `state` is a private member it is only possible for descendants of `Control` to implement `SelectableControl`. This is because only descendants of `Control` will have a `state` private member that originates in the same declaration, which is a requirement for private members to be compatible)。
|
||||
|
||||
在`Control`这个类中,通过`SelectableControl`的某个实例去访问`state`这个私有成员,是可能的。同时,某个`SelectableControl`也会与一个已知有着`select`方法的`Control`那样行事(Effectively, a `SelectableControl` acts like a `Control` that is known to have a `select` method)。这里的`Button`与`TextBox`都是`SelectableControl`的子类型(因为它们都是继承自`Control`,并有着`select`方法), 但`Image`与`Location`就不是了。
|
416
05_functions.md
Normal file
416
05_functions.md
Normal file
@ -0,0 +1,416 @@
|
||||
# 函数(Functions)
|
||||
|
||||
## 简介
|
||||
|
||||
在JavaScript中,函数是所有应用的基石。正是使用它们来构建出抽象层、模仿类、信息的隐藏,以及模块(Functions are the fundamental building block of any application in JavaScript. They're how you build up layers of abstraction, mimicking classes, information hiding, and modules)。在TypeScript中,尽管有着类、命名空间及模块特性,在描述怎么完成某些事情上,函数仍然扮演了重要角色。为更易于使用函数,TypeScript还为标准的JavaScript函数,加入了一些新的功能。
|
||||
|
||||
## 关于函数
|
||||
|
||||
如同在JavaScript中那样,一开始呢,TypeScript的函数可以命名函数,或匿名函数的形式予以创建。这就令到可选择对于应用最为适当的方式,无论是在构建API中的一个函数清单,或者构建一个传递给另一函数的一次性函数都行。
|
||||
|
||||
下面就用示例来快速地概括JavaScript中这两种方式的样子:
|
||||
|
||||
```javascript
|
||||
// 命名函数
|
||||
function add (x, y){
|
||||
return x+y;
|
||||
}
|
||||
|
||||
//匿名函数
|
||||
let myAdd = function (x, y) { return x+y; };
|
||||
```
|
||||
|
||||
与在JavaScript中一样,函数可对函数体外部的变量进行引用。在这样做的时候,它们就被叫做对这些变量进行捕获(Just as in JavaScript, functions can refer to variable outside of **the function body**. When they do so, they're said **to `capture` these variables**)。尽管对捕获的原理的掌握,及使用此技巧时所做的权衡超出了本文的范围,对此机制的扎实理解,仍然是熟练运用JavaScript与TypeScript的重要方面。
|
||||
|
||||
```typescript
|
||||
let z = 100;
|
||||
|
||||
function addToZ (x, y) {
|
||||
return x + y + z;
|
||||
}
|
||||
```
|
||||
|
||||
## 函数类型(Function Types)
|
||||
|
||||
### 给函数赋予类型(Typing the function)
|
||||
|
||||
下面就给上一个简单的示例加上类型:
|
||||
|
||||
```typescript
|
||||
function add (x: number, y: number): number {
|
||||
return x + y;
|
||||
}
|
||||
|
||||
let myAdd = function (x: number, y: number): number { return x + y; };
|
||||
```
|
||||
|
||||
可将类型添加到各个参数,并于随后以添加类型的方式,为函数本身加上类型。TypeScript可通过查看`return`语句,来推断出返回值的类型,因此在很多情况下就可以省略返回值的类型。
|
||||
|
||||
### 函数类型的编写(Writing the function type)
|
||||
|
||||
既然已经输入了函数,那么就来通过查看函数类型的各个部分,从而写出该函数的完整类型吧(Now that we've typed the function, let's write the full type of the function out by looking at the each piece of the function type)。
|
||||
|
||||
```typescript
|
||||
// 注意,这里的 myAdd 就是一个函数类型
|
||||
let myAdd: (x: number, y: number) => number = function (x: number, y: number): number {return x+y;};
|
||||
```
|
||||
|
||||
某个函数的类型,有着同样的两个部分:参数的类型以及返回值类型。在写出整个函数类型时,两个部分都是必须的。参数部分的编写与参数列表一样,给出各个参数名称与类型就可以了。此名称仅对程序的易读性有帮助。因此我们也可以像下面这样编写:
|
||||
|
||||
```typescript
|
||||
let myAdd: (baseValue: number, increment: number) => number =
|
||||
function (x: number, y: number): number { return x + y; };
|
||||
```
|
||||
|
||||
一旦有了参数类型这一行,它就会被认为是该函数的有效类型,而不管在函数类型中所给予参数的名称。
|
||||
|
||||
第二部分就是返回值类型了。这里是通过在参数与返回值之间使用胖箭头(a fat arrow, `=>`),来表明哪一个是返回值类型的。正如前面所提到的, **返回值类型正是函数类型所必要的部分,因此即使函数没有返回值,也要使用`void`来表示返回值类型,而不是省略掉**。
|
||||
|
||||
值得一提的是,函数类型的组成,仅是参数类型与返回值类型。捕获的变量在类型中并未体现出来。实际上,捕获的变量是所有函数的“隐藏状态”的部分,且不构成其API(Captured variables are not reflected in the type. In effect, captured variables are part of the "hidden state" of any function and do not make up its API)。
|
||||
|
||||
### 类型推理(Inferring the types)
|
||||
|
||||
在上面的示例中,你可能已经注意到,就算只在等号的一侧有类型,TypeScript编译器也能推断出类型:
|
||||
|
||||
```typescript
|
||||
// 这里的 myAdd 有着完整的函数类型
|
||||
let myAdd = function (x: number, y: number): number { return x+y; };
|
||||
|
||||
// 此处 'x' 与 'y' 仍然有着数字类型
|
||||
let myAdd: (baseValue: number, increment: number) => number =
|
||||
function (x, y) {return x+y;};
|
||||
```
|
||||
|
||||
这就叫做“上下文赋型(contextual typing)”,是类型推理的一种形式。此特性有助于降低为维护程序类型化所做的努力(This is called "contextual typing", a form of type inference. This helps cut down on the amount of effort to keep your program typed)。
|
||||
|
||||
## 可选参数与默认参数(Optional and Default Parameters)
|
||||
|
||||
在TypeScript中,所有参数都假定为是函数所要求的。但这并不意味着参数不可以被给予`null`或`undefined`,相反,在函数被调用时,编译器会对用户是否为各个参数提供了值进行检查。编译器同时也假定这些参数就仅是需要传递给函数的参数。简单的说,给予函数的参数个数,必须与函数所期望的参数个数一致。
|
||||
|
||||
```typescript
|
||||
function buildName ( firstName: string, lastName: string ) {
|
||||
return firstName + "" + lastName;
|
||||
}
|
||||
|
||||
let result1 = buildName ( "Bob" );
|
||||
let result2 = buildName ("Bob", "Adams", "Sr.");
|
||||
let result3 = buildName ("Bob", "Adams");
|
||||
```
|
||||
|
||||
而在JavaScript中,所有参数都是可选的,同时用户可以在适当的时候省略这些参数。在省略参数时,这些参数就是`undefined`。通过在参数上添加`?`,也能在TypeScript中获得此功能。比如在上一个示例中要令到姓这个参数(the last name parameter)是可选的:
|
||||
|
||||
```typescript
|
||||
function buildName (firstname: string, lastname?: string) {
|
||||
if (lastname)
|
||||
return firstName + "" lastName;
|
||||
else
|
||||
return firstName;
|
||||
}
|
||||
|
||||
let result1 = buildName ( "Bob" );
|
||||
let result2 = buildName ("Bob", "Adams", "Sr.");
|
||||
let result3 = buildName ("Bob", "Adams");
|
||||
```
|
||||
|
||||
所有可选参数都应放在必需参数之后。比如这里打算令到名(the first name)可选,而不是姓可选,那么就需要调整函数中参数的顺序,将名放在姓的后面。
|
||||
|
||||
在TypeScript中,还可以为参数设置一个默认值,以便在用户没有提供该参数值,或者用户在该参数位置提供了`undefined`时,赋值给那个参数。这类参数叫做已默认初始化了的参数(default-initialized parameters)。这里同样用上一个示例,将姓默认设置为`Smith`。
|
||||
|
||||
```typescript
|
||||
function buildName (firstName: string, lastName = "Smith") {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
let result1 = buildName ("Bob");
|
||||
let result2 = buildName ("Bob", undefined);
|
||||
let result3 = buildName ("Bob", "Adams", "Sr. ");
|
||||
let result4 = buildName ("Bob", "Adams");
|
||||
```
|
||||
|
||||
位于所有必需参数之后的已默认初始化的参数,是作为可选参数加以处理的,同时与可选参数一样,在对其相应函数进行调用时可以省略。这就意味着可选参数与随后的默认参数,在其类型上有着共性,因此这两个函数:
|
||||
|
||||
```typescript
|
||||
function buildName (firstName: string, lastName?: string) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
与
|
||||
|
||||
```typescript
|
||||
function buildName (firstName: string, lastName = "Smith") {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
共用了同样的类型 `(firstName: string, lastName?: string) => string`。在类型中,`lastName`的默认值已然消失了,而只剩下该参数是可选参数的事实。
|
||||
|
||||
与普通可选参数不同,已默认初始化的参数,并不需要出现在必需参数后面。在某个已默认初始化参数位处某个必需参数之前时,用户就需要显式地传递`undefined`,以取得默认值。比如,这里可将上一个示例编写为仅在`firstName`上有一个默认初始参数(a default initializer):
|
||||
|
||||
```typescript
|
||||
function buildName (firstName = "Will", lastName: string) {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
let result1 = buildName ("Bob"); // 将报错,参数太少
|
||||
let result2 = buildName ("Bob", "Adams", "Sr. "); // 报错,参数太多
|
||||
let result3 = buildName ("Bob", "Adams");
|
||||
let result4 = buildName (undefined, "Adams");
|
||||
```
|
||||
|
||||
## 其余参数(Rest Parameters)
|
||||
|
||||
必需参数、可选参数与默认参数,它们都有着一个相同点:它们同时都只能与一个参数交谈。某些情况下,需要处理作为一组的多个参数的情况,或者可能不知道函数最终会取多少个参数。在JavaScript中,可以直接使用每个函数体中都可见的`arguments`变量,来处理此类问题。
|
||||
|
||||
在TypeScript中,可将这些参数聚集到一个变量中:
|
||||
|
||||
```typescript
|
||||
function buildName (firstName: string, ...restOfName: string[]) {
|
||||
return firstName + " " + restOfName.join(" ");
|
||||
}
|
||||
|
||||
let employeeName = buildName ("Joseph", "Sameul", "Lucas", "MacKinzie");
|
||||
```
|
||||
|
||||
*其余参数* 是以数量不限的可选参数加以处理的( *Rest parameters* are treated as a boundless number of optional parameters)。在将参数传递给某个其余参数时,可传递任意所需数目的参数;一个也不传也是可以的。编译器将构建一个使用位处省略号(the ellipsis, `...`)之后的名称,而传递的那些参数的数组,从而允许在函数中使用到这些参数。
|
||||
|
||||
在带有其余参数的函数类型中,也有使用省略号:
|
||||
|
||||
```typescript
|
||||
function buildName (firstName: string, ...restOfName: string[]) {
|
||||
return firstName + " " + restOfName.join(" ");
|
||||
}
|
||||
|
||||
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
|
||||
```
|
||||
|
||||
## 关于`this`
|
||||
|
||||
在JavaScript中,学会如何使用`this`,就相当于是一个成人仪式(Learning how to use `this` in JavaScript is something of a rite of passage)。因为TypeScript是JavaScript的一个超集,那么TypeScript的开发者同样需要掌握怎样使用`this`,以及怎样发现其未被正确使用。
|
||||
|
||||
幸运的是,TypeScript提供了几种捕获不正确使用`this`的技巧。如想要了解JavaScript中`this`的运作原理,请移步 Yehuda Katz 的 [Understanding JavaScript Function Invocation and "this"](http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/)一文。Yehuda的文章对`this`的内部运作讲得很好,因此这里就只涉及一些基础知识。
|
||||
|
||||
### `this`与箭头函数(arrow functions)
|
||||
|
||||
在JavaScript中,`this`是于某个函数被调用时,设置的一个变量。这就令到其成为一项非常强大且灵活的特性,不过其代价就是务必要知悉函数执行所在的上下文。这是非常容易搞混的,尤其是在返回值是个函数,或将函数作为参数加以传递时(注:也就是回调函数,callback。In JavaScript, `this` is a variable that's set when a function is called. This makes it a very powerful and flexible feature, but it comes at the cost of always having to know about the context that a function is executing in. This is notoriously confusing, especially when returning a function or passing a function as an argument)。
|
||||
|
||||
请看一个示例:
|
||||
|
||||
```typescript
|
||||
let deck = {
|
||||
suits: ["hearts", "spades", "clubs", "diamonds"],
|
||||
cards: Array(52),
|
||||
createCardPicker: function () {
|
||||
return function () {
|
||||
let pickedCard = Math.floor(Math.random() * 52);
|
||||
let pickedSuit = Math.floor(pickedCard / 13);
|
||||
|
||||
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cardPicker = deck.createCardPicker ();
|
||||
let pickedCard = cardPicker ();
|
||||
|
||||
alert ("card: " + pickedCard.card + " of" + pickedCard.suit);
|
||||
```
|
||||
|
||||
请注意`createCardPicker`是一个本身返回函数的函数。如果运行此示例,将得到一个错误(`Uncaught TypeError: Cannot read property 'suits' of undefined`),而不是期望的警告框。这是因为在有`createCardPicker`所创建的函数中所使用的`this`,将被设置为`window`而不是`deck`对象。那是因为这里是在`cardPicker`本身上对其进行调用的。像这样的 **顶级非方法(对象的方法)语法调用**,将使用`window`作为`this`(注意:严格模式下,`this`将是`undefined`而不是`window`。Notice that `createCardPicker` is a function that itself returns a function. If we tried to run the example, we would get an error instead of the expected alert box. This is because the `this` being used in the function created by `createCardPicker` will be set to `window` instead of our `deck` object. That's because we call `cardPicker` on its own. **A top-level non-method syntax call** like this will use `window` for `this`. (Note: under strict mode, `this` will be `undefined` rather than `window`))。
|
||||
|
||||
要解决此问题,只需要在返回该函数以便后续使用之前,确保该函数是绑定到正确的`this`就可以了。这样的话,无论后续如何被使用该函数,它都能够参考最初的`deck`对象了。为实现此目的,这里就要将该函数表达式,修改为使用ECMAScript 6的箭头语法。箭头函数实在函数被创建时捕获`this`,而不是在函数被调用时。
|
||||
|
||||
```typescript
|
||||
let deck = {
|
||||
suits: ["hearts", "spades", "clubs", "diamonds"],
|
||||
cards: Array(52),
|
||||
createCardPicker: function () {
|
||||
// 注意:现在下面这行是一个箭头函数,令到可以立即对`this`进行捕获
|
||||
return () => {
|
||||
let pickedCard = Math.floor(Math.random() * 52);
|
||||
let pickedSuit = Math.floor(pickedCard / 13);
|
||||
|
||||
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cardPicker = deck.createCardPicker ();
|
||||
let pickedCard = cardPicker ();
|
||||
|
||||
alert ("card: " + pickedCard.card + " of" + pickedCard.suit);
|
||||
```
|
||||
|
||||
更甚者,如将`--noImplicitThis`编译指令传递给编译器,那么TypeScript就会在代码中有着此类错误时,给出警告。编译器将指出`this.suits[pickedSuit]`中的`this`的类型为`any`。
|
||||
|
||||
### `this` 参数(`this` parameters)
|
||||
|
||||
不幸的是,`this.suits[pickedSuit]`的类型,仍然是`any`。这是因为`this`来自于该对象字面值内部的函数表达式。要解决这个问题,就可以提供到一个显式的`this`参数。`this`参数都是位于函数参数清单的第一个位置,是假参数(Unfortunately, the type of `this.suits[pickedCard]` is still `any`. That's because `this` comes from the function expression inside the object literal. To fix this, you can provide an explicit `this` parameter. `this` parameters are fake parameters that come first in the parameter list of a function):
|
||||
|
||||
```typescript
|
||||
function f(this: void) {
|
||||
// 确保`this`在此对立函数中是不可用的的(make sure `this` is unusable in this standalone function)
|
||||
}
|
||||
```
|
||||
|
||||
来给上面的示例加入接口 `Card` 与 `Deck`,从而使得类型更为清晰明了而更易于重用:
|
||||
|
||||
```typescript
|
||||
interface Card {
|
||||
suit: string;
|
||||
card: number;
|
||||
}
|
||||
|
||||
interface Deck {
|
||||
suits: string [];
|
||||
cards: number [];
|
||||
createCardPicker (this: Deck): () => Card;
|
||||
}
|
||||
|
||||
let deck: Deck = {
|
||||
suits: ["hearts", "spades", "clubs", "diamonds"],
|
||||
cards: Array(52),
|
||||
// 注意:此函数现在显式地指明了其被调必须是类型`Deck`(NOTE: The function now explicitly specifies
|
||||
// that its callee must be of type Deck)
|
||||
|
||||
createCardPicker: function (this: Deck) {
|
||||
return () => {
|
||||
let pickedCard = Math.floor (Math.random() * 52);
|
||||
let pickedSuit = Math.floor (pickedCard / 13);
|
||||
|
||||
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cardPicker = deck.createCardPicker ();
|
||||
let pickedCard = cardPicker ();
|
||||
|
||||
console.log("Card: " + pickedCard.card + " of " + pickedCard.suit);
|
||||
```
|
||||
|
||||
现在TypeScript就知道了`createCardPicker`期望是在`Deck`对象上被调用了。那就意味着现在的`this`是`Deck`类型,而不再是`any`类型了,由此`--noImplicitThis`编译指令也不会再引起任何的错误了。
|
||||
|
||||
|
||||
### 回调函数中的`this`
|
||||
|
||||
在将函数传递给将随后掉用到这些函数的某个库时,对于回调函数中的`this`,也是非常容易出错的地方。因为调用回调函数的库,将像调用普通函数那样调用回调函数,所以`this`将是`undefined`。同样,作出一些努力后,也可以使用`this`参数,来防止回调中错误的发生。首先,编写库的同志们,你们要使用`this`来对回调类型加以注释:
|
||||
|
||||
```typescript
|
||||
interface UIElement {
|
||||
addClickListener (onclick: (this: void, e: Event) => void): void;
|
||||
}
|
||||
```
|
||||
|
||||
`this: void` 指的是`addClickListener`期望`onclick`是一个不要求`this`类型的函数(`this: void` means that `addClickListener` expects `onclick` to be a function that does not require a `this` type)。
|
||||
|
||||
接着,使用`this`来对调用代码进行注释:
|
||||
|
||||
```typescript
|
||||
class Handler {
|
||||
info: string;
|
||||
onClickBad (this: Handler, e: Event) {
|
||||
// 呃,这里使用了 `this`。如果使用这个回调函数,那么在运行时就将崩溃
|
||||
this.info = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
let h = new Handler ();
|
||||
uiElement.addClickListener (h.onClickGood);
|
||||
```
|
||||
|
||||
在对`this`进行了注释后,就显式的要求`onClickGood`必须在`Handler`类的某个实例上加以调用(With `this` annotated, you make it explicit that `onClickGood` must be called on an instance of `Handler`)。那么TypeScript就将侦测到`addClickListener`要求有着`this: void`的函数了。为解决这个问题,就需要修改`this`的类型:
|
||||
|
||||
```typescript
|
||||
class Handler {
|
||||
info: string;
|
||||
onClickGood (this: void, e: Event) {
|
||||
// 这里是无法使用`this`的,因为其为`void`类型
|
||||
console.log('clicked!');
|
||||
}
|
||||
}
|
||||
|
||||
let h = new Handler ();
|
||||
uiElement.addClickListener (h.onClickGood);
|
||||
|
||||
```
|
||||
|
||||
因为`onClickGood`将其`this`类型指定为了`void`,所以传递给`addClickListener`是合法的。当然,这也意味着`onClickGood`不能使用`this.info`了。如既要传递给`addClickListener`又要使用`this.info`,那么就不得不使用一个箭头函数了(箭头函数在创建时捕获`this`,调用时不捕获)。
|
||||
|
||||
```typescript
|
||||
class Handler {
|
||||
info: string;
|
||||
onClickGood = (e: Event) => { this.info = e.message; }
|
||||
}
|
||||
```
|
||||
|
||||
这会起作用,因为箭头函数不对`this`进行捕获,因此总是能够将它们传递给那些期望`this: void`的主调函数。此方法的不足之处在于,对于每个类型处理器对象,一个箭头函数就被创建出来。而对于作为另一方式的对象方法,则是只被创建一次,随后就附着在处理器的原型之上。这些对象方法,在类型处理器的所有对象之间得以共享(The downside is that one arrow function is created per object of type Handler. Methods, on the other hand, are only created once and attached to Handler's prototype. They are shared between all objects of type Handler)。
|
||||
|
||||
## Overloads
|
||||
|
||||
JavaScript本质上是一种甚为动态的语言。基于所传入的参数形状,某单个的JavaScript函数返回不同类型的对象,这种情况并不罕见(JavaScript is inherently a very dynamic language. It's not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in)。
|
||||
|
||||
```typescript
|
||||
let suits = ["hearts", "spades", "clubs", "diamonds"];
|
||||
|
||||
function pickCard(x): any {
|
||||
//
|
||||
//
|
||||
if ( typeof x == "object" ) {
|
||||
let pickedCard = Math.floor (Math.random() * x.length);
|
||||
return pickedCard;
|
||||
}
|
||||
|
||||
//
|
||||
else if (typeof x == "number") {
|
||||
let pickedSuit = Math.floor(x/13);
|
||||
return { suit: suits[pickedSuit], card: x%13 };
|
||||
}
|
||||
}
|
||||
|
||||
let myDeck = [{suit: "diamonds", card: 2}, {suit: "spades", card: 10}, {suit: "hearts", card: 4}];
|
||||
let pickedCard1 = myDeck[pickCard(myDeck)];
|
||||
alert("Card: " + pickedCard1.card + " of " + pickedCard1.suit);
|
||||
|
||||
let pickedCard2 = pickCard(15);
|
||||
alert("Card: " + pickedCard2.card + " of " + pickedCard2.suit);
|
||||
```
|
||||
|
||||
基于用户传入参数,这里的`pickCard`函数将返回两种不同的结果。如果用户传入一个表示扑克牌的对象,那么该函数将抽出一张牌。而如果用户抽取了一张牌,那么这里将告诉他抽取的是那张牌。但怎么来将此逻辑描述给类型系统呢?
|
||||
|
||||
答案就是,以 **过载清单** 的形式,为同一函数提供多个函数类型(The answer is to supply multiple function types for the same function as **a list of overloads**)。下面就来建立一个描述`pickCard`函数接受何种参数,以及返回什么值的过载清单。
|
||||
|
||||
```typescript
|
||||
let suits = ["hearts", "spades", "clubs", "diamonds"];
|
||||
|
||||
function pickCard (x: {suit: string; card: number;} []): number;
|
||||
function pickCard (x: number): {suit: string; card: number;};
|
||||
|
||||
function pickCard (x): any {
|
||||
//
|
||||
//
|
||||
if ( typeof x == "object" ) {
|
||||
let pickedCard = Math.floor (Math.random() * x.length);
|
||||
return pickedCard;
|
||||
}
|
||||
|
||||
//
|
||||
else if (typeof x == "number") {
|
||||
let pickedSuit = Math.floor(x/13);
|
||||
return { suit: suits[pickedSuit], card: x%13 };
|
||||
}
|
||||
}
|
||||
|
||||
let myDeck = [{suit: "diamonds", card: 2}, {suit: "spades", card: 10}, {suit: "hearts", card: 4}];
|
||||
let pickedCard1 = myDeck[pickCard(myDeck)];
|
||||
alert("Card: " + pickedCard1.card + " of " + pickedCard1.suit);
|
||||
|
||||
let pickedCard2 = pickCard(15);
|
||||
alert("Card: " + pickedCard2.card + " of " + pickedCard2.suit);
|
||||
```
|
||||
|
||||
做出此改变后,现在过载就给到了对`pickCard`函数 **有类型检查的调用**( **type-checked calls** )了。
|
||||
|
||||
为了让编译器拾取到正确的类型检查,编译器采取了与JavaScript底层类似的处理。编译器查看过载清单,从首条过载开始尝试以所提供的参数,对函数进行调用。在发现参数与函数类型中的参数类型匹配时,就取用该过载作为正确的过载。因此,就应将那些过载,以最具体到最宽泛的顺序加以排列。
|
||||
|
||||
请注意这里的`function pickCard(x): any`代码,就并非该过载清单的部分了,那么函数`pickCard`就只有两条过载:一个是取得一个对象,另一个是取得一个数字。若以任何其它参数类型调用`pickCard`,都将引发错误。
|
294
06_generics.md
Normal file
294
06_generics.md
Normal file
@ -0,0 +1,294 @@
|
||||
# 泛型(Generics)
|
||||
|
||||
## 简介
|
||||
|
||||
软件工程的一个主要部分,就是有关不仅有着良好定义并具备一致性,而且具备可重用性组件的构建(A major part of software engineering, is building components that not only have well-defined and consistent APIs)。既可处理现今的数据,又能处理往后的数据的组件,对于构建大型软件系统,将带来最灵活的效能。
|
||||
|
||||
在诸如C#与Java这样的程序语言中,它们工具箱中用于可重用组件创建的主要工具之一,就是 *泛型(generics)*,借助于泛型特性,就可以创建出可工作于不同类型,而非单一类型的组件。这就允许用户对组件进行消费,并使用其各自的类型。
|
||||
|
||||
(注:[Wikipedia:泛型](https://en.wikipedia.org/wiki/Generic_programming) )
|
||||
|
||||
## 泛型入门
|
||||
|
||||
这里以泛型特性的“Hello World”开始。下面的`identity`函数将返回任何传递给它的东西。可将其想作与`echo`命令类似。
|
||||
|
||||
在没有泛型特性时,就要么必须给予该`identity`函数某种指定类型:
|
||||
|
||||
```typescript
|
||||
function identity (arg: number): number {
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
或者使用`any`类型类描述该`identity`函数:
|
||||
|
||||
```typescript
|
||||
function identity (arg: any): any {
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
尽管使用`any`具备泛型,因为这样做导致该函数接收任何且所有类型的`arg`,不过实际上丢失了函数返回值时的类型。比如假设传入了一个数字,能得到的信息就仅是可返回任意类型(While using `any` is certainly genric in that it will cause the fucntion to accept any and all types for the type of `any`, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned)。
|
||||
|
||||
取而代之的是,这里需要某种捕获参数类型的方式,通过此方式带注解将返回何种类型。那么这里将使用 *类型变量(type variable)*,类型变量与作用在值上的变量不同,其是一种作用在类型上的变量(Instead, we need a way of capturing the type of the argument in such a way that we can also use it to denote what is being returned. Here, we will use a *type variable*, a special kind of variable that works on types rather than values)。
|
||||
|
||||
```typescript
|
||||
function identity<T> (arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
现在已经给`identity`函数加上了一个类型变量`T`。此`T`允许对用户提供的类型进行捕获(比如:`number`),因此就可以于随后使用该信息。这里再度使用`T`作为返回值类型。在检查时,就可以看到对参数与返回值类型,使用的是同一种类型了。这样做就允许将函数一侧的类型信息,运送到另一侧。
|
||||
|
||||
那么就说此版本的`identity`就是泛型的了,因为其在一系列的类型上都可运作。与使用`any`不同,泛型的使用与上面的第一个对参数与返回值类型都用了数字的`identity`函数同样精确(也就是其并没有丢失任何信息)。
|
||||
|
||||
而一旦写好这个泛型的`identity`函数,就可以两种方式对其进行调用了。第一种方式是将所有参数,包括参数类型,传递给该函数:
|
||||
|
||||
```typescript
|
||||
let output = identity<string>("myString");
|
||||
```
|
||||
|
||||
这里显式地将`T`置为`string`,作为函数调用的参数之一,注意这里使用的`<>`而非`()`进行注记。
|
||||
|
||||
第二种方式,也是最常见的了。就是使用 *类型参数推理(type argument inference)* -- 也就是,让编译器基于传递给它的参数类型,来自动设定`T`的值。
|
||||
|
||||
```typescript
|
||||
let output = identity("myString"); // 输出类型将是 `string`
|
||||
```
|
||||
|
||||
注意这里不必显式地传入尖括号(the angle brackets, `<>`)中的类型;编译器只需查看值`myString`,并将`T`设置为`myString`的类型。尽管类型参数推理在保持代码简短及更具可读性上,能够作为一项有用的工具,但在一些更为复杂的示例中可能发生编译器无法完成类型推理时,仍需像先前的示例那样,显式地传入类型参数,
|
||||
|
||||
## 泛型类型变量的使用(Working with Generic Type Variables)
|
||||
|
||||
在一开始使用泛型时,将注意到在创建诸如`identify`这样的函数时,编译器将强制在函数体中正确地使用任意泛型的类型化参数。那就是说,实际上可将这些参数,像是任意及所有类型那样对待(When you begin to use generics, you'll notice that when you create generic functions like `identity`, the compiler will enforce that you use any generically typed parameters in the body of the function correctly. That is, that you actually treat these parameters as if they could be any and all types)。
|
||||
|
||||
这里仍然以前面的`identity`函数做示例:
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
那么如果在各个调用中要同时记录参数`arg`的长度到控制台会怎样呢?就可能会尝试这样来编写:
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
console.log(arg.length); // Property 'length' does not exist on type 'T'. (2339)
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
这样做的话,编译器将给出一个在成员`arg`上使用`.length`的错误,然而没有那里说过`arg`上有着此成员。请记住,前面已经提及到,这些类型变量代替的是`any`及所有类型,因此使用此函数的某个人可能传入的是一个`number`,而一个`number`显然是没有`.length`成员的。
|
||||
|
||||
这里实际上是要该函数在`T`的数组上操作,而不是在`T`上。而一旦对数组进行操作,那么`.length`成员就可用了。可像下面将创建其它类型的数组那样,对此进行描述:
|
||||
|
||||
```typescript
|
||||
function loggingIdentity<T>(arg: T[]): T[] {
|
||||
console.log(arg.length); // 因为数组有着长度,因此不再发生错误
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
可将`loggingIdentity`的类型,读作“通用函数`loggingIdentity`,获取一个类型参数`T`,以及一个为`T`的数组的参数`arg`,而返回一个`T`的数组”("the generic function `loggingIdentity` takes a type parameter `T`, and an argument `arg` which is an array of `T`s, and returns an array of `T`s")。在将一个数字数组传递进去时,将获取到一个返回的数字数组,同时`T`将绑定到`number`类型。这就允许将这里的泛型变量`T`作为所处理的类型的一部分,而非整个类型,从而带来更大的灵活性(This allows us to use our generic type variable `T` as part of the types we're working with, rather than the whole type, giving us greater flexibility,这里涉及两个类型,泛型`T`及泛型`T`的数组,因此说`T`是处理类型的部分)。
|
||||
|
||||
还可以将同一示例,写成下面这种形式:
|
||||
|
||||
```typescript
|
||||
function loggingIdentity<T>(arg: Array<T>): Array<T> {
|
||||
console.log(arg.length);
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
其它语言中也有此种写法。下一小节,将探讨如何创建自己的诸如`Array<T>`这样的泛型。
|
||||
|
||||
|
||||
## 泛型(Generic Types)
|
||||
|
||||
上一小节中,创建出了通用的、可处理一系列类型的identity函数。本小节中,将就该函数本身的类型,以及如何创建通用接口,进行探索。
|
||||
|
||||
通用函数的类型(the type of generic functions)与非通用函数一样,以所列出的类型参数开始,类似与函数的声明:
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
|
||||
let myIdentity: <T>(arg: T) => T = identity;
|
||||
```
|
||||
|
||||
对于类型中的泛型参数,则可以使用不同的名称,只要与类型变量的数目及类型变量使用顺序一致即可(We could also have used a different name for the generic type parameter in the type, so long as the number of type variables and how the type variables are used line up)。
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
|
||||
let myIdentity: <U>(arg: U) => U = identity;
|
||||
```
|
||||
|
||||
还可以将该泛型写为某对象字面类型的调用签名(a call signature of an object literal type):
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
|
||||
let myIdentity: {<T>(arg: T): T} = identity;
|
||||
```
|
||||
|
||||
这就引入编写首个通用接口(the generic interface)的问题了。这里把上一示例中的对象字面值,改写为接口的形式:
|
||||
|
||||
```typescript
|
||||
interface GenericIdentityFn {
|
||||
<T>(arg: T): T;
|
||||
}
|
||||
|
||||
function identity<T>(arg: T) T {
|
||||
return arg;
|
||||
}
|
||||
|
||||
let myIdentity: GenericIdentityFn = identity;
|
||||
```
|
||||
|
||||
在类似示例中,可能想要将通用参数,修改为整个接口的一个参数。这样做可获悉是对那些类型进行泛型处理(比如,是`Dictionary<string>`而不只是`Dictionary`)。这样处理可将类型参数暴露给该接口的其它成员(In a similar example, we may want to move the generic parameter to be a parameter of the whole interface. This lets us see what type(s) we're generic over(e.g. `Dictionary<string>` rather than just `Dictionary`). This makes the type parameter visible to all the other members of the interface)。
|
||||
|
||||
```typescript
|
||||
interface GenericIdentityFn<T> {
|
||||
(arg: T): T;
|
||||
}
|
||||
|
||||
function identity<T>(arg: T) T {
|
||||
return arg;
|
||||
}
|
||||
|
||||
let myIdentity: GenericIdentityFn<number> = identity;
|
||||
```
|
||||
|
||||
请注意这里的示例已被修改为有一点点的不同了。这里有了一个作为泛型一部分的非通用函数,取代了对一个通用函数的描述。现在使用`GenericIdentityFn`时,就需要明确指明一个对应的类型参数了(这里是`number`),从而有效锁定当前调用签名所具体使用的类型。掌握何时将类型参数直接放在调用签名上,以及何时将其放在接口本身上,对于阐明泛型的各个方面是有帮助的(Instead of describing a generic function, we now have a non-generic function signature that is a part of a generic type. When we use `GenericIdentityFn`, we now will also need to specify the corresponding type argument(here: `number`), effectively locking in what the underlying call signature will use. Understanding when to put the type parameter directly on the call signature and when to put it on the interface itself will be helpful in describing what aspects of a type are generic)。
|
||||
|
||||
除开通用接口,还可以创建通用类。但请注意是不能创建通用枚举与命名空间的。
|
||||
|
||||
## 通用类(Generic Classes)
|
||||
|
||||
通用类与通用接口有着类似外观。通用类在类名称之后,有着一个于尖括号(`<>`)中所列出的泛型参数清单(A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list in angle brackets(`<>`) following the name of the class)。
|
||||
|
||||
```typescript
|
||||
class GenericNumber<T> {
|
||||
zeroValue: T;
|
||||
add: (x: T, y: T) => T;
|
||||
}
|
||||
|
||||
let myGenericNumber = new GenericNumber<number>();
|
||||
myGenericNumber.zeroValue = 0;
|
||||
myGenericNumber.add = function (x, y) {return x+y;};
|
||||
```
|
||||
|
||||
这是对`GenericNumber`类的相当直观的用法了,不过可能会注意到这里并没有限制该类仅使用`number`类型。因此可以使用`string`甚至更复杂的JavaScript对象。
|
||||
|
||||
```typescript
|
||||
let stringNumeric = new GenericNumber<string>();
|
||||
|
||||
stringNumeric.zeroValue = "";
|
||||
stringNumeric.add = function (x, y) { return x + y; };
|
||||
|
||||
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
|
||||
```
|
||||
|
||||
与接口一样,将类型参数放在类本身上,可确保该类的所有属性,都与同一类型进行运作。
|
||||
|
||||
如同在[类部分](03_classes.md)所讲到的,类在其类型上有两侧:静态侧与示例侧。通用类则仅在示例侧是通用的,静态侧不具有通用性,因此在使用类时,静态成员无法使用到类的类型参数。
|
||||
|
||||
## 泛型约束(Generic Constraints)
|
||||
|
||||
如还记得早先的一个示例,有时候在了解到某些类型集所具备的功能时,而想要编写一个处理类型集的通用函数。在示例`loggingIdentity`中,是打算能够访问到`arg`的`length`属性,但编译器却无法证实每个类型都有`length`属性,因此它就警告无法做出此种假定。
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
console.log(arg.length); // Property 'length' does not exist on type 'T'. (2339)
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
为了避免处理任意与所有类型,这里就要将该函数约束为处理有着`length`属性的任意及所有类型。只要类型具有该成员,这里允许该类型,但仍要求该类型至少具备该属性。为了达到这个目的,就必须将这里的要求,作为`T`可以是何种类型的一个约束加以列出。
|
||||
|
||||
做法就是,创建出一个描述约束的接口。下面将创建一个具有单一`.length`的接口,并使用该接口及`extends`语句,来表示这里的约束:
|
||||
|
||||
```typescript
|
||||
interface Lengthwise {
|
||||
length: number;
|
||||
}
|
||||
|
||||
function loggingIdentity<T extends Lengthwise>(arg: T): T {
|
||||
console.log(arg.length); // 现在知道`arg`有着一个`.length`属性,因此不再报出错误
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
因为该通用函数现在已被约束,故其不再对任意及所有类型运作:
|
||||
|
||||
```typescript
|
||||
loggingIdentity(3); // 错误,数字没有`.length`属性
|
||||
```
|
||||
|
||||
相反,这里需传入那些具有全部所需属性类型的值:
|
||||
|
||||
```typescript
|
||||
loggingIdentity({length: 10; value: 3});
|
||||
```
|
||||
|
||||
### 在泛型约束中使用类型参数(Using Type Parameter in Generic Constraints)
|
||||
|
||||
定义一个受其它类型参数约束的类型参数,也是可以的。比如这里要从一个对象,经由属性名称而获取到某个属性。肯定是要确保不会偶然去获取某个并不存在于该`obj`上的属性,因此就将在两个类型上,加上一条约束(You can declare a type parameter that is constrained by another type parameter. For example, here we'd like to get a property from an object given its name. We'd like to ensure that we're not accidentally grabbing a property that does not exist on the `obj`, so we'll place a constraint between the two types):
|
||||
|
||||
```typescript
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K) {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
let x = { a: 1, b: 2, c: 3, d: 4 };
|
||||
|
||||
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'. (2345)
|
||||
```
|
||||
|
||||
### 在泛型中使用类类型(Using Class Types in Generics)
|
||||
|
||||
在运用泛型来创建TypeScript的工厂(工厂是一种面向对象编程的设计模式,参见[Design patterns in TypeScript: Factory](https://thedulinreport.com/2017/07/30/design-patters-in-typescript-factory/), [oodesign.com: Factory Pattern](http://www.oodesign.com/factory-pattern.html))时,有必要通过类的构造函数,对类的类型加以引用(When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions)。比如:
|
||||
|
||||
```typescript
|
||||
function create<T>(c: { new(): T; }): T {
|
||||
return new c();
|
||||
}
|
||||
```
|
||||
|
||||
下面是一个更为复杂的示例,其使用了原型属性,来推断及约束构造函数与类的类型实例侧之间的关系(A more advanced example uses the prototype property to infer and constrain relationships between the constructor function and the instance side of class types)。
|
||||
|
||||
```typescript
|
||||
class BeeKeeper {
|
||||
hasMask: boolean;
|
||||
}
|
||||
|
||||
class ZooKeeper {
|
||||
nametag: string;
|
||||
}
|
||||
|
||||
class Animal {
|
||||
numLegs: number;
|
||||
}
|
||||
|
||||
class Bee extends Animal {
|
||||
keeper: BeeKeeper;
|
||||
}
|
||||
|
||||
class Lion extends Animal {
|
||||
keeper: ZooKeeper;
|
||||
}
|
||||
|
||||
function createInstance<A extends Animal>(c: new () => A): A {
|
||||
return new c();
|
||||
}
|
||||
|
||||
createInstance(Lion).keeper.nametag; // 类型检查, Cannot read property 'nametag' of undefined
|
||||
createInstance(Bee).keeper.hasMask;
|
||||
```
|
62
README.md
62
README.md
@ -1 +1,61 @@
|
||||
# ts-learnings
|
||||
# ECMAScript 2015(ES6) 学习记录
|
||||
|
||||
ECMAScript 2015 (ES6)已经正式发布,所有浏览器均已支持,同时许多项目,如Angular, Ionic/Electron框架等,均已在往ES6迁移。故需要学习掌握这一新版的Javascript。
|
||||
|
||||
## ES6与 Javascript
|
||||
|
||||
ES6仍然是Javascript, 只不过是在我们已经熟悉的Javascript上加入了一些新的东西。使得Javascript更为强大,可以应对大型程序的要求。
|
||||
|
||||
## ES6的实现
|
||||
|
||||
ES6只是新一代Javascript的规范,几大公司、各个浏览器引擎等都有具体的实现。微软的TypeScript、CoffeeScript等都是ES6的具体实现。
|
||||
|
||||
参考链接:
|
||||
|
||||
- https://blog.mariusschulz.com/2017/01/13/typescript-vs-flow
|
||||
- http://blog.ionicframework.com/ionic-and-typescript-part-1/
|
||||
|
||||
鉴于Angular与Ionic都是使用了微软的TypeScript, 因此在学习ES6时,将学习TypeScript这一实现。
|
||||
|
||||
## 关于TypeScript
|
||||
|
||||
TypeScript是Javascript的超集,有着以下优势:
|
||||
|
||||
- 可选的静态类型(关键就是这里的“可选”, Optional static typing, the key here is optional)
|
||||
- 类型推理,此特性在并没有使用到类型的情况下,带来那些类型的诸多益处(Type Inference, which gives some of the benefits of types, without actually using them)
|
||||
- 可在主流浏览器尚未对ES6/ES7提供支持之前,通过TypeScript用上ES6及ES7的特性
|
||||
- TypeScript有着将程序向下编译到所有浏览器都支持的某个Javascript版本的能力
|
||||
- IntelliSense提供了极好的工具支持
|
||||
|
||||
因为TypeScript带给如你一样的开发者这些不错的特性及巨大优势,Ionic是以TypeScript编写的,而不是ES6(这里就表明了**TypeScript并不是ES6**)。
|
||||
|
||||
### 关于可选的静态类型
|
||||
|
||||
可能TypeScript最能打动人心的,就是其所提供到的可选静态类型系统了。将给变量、函数、属性等加上类型。这将帮到编译器,且在app尚未运行时,就给出有关代码中任何潜在错误的警告。在使用到库及框架时,类型也有帮助,这是由于类型可令到开发者准确知悉那些APIs期望何种类型的数据。而关于类型系统,你首先要记住的是它是可选的。TypeScript并不强制要求开发者在他们不想添加的上必须添加类型。但随着应用变得越来越大、越来越复杂,类型确实可以提供到一些很棒的优势。
|
||||
|
||||
关于 IntelliSense:
|
||||
|
||||
> 一种 Microsoft 技术,这种技术通过在光标悬停在函数上时显示类定义和注释,从而让您可以分析源代码。当您在 IDE 中键入函数名时,IntelliSense 还可以完成这些名称。
|
||||
|
||||
TypeScript的一大优势,就是其代码补全与IntelliSense了。IntelliSense在敲入代码时,提供有用的提示。因为Ionic本身就是用TypeScript写就的,代码编辑器就可以展示出所有可用的方法,以及这些方法所期望的参数。当今所有最好的集成开发环境,比如VScode、Atom、Sublime text,甚至那些诸如Vim/Neovim等命令行的编辑器,都有对代码补全的支持。
|
||||
|
||||
TypeScript的许多优势,带来了一种好得多的app开发体验。因此,Ionic将全力压注到TypeScript上,而不提供ES6的启动器。
|
||||
|
||||
摘录自:
|
||||
|
||||
> [TypeScript的优势](https://ionicframework.com/docs/developer-resources/typescript/)
|
||||
|
||||
## 本教程特色
|
||||
|
||||
针对新特性的详细讨论,并与与实例代码结合。TypeScript是在Javascript的基础上,引入了诸多新特性,本教程将逐一讨论这些新特性,并同时编写相应代码加以验证。
|
||||
|
||||
## 捐助此教程
|
||||
|
||||
支付宝:
|
||||
|
||||
![Alipay: laxer@gmail.com](images/a6x09981lks9yco3b8xcqf0.png "alipay:laxers@gmail.com")
|
||||
|
||||
Bitcoin:
|
||||
|
||||
|
||||
![bitcoin: ](images/btc-qrcode.png "")
|
||||
|
8
SUMMARY.md
Normal file
8
SUMMARY.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Summary
|
||||
|
||||
* [基本数据类型](01_basic_data_types.md)
|
||||
* [变量声明](02_variables_declaration.md)
|
||||
* [类](03_classes.md)
|
||||
* [接口](04_interfaces.md)
|
||||
* [函数](05_functions.md)
|
||||
* [泛型](06_generics.md)
|
13
git-push.sh
Executable file
13
git-push.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
git add .
|
||||
|
||||
if [ "$1" = "" ]
|
||||
then
|
||||
echo "没有commit -m的输入,请输入commit -m内容,以[ENTER]结束:"
|
||||
read msg
|
||||
git commit -m "$msg"
|
||||
git push
|
||||
else
|
||||
git commit -m "$1"
|
||||
git push
|
||||
fi
|
35
gulpfile.js
Normal file
35
gulpfile.js
Normal file
@ -0,0 +1,35 @@
|
||||
const gulp = require('gulp'),
|
||||
browserify = require('browserify'),
|
||||
source = require('vinyl-source-stream'),
|
||||
watchify = require('watchify'),
|
||||
tsify = require('tsify'),
|
||||
gutil = require('gulp-util');
|
||||
|
||||
let paths = {
|
||||
pages: ["src/*.html"]
|
||||
};
|
||||
|
||||
let watchedBrowserify = watchify(browserify({
|
||||
basedir: '.',
|
||||
debug: true,
|
||||
entries: ["src/main.ts"],
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
// tsify 是browserify的插件,用于编译 TypeScript, 选项写在后面
|
||||
}).plugin(tsify, { noImplicitAny: true }));
|
||||
|
||||
gulp.task("copy-html", ()=>{
|
||||
return gulp.src(paths.pages)
|
||||
.pipe(gulp.dest("dist"))
|
||||
});
|
||||
|
||||
function bundle () {
|
||||
return watchedBrowserify
|
||||
.bundle()
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe(gulp.dest("dist"))
|
||||
}
|
||||
|
||||
gulp.task('default', ['copy-html'], bundle);
|
||||
watchedBrowserify.on("update", bundle);
|
||||
watchedBrowserify.on("log", gutil.log);
|
BIN
images/a6x09981lks9yco3b8xcqf0.png
Normal file
BIN
images/a6x09981lks9yco3b8xcqf0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
images/btc-qrcode.png
Normal file
BIN
images/btc-qrcode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
4551
package-lock.json
generated
Normal file
4551
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "typescript-learnings",
|
||||
"version": "0.1.0",
|
||||
"description": "TypeScript Learning stuffs.",
|
||||
"main": "/dist/main.js",
|
||||
"scripts": {
|
||||
"start": "live-server dist/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"git-push": "./push.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gnu4cn/typescript-learnings.git"
|
||||
},
|
||||
"keywords": [
|
||||
"TypeScript"
|
||||
],
|
||||
"author": "Peng Hailin, unisko@gmail.com",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/gnu4cn/typescript-learnings/issues"
|
||||
},
|
||||
"homepage": "https://github.com/gnu4cn/typescript-learnings#readme",
|
||||
"devDependencies": {
|
||||
"browserify": "^14.5.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-typescript": "^3.2.3",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-util": "^3.0.8",
|
||||
"live-server": "^1.2.0",
|
||||
"tsify": "^3.0.3",
|
||||
"typescript": "^2.6.2",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.9.0"
|
||||
}
|
||||
}
|
6
src/greet.js
Normal file
6
src/greet.js
Normal file
@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
exports.__esModule = true;
|
||||
function sayHello(name) {
|
||||
return "Hello from " + name;
|
||||
}
|
||||
exports.sayHello = sayHello;
|
5
src/greet.ts
Normal file
5
src/greet.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function sayHello(name: string) {
|
||||
|
||||
return `Hello from ${name}`;
|
||||
|
||||
}
|
13
src/index.html
Normal file
13
src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>测试...</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3 id="greeting">Loading...</h3>
|
||||
</body>
|
||||
|
||||
<script src="./bundle.js"></script>
|
||||
</html>
|
||||
|
48
src/main.ts
Normal file
48
src/main.ts
Normal file
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
interface Lengthwise {
|
||||
length: number;
|
||||
}
|
||||
|
||||
function loggingIdentity<T extends Lengthwise>(arg: T): T {
|
||||
console.log(arg.length); // 现在知道`arg`有着一个`.length`属性,因此不再报出错误
|
||||
return arg;
|
||||
}
|
||||
|
||||
loggingIdentity("test");
|
||||
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K) {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
let x = { a: 1, b: 2, c: 3, d: 4 };
|
||||
|
||||
console.log(getProperty(x, "a")); // 没有问题
|
||||
|
||||
|
||||
class BeeKeeper {
|
||||
hasMask: boolean;
|
||||
}
|
||||
|
||||
class ZooKeeper {
|
||||
nametag: string;
|
||||
}
|
||||
|
||||
class Animal {
|
||||
numLegs: number;
|
||||
}
|
||||
|
||||
class Bee extends Animal {
|
||||
keeper: BeeKeeper;
|
||||
}
|
||||
|
||||
class Lion extends Animal {
|
||||
keeper: ZooKeeper;
|
||||
}
|
||||
|
||||
function createInstance<A extends Animal>(c: new () => A): A {
|
||||
return new c();
|
||||
}
|
||||
|
||||
createInstance(Lion).keeper.nametag; //
|
||||
createInstance(Bee).keeper.hasMask;
|
||||
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
"target": "es5",
|
||||
"outDir": "dist/"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user