├── basics ├── README.md ├── class-and-interfaces.md ├── basic-types.md ├── interfaces.md ├── generics.md ├── functions.md └── classes.md ├── advanced ├── README.md ├── symbols.md ├── iterators-and-generators.md ├── declaration-merging.md ├── basic-types.md ├── decorators.md ├── variable-declarations.md └── declaration-files.md ├── .gitignore ├── README.md ├── introduction ├── what-is-typescript.md ├── get-typescript.md └── hello-typescript.md ├── SUMMARY.md ├── package.json ├── SUMMARY copy.md └── book.json /basics/README.md: -------------------------------------------------------------------------------- 1 | # 基础 -------------------------------------------------------------------------------- /advanced/README.md: -------------------------------------------------------------------------------- 1 | # 高级 2 | ## 基础类型 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | node_modules 3 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | > 本小册是《千锋大前端小册》系列之 TypeScript 部分。通过本小册,可以系统学习TypeScript基础知识,为将来TypeScript在大前端项目的应用打下坚实的基础。***—— 作者:古艺散人*** -------------------------------------------------------------------------------- /introduction/what-is-typescript.md: -------------------------------------------------------------------------------- 1 | # 什么是TypeScript 2 | 3 | TypeScript是Microsoft公司注册商标。 4 | 5 | TypeScript具有类型系统,且是JavaScript的超集。 它可以编译成普通的JavaScript代码。 TypeScript支持任意浏览器,任意环境,任意系统并且是开源的。 -------------------------------------------------------------------------------- /introduction/get-typescript.md: -------------------------------------------------------------------------------- 1 | # 安装TypeScript 2 | ## 1、在全局环境里安装TS 3 | ``` 4 | npm install -g typescript 5 | ``` 6 | 7 | ## 2、用 tsc 命令编译 .ts 文件 8 | app.ts 文件: 9 | ``` 10 | let title: string = '千锋教育' 11 | ``` 12 | 在命令行里输入以下命令都可以将.ts文件编译为.js文件: 13 | ``` 14 | tsc ./src/app.ts --outFile ./dist/app.js 15 | tsc ./src/* --outDir ./dist --watch 16 | ``` 17 | 18 | ## 3、tsconfig.json 配置文件 19 | 在命令行里输入 `tsc --init`命令,创建一个 tsconfig.json 文件,在此配置文件里修改: 20 | ``` 21 | "outDir": "./dist", 22 | "rootDir": "./src" 23 | ``` -------------------------------------------------------------------------------- /advanced/symbols.md: -------------------------------------------------------------------------------- 1 | # Symbols 2 | 3 | 自ECMAScript 2015起,symbol成为了一种新的原生类型,就像number和string一样。 4 | symbol类型的值是通过Symbol构造函数创建的。 5 | 6 | ``` 7 | let sym1 = Symbol(); 8 | let sym2 = Symbol("key"); // 可选的字符串key 9 | ``` 10 | 11 | Symbols是不可改变且唯一的。 12 | 13 | ``` 14 | let sym2 = Symbol("key") 15 | let sym3 = Symbol("key") 16 | 17 | sym2 === sym3 // false, symbols是唯一的 18 | ``` 19 | 20 | 像字符串一样,symbols也可以被用做对象属性的键。 21 | 22 | ``` 23 | let sym = Symbol() 24 | 25 | let obj = { 26 | [sym]: "value" 27 | } 28 | 29 | console.log(obj[sym]) // "value" 30 | ``` 31 | 32 | Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。 33 | 34 | ``` 35 | const getClassNameSymbol = Symbol() 36 | 37 | class C { 38 | [getClassNameSymbol](){ 39 | return "C" 40 | } 41 | } 42 | 43 | let c = new C() 44 | let className = c[getClassNameSymbol]() // "C" 45 | ``` -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [前言](README.md) 4 | 5 | - [简介](introduction/README.md) 6 | - [什么是 TypeScript](introduction/what-is-typescript.md) 7 | - [安装 TypeScript](introduction/get-typescript.md) 8 | - [5分钟了解 TypeScript](introduction/hello-typescript.md) 9 | 10 | - [基础](basics/README.md) 11 | - [基础类型-基础](basics/basic-types.md) 12 | - [函数](basics/functions.md) 13 | - [接口](basics/interfaces.md) 14 | - [类](basics/classes.md) 15 | - [类和接口](basics/class-and-interfaces.md) 16 | - [泛型](basics/generics.md) 17 | 18 | - [进阶](advanced/README.md) 19 | - [基础类型-高级](advanced/basic-types.md) 20 | - [Symbols](advanced/symbols.md) 21 | - [迭代器和生成器](advanced/iterators-and-generators.md) 22 | - [装饰器](advanced/decorators.md) 23 | - [声明文件](advanced/declaration-files.md) 24 | - [声明合并](advanced/declaration-merging.md) 25 | - [变量声明](advanced/variable-declarations.md) 26 | 27 | - [感谢](thanks/README.md) -------------------------------------------------------------------------------- /advanced/iterators-and-generators.md: -------------------------------------------------------------------------------- 1 | # 迭代器和生成器 2 | ## 可迭代性 3 | ### for..of 语句 4 | for..of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。 下面是在数组上使用 for..of的简单例子: 5 | 6 | ``` 7 | let someArray = [1, "string", false] 8 | 9 | for (let entry of someArray) { 10 | console.log(entry) // 1, "string", false 11 | } 12 | ``` 13 | 14 | ### for..of vs. for..in 语句 15 | for..of和for..in均可迭代一个列表;但是用于迭代的值却不同,for..in迭代的是对象的键的列表,而for..of则迭代对象的键对应的值。 16 | 17 | 下面的例子展示了两者之间的区别: 18 | 19 | ``` 20 | let list = [4, 5, 6] 21 | 22 | for (let i in list) { 23 | console.log(i) // "0", "1", "2", 24 | } 25 | 26 | for (let i of list) { 27 | console.log(i) // "4", "5", "6" 28 | } 29 | ``` 30 | 31 | 另一个区别是for..in可以操作任何对象, 它提供了查看对象属性的一种方法。 但是 for..of关注于迭代对象的值。内置对象Map和Set已经实现了Symbol.iterator方法,让我们可以访问它们保存的值。 32 | 33 | ``` 34 | let pets = new Set(["Cat", "Dog", "Hamster"]); 35 | pets["species"] = "mammals" 36 | 37 | for (let pet in pets) { 38 | console.log(pet) // "species" 39 | } 40 | 41 | for (let pet of pets) { 42 | console.log(pet) // "Cat", "Dog", "Hamster" 43 | } 44 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "description": "> 本小册是《千锋大前端小册》系列之 TypeScript 部分。通过本小册,可以系统学习TypeScript基础知识,为将来TypeScript在大前端项目的应用打下坚实的基础。***—— 作者:古艺散人***", 5 | "main": "index.js", 6 | "dependencies": { 7 | "gitbook-plugin-anchor-navigation-ex": "^1.0.14", 8 | "gitbook-plugin-anchors": "^0.7.1", 9 | "gitbook-plugin-donate": "^1.0.2", 10 | "gitbook-plugin-expandable-chapters-small": "^0.1.7", 11 | "gitbook-plugin-favicon": "^0.0.2", 12 | "gitbook-plugin-github": "^2.0.0", 13 | "gitbook-plugin-github-buttons": "^3.0.0", 14 | "gitbook-plugin-sharing-plus": "^0.0.2", 15 | "gitbook-plugin-splitter": "^0.0.8" 16 | }, 17 | "devDependencies": {}, 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/lurongtao/felixbooks-typescript.git" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/lurongtao/felixbooks-typescript/issues" 30 | }, 31 | "homepage": "https://github.com/lurongtao/felixbooks-typescript#readme" 32 | } 33 | -------------------------------------------------------------------------------- /SUMMARY copy.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [前言](README.md) 4 | - [简介](introduction/README.md) 5 | - [什么是 TypeScript](introduction/what-is-typescript.md) 6 | - [安装 TypeScript](introduction/get-typescript.md) 7 | - [5分钟了解 TypeScript](introduction/hello-typescript.md) 8 | - [基础](basics/README.md) 9 | - [基础类型](basics/basic-types.md) 10 | - [函数](basics/functions.md) 11 | - [接口](basics/interfaces.md) 12 | - [类](basics/classes.md) 13 | - [类和接口](basics/class-and-interfaces.md) 14 | - [泛型](basics/generics.md) 15 | - [进阶](advanced/README.md) 16 | - [基础类型](advanced/basic-types.md) 17 | - [变量声明](advanced/variable-declarations.md) 18 | - [枚举](basics/enums.md) 19 | - [类型推论](basics/type-inference.md) 20 | - [类型兼容性](basics/type-compatibility.md) 21 | - [高级类型](basics/advanced-types.md) 22 | - [Symbols](basics/symbols.md) 23 | - [迭代器和生成器](basics/iterators-and-generators.md) 24 | - [模块](basics/modules.md) 25 | - [命名空间](basics/namespaces.md) 26 | - [命名空间和模块](basics/namespaces-and-modules.md) 27 | - [模块解析](basics/module-resolution.md) 28 | - [声明合并](basics/declaration-merging.md) 29 | - [JSX](basics/jsx.md) 30 | - [装饰器](basics/decorators.md) 31 | - [Mixins](basics/mixins.md) 32 | - [三斜线指令](basics/type-slash-directives.md) 33 | - [JavaScript类型检查](basics/type-checking-javascript-files.md) 34 | - [工具类型](basics/utility-types.md) 35 | 36 | - [变量声明](advanced/variable-declarations.md) 37 | - [声明文件](advanced/declaration-files.md) 38 | 39 | - [声明文件](engineering/README.md) 40 | 41 | - [感谢](thanks/README.md) -------------------------------------------------------------------------------- /advanced/declaration-merging.md: -------------------------------------------------------------------------------- 1 | # 声明合并 2 | 3 | 如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型: 4 | 5 | ## 函数的合并 6 | 7 | 之前学习过重载,我们可以使用重载定义多个函数类型: 8 | 9 | ``` 10 | function reverse(x: number): number 11 | function reverse(x: string): string 12 | function reverse(x: number | string): number | string { 13 | if (typeof x === 'number') { 14 | return Number(x.toString().split('').reverse().join('')) 15 | } else if (typeof x === 'string') { 16 | return x.split('').reverse().join('') 17 | } 18 | } 19 | ``` 20 | 21 | ## 接口的合并 22 | 23 | 接口中的属性在合并时会简单的合并到一个接口中: 24 | 25 | ``` 26 | interface Alarm { 27 | price: number 28 | } 29 | interface Alarm { 30 | weight: number 31 | } 32 | ``` 33 | 34 | 相当于: 35 | 36 | ``` 37 | interface Alarm { 38 | price: number 39 | weight: number 40 | } 41 | ``` 42 | 43 | 注意,**合并的属性的类型必须是唯一的**: 44 | 45 | ``` 46 | interface Alarm { 47 | price: number 48 | } 49 | interface Alarm { 50 | price: number // 虽然重复了,但是类型都是 `number`,所以不会报错 51 | weight: number 52 | } 53 | ``` 54 | 55 | ``` 56 | interface Alarm { 57 | price: number 58 | } 59 | interface Alarm { 60 | price: string // 类型不一致,会报错 61 | weight: number 62 | } 63 | 64 | // index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type. Variable 'price' must be of type 'number', but here has type 'string'. 65 | ``` 66 | 67 | 接口中方法的合并,与函数的合并一样: 68 | 69 | ``` 70 | interface Alarm { 71 | price: number 72 | alert(s: string): string 73 | } 74 | interface Alarm { 75 | weight: number 76 | alert(s: string, n: number): string 77 | } 78 | ``` 79 | 80 | 相当于: 81 | 82 | ``` 83 | interface Alarm { 84 | price: number 85 | weight: number 86 | alert(s: string): string 87 | alert(s: string, n: number): string 88 | } 89 | ``` 90 | 91 | ## 类的合并 92 | 93 | 类的合并与接口的合并规则一致。 -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "千锋大前端小册-TypeScript", 3 | "author": "古艺散人", 4 | "description": "TypeScript 入门教程", 5 | "language": "zh-hans", 6 | "gitbook": "3.2.3", 7 | "styles": { 8 | "website": "./styles/website.css" 9 | }, 10 | "structure": { 11 | "readme": "README.md" 12 | }, 13 | "links": { 14 | 15 | }, 16 | "plugins": [ 17 | "-sharing", 18 | "splitter", 19 | "expandable-chapters-small", 20 | "anchors", 21 | "github", 22 | "github-buttons", 23 | "donate", 24 | "sharing-plus", 25 | "anchor-navigation-ex", 26 | "favicon" 27 | ], 28 | "pluginsConfig": { 29 | "github": { 30 | "url": "https://github.com/lurongtao/felixbooks-typescript" 31 | }, 32 | "github-buttons": { 33 | "buttons": [{ 34 | "user": "lurongtao", 35 | "repo": "felixbooks-typescript", 36 | "type": "star", 37 | "size": "small", 38 | "count": true 39 | } 40 | ] 41 | }, 42 | "donate": { 43 | "alipay": "./source/images/donate.png", 44 | "title": "", 45 | "button": "赞赏", 46 | "alipayText": " " 47 | }, 48 | "sharing": { 49 | "douban": false, 50 | "facebook": false, 51 | "google": false, 52 | "hatenaBookmark": false, 53 | "instapaper": false, 54 | "line": false, 55 | "linkedin": false, 56 | "messenger": false, 57 | "pocket": false, 58 | "qq": false, 59 | "qzone": false, 60 | "stumbleupon": false, 61 | "twitter": false, 62 | "viber": false, 63 | "vk": false, 64 | "weibo": false, 65 | "whatsapp": false, 66 | "all": [ 67 | "google", "facebook", "weibo", "twitter", 68 | "qq", "qzone", "linkedin", "pocket" 69 | ] 70 | }, 71 | "anchor-navigation-ex": { 72 | "showLevel": false 73 | }, 74 | "favicon":{ 75 | "shortcut": "./source/images/favicon.jpg", 76 | "bookmark": "./source/images/favicon.jpg", 77 | "appleTouch": "./source/images/apple-touch-icon.jpg", 78 | "appleTouchMore": { 79 | "120x120": "./source/images/apple-touch-icon.jpg", 80 | "180x180": "./source/images/apple-touch-icon.jpg" 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /basics/class-and-interfaces.md: -------------------------------------------------------------------------------- 1 | # 类与接口 2 | 3 | [之前学习过](./interfaces.md),接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。 4 | 5 | 这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。 6 | 7 | ## 类实现接口 8 | 9 | 实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 `implements` 关键字来实现。这个特性大大提高了面向对象的灵活性。 10 | 11 | 举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它: 12 | 13 | ```ts 14 | interface Alarm { 15 | alert() 16 | } 17 | 18 | class Door { 19 | } 20 | 21 | class SecurityDoor extends Door implements Alarm { 22 | alert() { 23 | console.log('SecurityDoor alert') 24 | } 25 | } 26 | 27 | class Car implements Alarm { 28 | alert() { 29 | console.log('Car alert') 30 | } 31 | } 32 | ``` 33 | 34 | 一个类可以实现多个接口: 35 | 36 | ```ts 37 | interface Alarm { 38 | alert() 39 | } 40 | 41 | interface Light { 42 | lightOn() 43 | lightOff() 44 | } 45 | 46 | class Car implements Alarm, Light { 47 | alert() { 48 | console.log('Car alert') 49 | } 50 | lightOn() { 51 | console.log('Car light on') 52 | } 53 | lightOff() { 54 | console.log('Car light off') 55 | } 56 | } 57 | ``` 58 | 59 | 上例中,`Car` 实现了 `Alarm` 和 `Light` 接口,既能报警,也能开关车灯。 60 | 61 | ## 接口继承接口 62 | 63 | 接口与接口之间可以是继承关系: 64 | 65 | ```ts 66 | interface Alarm { 67 | alert() 68 | } 69 | 70 | interface LightableAlarm extends Alarm { 71 | lightOn() 72 | lightOff() 73 | } 74 | ``` 75 | 76 | 上例中,我们使用 `extends` 使 `LightableAlarm` 继承 `Alarm`。 77 | 78 | ## 接口继承类 79 | 80 | 接口也可以继承类: 81 | 82 | ```ts 83 | class Point { 84 | x: number 85 | y: number 86 | } 87 | 88 | interface Point3d extends Point { 89 | z: number 90 | } 91 | 92 | let point3d: Point3d = {x: 1, y: 2, z: 3} 93 | ``` 94 | 95 | ## 混合类型 96 | 97 | 之前学习过,可以使用接口的方式来定义一个函数需要符合的形状: 98 | 99 | ```ts 100 | interface SearchFunc { 101 | (source: string, subString: string): boolean 102 | } 103 | 104 | let mySearch: SearchFunc 105 | mySearch = function(source: string, subString: string) { 106 | return source.search(subString) !== -1 107 | } 108 | ``` -------------------------------------------------------------------------------- /introduction/hello-typescript.md: -------------------------------------------------------------------------------- 1 | # 5分钟了解TypeScript 2 | 3 | ## 构建第一个TypeScript文件 4 | 在编辑器,将下面的代码输入到 ***src/greeter.ts*** 文件里。我们注意到 ***person: string***,表示 string 是 person 函数的参数类型注解: 5 | ``` 6 | function greeter(person: string) { 7 | return "Hello, " + person 8 | } 9 | 10 | let user = "Jane User" 11 | console.log(greeter(user)) 12 | ``` 13 | 14 | ## 类型注解 15 | TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望 greeter函数接收一个字符串参数。 然后尝试把 greeter的调用改成传入一个数组: 16 | ``` 17 | function greeter(person: string) { 18 | return "Hello, " + person 19 | } 20 | 21 | let user = [0, 1, 2] 22 | console.log(greeter(user)) 23 | ``` 24 | 重新编译,你会看到产生了一个错误: 25 | ``` 26 | Argument of type 'number[]' is not assignable to parameter of type 'string'. 27 | ``` 28 | 类似地,尝试删除greeter调用的所有参数。 TypeScript会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。 29 | 30 | 要注意的是尽管有错误,greeter.js文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。 31 | 32 | ## 接口 33 | 让我们开发这个示例应用。这里我们使用接口来描述一个拥有firstName和lastName字段的对象。 在TypeScript里,只在两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements语句。 34 | ``` 35 | interface Person { 36 | firstName: string 37 | lastName: string 38 | } 39 | 40 | function greeter(person: Person) { 41 | return "Hello, " + person.firstName + " " + person.lastName 42 | } 43 | 44 | let user = { firstName: "Jane", lastName: "User" } 45 | console.log(greeter(user)) 46 | ``` 47 | 48 | ## 类 49 | 最后,让我们使用类来改写这个例子。 TypeScript支持JavaScript的新特性,比如支持基于类的面向对象编程。让我们创建一个Student类,它带有一个构造函数和一些公共字段。 注意类和接口可以一起工作。 50 | 51 | 还要注意的是,在构造函数的参数上使用public等同于创建了同名的成员变量。 52 | > ***注:***public修饰符会引发 `Parameter 'firstName' implicitly has an 'any' type.`,解决方法是在`tsconfig.json`文件中,添加`"noImplicitAny": false`,或者将`"strict": true`,改为`"strict": false` 53 | 54 | ``` 55 | class Student { 56 | fullName: string 57 | constructor(public firstName, public middleInitial, public lastName) { 58 | this.fullName = firstName + " " + middleInitial + " " + lastName 59 | } 60 | } 61 | 62 | interface Person { 63 | firstName: string 64 | lastName: string 65 | } 66 | 67 | function greeter(person: Person) { 68 | return "Hello, " + person.firstName + " " + person.lastName 69 | } 70 | 71 | let user = new Student("Jane", "M.", "User") 72 | console.log(greeter(user)) 73 | ``` -------------------------------------------------------------------------------- /advanced/basic-types.md: -------------------------------------------------------------------------------- 1 | # 基础类型-高级 2 | ## 类型推断 3 | 如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。 4 | ### 什么是类型推断 5 | 以下代码虽然没有指定类型,但是会在编译的时候报错: 6 | ``` 7 | let lunarDay = '初一' 8 | lunarDay = 1 9 | // Type '1' is not assignable to type 'string'. 10 | ``` 11 | 事实上,它等价于: 12 | ``` 13 | let lunarDay: string = '初一' 14 | lunarDay = 1 15 | ``` 16 | 17 | TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。 18 | 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查: 19 | let myFavoriteNumber 20 | myFavoriteNumber = 'seven' 21 | myFavoriteNumber = 7 22 | 23 | let someValue: any = "this is a string" 24 | 25 | let strLength: number = (someValue).length 26 | 另一个为as语法: 27 | 28 | let someValue: any = "this is a string" 29 | 30 | let strLength: number = (someValue as string).length 31 | 两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。 32 | 33 | ## 联合类型 34 | 联合类型(Union Types)表示取值可以为多种类型中的一种。 35 | ### 简单的例子 36 | ``` 37 | let lunarDay: string | number 38 | lunarDay = '初一' 39 | lunarDay = 1 40 | ``` 41 | 联合类型使用 | 分隔每个类型。 42 | 43 | 这里的` let lunarDay: string | number `的含义是,允许 lunarDay 的类型是 string 或者 number,但是不能是其他类型。 44 | 45 | ### 访问联合类型的属性或方法 46 | 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法: 47 | ``` 48 | function getLength(something: string | number): number { 49 | return something.length 50 | } 51 | // Property 'length' does not exist on type 'string | number'. 52 | // Property 'length' does not exist on type 'number'. 53 | ``` 54 | 55 | 上例中,length 不是 string 和 number 的共有属性,所以会报错。 56 | 访问 string 和 number 的共有属性是没问题的: 57 | ``` 58 | function getString(something: string | number): string { 59 | return something.toString() 60 | } 61 | ``` 62 | ### 联合类型赋值的类型推断 63 | 联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型: 64 | ``` 65 | let lunarDay: string | number 66 | lunarDay = '初一' 67 | console.log(lunarDay.length) // 2 68 | lunarDay = 1 69 | console.log(lunarDay.length) // 编译时报错 70 | ``` 71 | 上例中,第二行的 lunarDay 被推断成了 string,访问它的 length 属性不会报错。 72 | 而第四行的 lunarDay 被推断成了 number,访问它的 length 属性时就报错了。 73 | 74 | ## Null 和 Undefined 75 | `null` 是一个只有一个值的特殊类型。表示一个空对象引用。用 typeof 检测 null 返回是 `object`。 76 | typeof 一个没有值的变量会返回 `undefined`。 77 | 78 | null 和 Undefined 是其他任何类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined。 79 | 80 | 在TypeScript中启用严格的空校验(--strictNullChecks)特性,使得 `null` 和 `undefined` 只能被赋值给 `void` 或本身对应的类型 81 | 82 | 在 tsconfig.json 中启用 --strictNullChecks 83 | ``` 84 | let x: number 85 | x = 1 // 运行正确 86 | x = undefined // 运行错误 87 | x = null // 运行错误 88 | ``` 89 | 90 | 在 tsconfig.json 中启用 --strictNullChecks,需要将x赋值为联合类型 91 | ``` 92 | let x: number | null | undefined //本身对应的类型 93 | x = 1 // 运行正确 94 | x = undefined // 运行正确 95 | x = null // 运行正确 96 | ``` 97 | 98 | ## Never 99 | never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。 100 | 101 | never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。 102 | 103 | 下面是一些返回never类型的函数: 104 | ``` 105 | // 返回never的函数必须存在无法达到的终点 106 | function error(message: string): never { 107 | throw new Error(message) 108 | } 109 | 110 | // 推断的返回值类型为never 111 | function fail() { 112 | return error("Something failed") 113 | } 114 | 115 | // 返回never的函数必须存在无法达到的终点 116 | function infiniteLoop(): never { 117 | while (true) { 118 | } 119 | } 120 | ``` -------------------------------------------------------------------------------- /basics/basic-types.md: -------------------------------------------------------------------------------- 1 | # 基础类型-入门 2 | ## 介绍 3 | TypeScript包含的最简单的数据单元有:数字,字符串,布尔值,Null 和 Undefined等。TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。本节介绍基础类型的布尔值、数字、字符串、数组、元组、枚举、any 和 void 等,其他几种基础类型详见 [基础类型-高级](/advanced/basic-types.html)。 4 | 5 | ## 布尔值 6 | 最基本的数据类型就是简单的`true/false`值,在JavaScript和TypeScript里叫做`boolean`。 7 | ``` 8 | let isDone: boolean = false 9 | ``` 10 | 11 | ## 数字 12 | 和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是`number`。 13 | ``` 14 | let decLiteral: number = 6 15 | ``` 16 | 17 | ## 字符串 18 | TypeScript像其它语言里一样,使用`string`表示文本数据类型。 和JavaScript一样,可以使用双引号(")或单引号(')表示字符串。 19 | ``` 20 | let from: string = "千锋教育" 21 | from = "好程序员" 22 | ``` 23 | 24 | 也使用模版字符串,定义多行文本和内嵌表达式。 这种字符串是被反引号包围(`),并且以${ expr }这种形式嵌入表达式。 25 | 26 | ``` 27 | let surname: string = `Felix` 28 | let age: number = 37 29 | let sentence: string = `Hello, my name is ${ surname }. 30 | 31 | I'll be ${ age + 1 } years old next month.` 32 | ``` 33 | 34 | ## 数组 35 | TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 `[]`,表示由此类型元素组成的一个数组: 36 | ``` 37 | let list: number[] = [1, 2, 3] 38 | ``` 39 | 第二种方式是使用数组泛型,Array<元素类型>: 40 | ``` 41 | let list: Array = [1, 2, 3] 42 | ``` 43 | 44 | ## 元组 Tuple 45 | 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。 46 | ``` 47 | // 声明一个元组类型 x 48 | let x: [string, number] 49 | // 初始化 x 50 | x = ['hello', 10] // OK 51 | // 无效的初始值 52 | x = [10, 'hello'] // Error 53 | ``` 54 | 当访问一个已知索引的元素,会得到正确的类型: 55 | ``` 56 | console.log(x[0].substr(1)) // OK 57 | console.log(x[1].substr(1)) // Error, 'number' 不存在 'substr' 方法 58 | ``` 59 | 当访问一个越界的元素,会出现错误: 60 | ``` 61 | x[3] = "world" // Error, '[string, number]' 未定义第 3 个元素的类型. 62 | console.log(x[5].toString()) // Error, '[string, number]' 未定义第 5 个元素的类型. 63 | ``` 64 | 65 | ## 枚举 66 | enum类型是对JavaScript标准数据类型的一个补充。 使用枚举类型可以为一组数值赋予友好的名字。 67 | ``` 68 | enum Color {Red, Green, Blue} 69 | let c: Color = Color.Green 70 | ``` 71 | 72 | 默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1 开始编号: 73 | ``` 74 | enum Color {Red = 1, Green, Blue} 75 | let c: Color = Color.Green 76 | ``` 77 | 78 | 或者,全部都采用手动赋值: 79 | ``` 80 | enum Color {Red = 1, Green = 2, Blue = 4} 81 | let c: Color = Color.Green 82 | ``` 83 | 84 | 枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字: 85 | ``` 86 | enum Color {Red = 1, Green, Blue} 87 | let colorName: string = Color[2] 88 | console.log(colorName) // 'Green' 89 | ``` 90 | 91 | ## any 92 | 有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用`any`类型来标记这些变量: 93 | ``` 94 | let notSure: any = 4 95 | notSure = "maybe a string instead" // OK 赋值了一个字符串 96 | notSure = false // OK 赋值了一个布尔值 97 | ``` 98 | 99 | 在对现有代码进行改写的时候,`any` 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 100 | ``` 101 | let notSure: any = 4 102 | notSure.ifItExists() // okay, ifItExists函数在运行时可能存在 103 | notSure.toFixed() // okay, toFixed 函数存在 (在编译时不做检查) 104 | ``` 105 | 106 | 当你只知道一部分数据的类型时,`any` 类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据: 107 | ``` 108 | let list: any[] = [1, true, "free"] 109 | list[1] = 100 110 | ``` 111 | 112 | ## void 113 | 某种程度上来说,`void`类型像是与`any`类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 `void`: 114 | ``` 115 | function echo(): void { 116 | console.log('做真实的自己,用良心做教育') 117 | } 118 | ``` 119 | 声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null: 120 | ``` 121 | let unusable: void = undefined 122 | let greeting: void = 'hello world' // void 类型不能赋值为字符串 123 | ``` 124 | -------------------------------------------------------------------------------- /basics/interfaces.md: -------------------------------------------------------------------------------- 1 | # 接口 2 | 在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。 3 | 4 | ## 什么是接口 5 | 在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。 6 | TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。 7 | 8 | ## 简单的例子 9 | ``` 10 | interface Person { 11 | name: string 12 | age: number 13 | } 14 | 15 | let tom: Person = { 16 | name: 'Tom', 17 | age: 25 18 | } 19 | ``` 20 | 上面的例子中,我们定义了一个接口 `Person`,接着定义了一个变量 `tom`,它的类型是 `Person`。这样,我们就约束了 `tom` 的形状必须和接口 `Person` 一致。 21 | 接口一般首字母大写。有的编程语言中会建议接口的名称加上 I 前缀。 22 | 定义的变量比接口少了一些属性是不允许的: 23 | ``` 24 | interface Person { 25 | name: string 26 | age: number 27 | } 28 | 29 | let tom: Person = { 30 | name: 'Tom' 31 | } 32 | // Property 'age' is missing in type '{ name: string }' but required in type 'Person'. 33 | ``` 34 | 多一些属性也是不允许的: 35 | ``` 36 | interface Person { 37 | name: string 38 | age: number 39 | } 40 | 41 | let tom: Person = { 42 | name: 'Tom', 43 | age: 25, 44 | gender: 'male' 45 | } 46 | 47 | // Type '{ name: string age: number gender: string }' is not assignable to type 'Person'. 48 | // Object literal may only specify known properties, and 'gender' does not exist in type 'Person'. 49 | ``` 50 | 可见,* 赋值的时候,变量的形状必须和接口的形状保持一致。* 51 | 52 | ## 可选属性 53 | 有时我们希望不要完全匹配一个形状,那么可以用可选属性: 54 | ``` 55 | interface Person { 56 | name: string 57 | age?: number 58 | } 59 | 60 | let tom: Person = { 61 | name: 'Tom' 62 | } 63 | ``` 64 | ``` 65 | interface Person { 66 | name: string 67 | age?: number 68 | } 69 | 70 | let tom: Person = { 71 | name: 'Tom', 72 | age: 25 73 | } 74 | ``` 75 | 可选属性的含义是该属性可以不存在。 76 | 77 | 这时仍然不允许添加未定义的属性: 78 | ``` 79 | interface Person { 80 | name: string 81 | age?: number 82 | } 83 | 84 | let tom: Person = { 85 | name: 'Tom', 86 | age: 25, 87 | gender: 'male' 88 | } 89 | 90 | // Type '{ name: string age: number gender: string }' is not assignable to type 'Person'. 91 | // Object literal may only specify known properties, and 'gender' does not exist in type 'Person'. 92 | ``` 93 | 94 | ## 任意属性 95 | 有时候我们希望一个接口允许有任意的属性,可以使用如下方式: 96 | ``` 97 | interface Person { 98 | name: string 99 | age?: number 100 | [propName: string]: any 101 | } 102 | 103 | let tom: Person = { 104 | name: 'Tom', 105 | gender: 'male' 106 | } 107 | ``` 108 | 使用 [propName: string] 定义了任意属性取 string 类型的值。 109 | 需要注意的是,一旦定义了任意属性,**那么确定属性和可选属性的类型都必须是它的类型的子集**: 110 | ``` 111 | interface Person { 112 | name: string 113 | age?: number 114 | [propName: string]: string 115 | } 116 | 117 | let tom: Person = { 118 | name: 'Tom', 119 | age: 25, 120 | gender: 'male' 121 | } 122 | 123 | // Property 'age' of type 'number | undefined' is not assignable to string index type 'string'. 124 | // Type '{ name: string age: number gender: string }' is not assignable to type 'Person'. 125 | // Property 'age' is incompatible with index signature. 126 | // Type 'number' is not assignable to type 'string'. 127 | ``` 128 | 129 | 上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。 130 | 131 | 另外,在报错信息中可以看出,此时 `{ name: 'Tom', age: 25, gender: 'male' }` 的类型被推断成了 { `[x: string]: string | number name: string age: number gender: string }`,这是联合类型和接口的结合。 132 | 133 | ## 只读属性 134 | 有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 `readonly` 定义只读属性: 135 | ``` 136 | interface Person { 137 | readonly id: number 138 | name: string 139 | age?: number 140 | [propName: string]: any 141 | } 142 | 143 | let tom: Person = { 144 | id: 89757, 145 | name: 'Tom', 146 | gender: 'male' 147 | } 148 | 149 | tom.id = 9527 150 | // Cannot assign to 'id' because it is a read-only property. 151 | ``` 152 | 153 | 上例中,使用 `readonly` 定义的属性 `id` 初始化后,又被赋值了,所以报错了。 154 | 155 | ***注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:*** 156 | ``` 157 | interface Person { 158 | readonly id: number 159 | name: string 160 | age?: number 161 | [propName: string]: any 162 | } 163 | 164 | let tom: Person = { 165 | name: 'Tom', 166 | gender: 'male' 167 | } 168 | 169 | tom.id = 89757 170 | // Property 'id' is missing in type '{ name: string gender: string }' but required in type 'Person'. 171 | // Cannot assign to 'id' because it is a read-only property. 172 | ``` 173 | 上例中,报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给 id 赋值。 174 | 第二处是在给 tom.id 赋值的时候,由于它是只读属性,所以报错了。 -------------------------------------------------------------------------------- /basics/generics.md: -------------------------------------------------------------------------------- 1 | # 泛型 2 | 3 | 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。 4 | 5 | ## 简单的例子 6 | 7 | 首先,我们来实现一个函数 `createArray`,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值: 8 | 9 | ```ts 10 | function createArray(length: number, value: any): Array { 11 | let result = [] 12 | for (let i = 0; i < length; i++) { 13 | result[i] = value 14 | } 15 | return result 16 | } 17 | 18 | createArray(3, 'x'); // ['x', 'x', 'x'] 19 | ``` 20 | 21 | 上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。 22 | 23 | 这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型: 24 | 25 | `Array` 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 `value` 的类型。 26 | 27 | 这时候,泛型就派上用场了: 28 | 29 | ```ts 30 | function createArray(length: number, value: T): Array { 31 | let result: T[] = [] 32 | for (let i = 0; i < length; i++) { 33 | result[i] = value 34 | } 35 | return result 36 | } 37 | 38 | createArray(3, 'x'); // ['x', 'x', 'x'] 39 | ``` 40 | 41 | 上例中,我们在函数名后添加了 ``,其中 `T` 用来指代任意输入的类型,在后面的输入 `value: T` 和输出 `Array` 中即可使用了。 42 | 43 | 接着在调用的时候,可以指定它具体的类型为 `string`。当然,也可以不手动指定,而让类型推论自动推算出来: 44 | 45 | ```ts 46 | function createArray(length: number, value: T): Array { 47 | let result: T[] = [] 48 | for (let i = 0; i < length; i++) { 49 | result[i] = value 50 | } 51 | return result 52 | } 53 | 54 | createArray(3, 'x') // ['x', 'x', 'x'] 55 | ``` 56 | 57 | ## 多个类型参数 58 | 59 | 定义泛型的时候,可以一次定义多个类型参数: 60 | 61 | ```ts 62 | function swap(tuple: [T, U]): [U, T] { 63 | return [tuple[1], tuple[0]] 64 | } 65 | 66 | swap([7, 'seven']) // ['seven', 7] 67 | ``` 68 | 69 | 上例中,我们定义了一个 `swap` 函数,用来交换输入的元组。 70 | 71 | ## 泛型约束 72 | 73 | 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法: 74 | 75 | ```ts 76 | function loggingIdentity(arg: T): T { 77 | console.log(arg.length) 78 | return arg 79 | } 80 | 81 | // Property 'length' does not exist on type 'T'. 82 | ``` 83 | 84 | 上例中,泛型 `T` 不一定包含属性 `length`,所以编译的时候报错了。 85 | 86 | 这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 `length` 属性的变量。这就是泛型约束: 87 | 88 | ```ts 89 | interface Lengthwise { 90 | length: number 91 | } 92 | 93 | function loggingIdentity(arg: T): T { 94 | console.log(arg.length) 95 | return arg 96 | } 97 | ``` 98 | 99 | 上例中,我们使用了 `extends` 约束了泛型 `T` 必须符合接口 `Lengthwise` 的形状,也就是必须包含 `length` 属性。 100 | 101 | 此时如果调用 `loggingIdentity` 的时候,传入的 `arg` 不包含 `length`,那么在编译阶段就会报错了: 102 | 103 | ```ts 104 | interface Lengthwise { 105 | length: number 106 | } 107 | 108 | function loggingIdentity(arg: T): T { 109 | console.log(arg.length) 110 | return arg 111 | } 112 | 113 | loggingIdentity(7) 114 | 115 | // Argument of type '7' is not assignable to parameter of type 'Lengthwise'. 116 | ``` 117 | 118 | ## 泛型接口 119 | 120 | 之前学习过接口中函数的定义,可以使用接口的方式来定义一个函数需要符合的形状: 121 | 122 | ```ts 123 | interface SearchFunc { 124 | (source: string, subString: string): boolean 125 | } 126 | 127 | let mySearch: SearchFunc; 128 | mySearch = function(source: string, subString: string) { 129 | return source.search(subString) !== -1 130 | } 131 | ``` 132 | 133 | 当然也可以使用含有泛型的接口来定义函数的形状: 134 | 135 | ```ts 136 | interface CreateArrayFunc { 137 | (length: number, value: T): Array 138 | } 139 | 140 | let createArray: CreateArrayFunc; 141 | createArray = function(length: number, value: T): Array { 142 | let result: T[] = [] 143 | for (let i = 0; i < length; i++) { 144 | result[i] = value 145 | } 146 | return result 147 | } 148 | 149 | createArray(3, 'x') // ['x', 'x', 'x'] 150 | ``` 151 | 152 | 进一步,我们可以把泛型参数提前到接口名上: 153 | 154 | ```ts 155 | interface CreateArrayFunc { 156 | (length: number, value: T): Array 157 | } 158 | 159 | let createArray: CreateArrayFunc 160 | createArray = function(length: number, value: T): Array { 161 | let result: T[] = [] 162 | for (let i = 0; i < length; i++) { 163 | result[i] = value 164 | } 165 | return result 166 | } 167 | 168 | createArray(3, 'x'); // ['x', 'x', 'x'] 169 | ``` 170 | 171 | 注意,此时在使用泛型接口的时候,需要定义泛型的类型。 172 | 173 | ## 泛型类 174 | 175 | 与泛型接口类似,泛型也可以用于类的类型定义中: 176 | 177 | ```ts 178 | class GenericNumber { 179 | zeroValue: T 180 | add: (x: T, y: T) => T 181 | } 182 | 183 | let myGenericNumber = new GenericNumber() 184 | myGenericNumber.zeroValue = 0 185 | myGenericNumber.add = function(x, y) { return x + y } 186 | ``` 187 | 此处 zeroValue,add 未赋值会出错,设置 "strictPropertyInitialization": false, 关闭提示 -------------------------------------------------------------------------------- /basics/functions.md: -------------------------------------------------------------------------------- 1 | # 函数的类型 2 | 3 | ## 函数声明 4 | 5 | 在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression): 6 | 7 | ```js 8 | // 函数声明(Function Declaration) 9 | function sum(x, y) { 10 | return x + y 11 | } 12 | 13 | // 函数表达式(Function Expression) 14 | let mySum = function (x, y) { 15 | return x + y 16 | } 17 | ``` 18 | 19 | 一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单: 20 | 21 | ```ts 22 | function sum(x: number, y: number): number { 23 | return x + y 24 | } 25 | ``` 26 | 27 | 注意,**输入多余的(或者少于要求的)参数,是不被允许的**: 28 | 29 | ```ts 30 | function sum(x: number, y: number): number { 31 | return x + y 32 | } 33 | sum(1, 2, 3) 34 | 35 | // Expected 2 arguments, but got 3. 36 | ``` 37 | 38 | ```ts 39 | function sum(x: number, y: number): number { 40 | return x + y 41 | } 42 | sum(1) 43 | 44 | // An argument for 'y' was not provided. 45 | ``` 46 | 47 | ## 函数表达式 48 | 49 | 如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样: 50 | 51 | ```ts 52 | let mySum = function (x: number, y: number): number { 53 | return x + y 54 | } 55 | ``` 56 | 57 | 这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 `mySum`,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 `mySum` 添加类型,则应该是这样: 58 | 59 | ```ts 60 | let mySum: (x: number, y: number) => number = function (x: number, y: number): number { 61 | return x + y 62 | } 63 | ``` 64 | 65 | 注意不要混淆了 TypeScript 中的 `=>` 和 ES6 中的 `=>`。 66 | 67 | 在 TypeScript 的类型定义中,`=>` 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。 68 | 69 | 在 ES6 中,`=>` 叫做箭头函数,应用十分广泛,可以参考 [ES6 中的箭头函数][]。 70 | 71 | ## 用接口定义函数的形状 72 | 73 | 我们也可以使用接口的方式来定义一个函数需要符合的形状: 74 | 75 | ```ts 76 | interface SearchFunc { 77 | (source: string, subString: string): boolean 78 | } 79 | 80 | let mySearch: SearchFunc 81 | mySearch = function(source: string, subString: string) { 82 | return source.search(subString) !== -1 83 | } 84 | ``` 85 | 86 | ## 可选参数 87 | 88 | 前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢? 89 | 90 | 与接口中的可选属性类似,我们用 `?` 表示可选的参数: 91 | 92 | ```ts 93 | function buildName(firstName: string, lastName?: string) { 94 | if (lastName) { 95 | return firstName + ' ' + lastName 96 | } else { 97 | return firstName 98 | } 99 | } 100 | let tomcat = buildName('Tom', 'Cat') 101 | let tom = buildName('Tom') 102 | ``` 103 | 104 | 需要注意的是,可选参数必须接在必需参数后面。换句话说,**可选参数后面不允许再出现必需参数了**: 105 | 106 | ```ts 107 | function buildName(firstName?: string, lastName: string) { 108 | if (firstName) { 109 | return firstName + ' ' + lastName 110 | } else { 111 | return lastName 112 | } 113 | } 114 | let tomcat = buildName('Tom', 'Cat') 115 | let tom = buildName(undefined, 'Tom') 116 | 117 | // A required parameter cannot follow an optional parameter. 118 | ``` 119 | 120 | ## 参数默认值 121 | 122 | 在 ES6 中,我们允许给函数的参数添加默认值,**TypeScript 会将添加了默认值的参数识别为可选参数**: 123 | 124 | ```ts 125 | function buildName(firstName: string, lastName: string = 'Cat') { 126 | return firstName + ' ' + lastName 127 | } 128 | let tomcat = buildName('Tom', 'Cat') 129 | let tom = buildName('Tom') 130 | ``` 131 | 132 | 此时就不受「可选参数必须接在必需参数后面」的限制了: 133 | 134 | ```ts 135 | function buildName(firstName: string = 'Tom', lastName: string) { 136 | return firstName + ' ' + lastName 137 | } 138 | let tomcat = buildName('Tom', 'Cat') 139 | let cat = buildName(undefined, 'Cat') 140 | ``` 141 | 142 | > 关于默认参数,可以参考 [ES6 中函数参数的默认值][]。 143 | 144 | ## 剩余参数 145 | 146 | ES6 中,可以使用 `...rest` 的方式获取函数中的剩余参数(rest 参数): 147 | 148 | ```js 149 | function push(array, ...items) { 150 | items.forEach(function(item) { 151 | array.push(item) 152 | }) 153 | } 154 | 155 | let a = [] 156 | push(a, 1, 2, 3) 157 | ``` 158 | 159 | 事实上,`items` 是一个数组。所以我们可以用数组的类型来定义它: 160 | 161 | ```ts 162 | function push(array: any[], ...items: any[]) { 163 | items.forEach(function(item) { 164 | array.push(item) 165 | }) 166 | } 167 | 168 | let a = [] 169 | push(a, 1, 2, 3) 170 | ``` 171 | 172 | 注意,rest 参数只能是最后一个参数,关于 rest 参数,可以参考 [ES6 中的 rest 参数][]。 173 | 174 | ## 重载 175 | 176 | 重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。 177 | 178 | 比如,我们需要实现一个函数 `reverse`,输入数字 `123` 的时候,输出反转的数字 `321`,输入字符串 `'hello'` 的时候,输出反转的字符串 `'olleh'`。 179 | 180 | 利用联合类型,我们可以这么实现: 181 | 182 | ```ts 183 | function reverse(x: number | string): number | string { 184 | if (typeof x === 'number') { 185 | return Number(x.toString().split('').reverse().join('')) 186 | } else if (typeof x === 'string') { 187 | return x.split('').reverse().join('') 188 | } 189 | } 190 | ``` 191 | 192 | 然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。 193 | 194 | 这时,我们可以使用重载定义多个 `reverse` 的函数类型: 195 | 196 | ```ts 197 | function reverse(x: number): number 198 | function reverse(x: string): string 199 | function reverse(x: number | string): number | string { 200 | if (typeof x === 'number') { 201 | return Number(x.toString().split('').reverse().join('')) 202 | } else if (typeof x === 'string') { 203 | return x.split('').reverse().join('') 204 | } 205 | } 206 | ``` 207 | 208 | 上例中,我们重复定义了多次函数 `reverse`,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。 209 | 210 | 注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。 -------------------------------------------------------------------------------- /advanced/decorators.md: -------------------------------------------------------------------------------- 1 | # 装饰器 2 | ## 介绍 3 | > 装饰器是一种特殊类型的声明,它能够附加到类、类的函数、类属性、类函数的参数上,以达到修改类的行为。 4 | 5 | ### 装饰器的种类 6 | #### 根据装饰器的位置 7 | * 类装饰器 8 | * 类函数装饰器 9 | * 类属性装饰器 10 | * 类函数参数装饰器 11 | 12 | #### 根据装饰器是否有参数 13 | * 无参装饰器(一般装饰器) 14 | * 有参装饰器(装饰器工厂) 15 | 16 | ### 类的装饰器 17 | * 类装饰器的写法 18 | 19 | ``` 20 | function desc(target) { 21 | console.log(target) // 输出 [Function: Person]表示当前装饰的类 22 | } 23 | 24 | @desc 25 | class Person { 26 | public name: string | undefined 27 | public age: number | 0 28 | 29 | constructor(name: string, age: number) { 30 | this.name = name 31 | this.age = age 32 | } 33 | } 34 | ``` 35 | 36 | 此处 target 类型会隐式定义为any, 这样会引发一个TS问题,解决方案:设置tsconfig.json 37 | 38 | 1、"noImplicitAny": false 39 | 40 | 或者 41 | 42 | 2、"strict": false 43 | 44 | * 使用类的装饰器扩展类的属性和方法 45 | interface Class { 46 | new(...args: any[]): {} 47 | } 48 | 49 | function desc(Target: T) { 50 | console.log(Target) 51 | return class extends Target { 52 | gender = '男' 53 | say() { 54 | console.log(this.gender) 55 | } 56 | } 57 | } 58 | 59 | @desc 60 | class Person { 61 | public name: string | undefined 62 | public age: number | 0 63 | 64 | constructor(name, age) { 65 | this.name = name 66 | this.age = age 67 | } 68 | 69 | say() { 70 | console.log(this.name, this.age) 71 | } 72 | } 73 | 74 | let p = new Person('Felix', 20) 75 | console.log(p) 76 | p.say() 77 | ``` 78 | 79 | * 使用装饰器修改类的构造函数(构造函数的重载、方法重载) 80 | 81 | ``` 82 | function desc(target) { 83 | return class extends target{ 84 | name = 'Felixlu' 85 | age = 18 86 | sayHell() { 87 | console.log('我是重载后的', this.name) 88 | } 89 | } 90 | } 91 | 92 | @desc 93 | class Person { 94 | public name: string | undefined 95 | public age: number | 0 96 | 97 | constructor() { 98 | this.name = 'Felix' 99 | this.age = 20 100 | } 101 | 102 | sayHell() { 103 | console.log('hello word', this.name) 104 | } 105 | } 106 | 107 | let p = new Person() 108 | console.log(p) 109 | p.sayHell() 110 | ``` 111 | 112 | * 装饰器工厂的写法 113 | 114 | ``` 115 | function desc(params: string) { 116 | return function (target: any) { 117 | console.log('params', params) 118 | console.log('target', target) 119 | // 直接在原型上扩展一个属性 120 | target.prototype.apiUrl = params 121 | } 122 | } 123 | 124 | @desc('好程序员') 125 | class P { 126 | say() { 127 | console.log('说话') 128 | } 129 | } 130 | 131 | let p: any = new P() 132 | console.log(p.apiUrl) 133 | ``` 134 | 135 | ### 类函数装饰器 136 | 137 | > 它应用到方法上,可以用来监视、修改、替换该方法 138 | 139 | * 基本使用 140 | 141 | ``` 142 | function desc(target, key, descriptor) { 143 | console.log('target', target) // Person { say: [Function] } 表示类的原型 144 | console.log('key', key) // 被装饰的函数名 145 | console.log('descriptor', descriptor) // 被装饰的函数的对象属性 146 | } 147 | 148 | class Person { 149 | public name: string | undefined 150 | public age: number | 0 151 | 152 | constructor(name, age) { 153 | this.name = name 154 | this.age = age 155 | } 156 | 157 | @desc 158 | say() { 159 | console.log('说的方法') 160 | } 161 | } 162 | ``` 163 | 164 | * 在装饰器中添加类的原型属性和原型方法 165 | 166 | ``` 167 | function desc(target, key, descriptor) { 168 | target.gender = '男' 169 | target.foo = function () { 170 | console.log('我是原型上的方法') 171 | } 172 | } 173 | 174 | class Person { 175 | public name: string | undefined 176 | public age: number | 0 177 | 178 | constructor(name, age) { 179 | this.name = name 180 | this.age = age 181 | } 182 | 183 | @desc 184 | say() { 185 | console.log('说的方法') 186 | } 187 | } 188 | 189 | // 测试代码 190 | let p: any = new Person('Felixlu', 20) 191 | console.log(p) 192 | console.log(Person.prototype) 193 | p.say() 194 | console.log(p.gender); // 使用p原型链上的属性 195 | p.foo() // 调用了p原型链上的方法 196 | ``` 197 | 198 | * 使用装饰器拦截函数的调用(替换) 199 | 200 | ``` 201 | function desc(params: string) { 202 | return function (target: any, key: string, descriptor: {[propsName: string]: any}) { 203 | // 修改被装饰的函数 204 | descriptor.value = function (...args: Array) { 205 | args = args.map(it => String(it)) 206 | console.log(args) 207 | } 208 | } 209 | } 210 | 211 | class Person { 212 | public name: string | undefined 213 | public age: number | 0 214 | 215 | constructor(name, age) { 216 | this.name = name 217 | this.age = age 218 | } 219 | 220 | @desc('装饰器上的参数') 221 | say() { 222 | console.log('说的方法') 223 | } 224 | } 225 | 226 | 227 | let p: any = new Person('Felixlu', 20) 228 | console.log(p) 229 | p.say(123, 23, '你好') 230 | ``` 231 | 232 | * 使用装饰器拦截函数的调用(附加新的功能) 233 | 234 | ``` 235 | function desc(params: string) { 236 | return function (target: any, key: string, descriptor: {[propsName: string]: any}) { 237 | // 修改被装饰的函数的 238 | let method = descriptor.value 239 | descriptor.value = function (...args: Array) { 240 | args = args.map(it => String(it)) 241 | console.log(args) 242 | method.apply(this, args) 243 | } 244 | } 245 | } 246 | class Person { 247 | public name: string | undefined 248 | public age: number | 0 249 | 250 | constructor(name, age) { 251 | this.name = name 252 | this.age = age 253 | } 254 | 255 | @desc('装饰器上的参数') 256 | say(...args) { 257 | console.log('说的方法', args) 258 | } 259 | } 260 | 261 | 262 | let p = new Person('Felixlu', 20) 263 | console.log(p) 264 | p.say(123, 23, '你好') 265 | ``` 266 | 267 | ### 类属性装饰器 268 | 269 | * 基本用法 270 | 271 | ``` 272 | function desc(target, name) { 273 | console.log('target', target, target.constructor) // 表示类的原型 274 | console.log('name', name) // 表示被装饰属性名 275 | } 276 | 277 | class Person { 278 | public name: string | undefined 279 | public age: number | 0 280 | 281 | @desc 282 | private gender: string | undefined 283 | 284 | constructor(name, age) { 285 | this.name = name 286 | this.age = age 287 | } 288 | } 289 | 290 | let p = new Person('Felixlu', 20) 291 | console.log(p) 292 | ``` 293 | 294 | * 在装饰器中修改属性值 295 | 296 | ``` 297 | function desc(target, name) { 298 | target[name] = '女' 299 | } 300 | 301 | 302 | class Person { 303 | public name: string | undefined 304 | public age: number | 0 305 | 306 | @desc 307 | public gender: string | undefined 308 | 309 | constructor(name, age) { 310 | this.name = name 311 | this.age = age 312 | } 313 | 314 | say() { 315 | console.log(this.name, this.age, this.gender) 316 | } 317 | } 318 | 319 | let p = new Person('Felixlu', 20) 320 | console.log(p) 321 | p.say() 322 | ``` 323 | 324 | * 类函数参数的装饰器 325 | 326 | > 参数装饰器表达式会在运行时候当做函数被调用,以使用参数装饰器为类的原型上附加一些元数据 327 | 328 | * 基本用法 329 | 330 | ``` 331 | function desc(params: string) { 332 | return function (target: any, key, index) { 333 | console.log(target); // 类的原型 334 | console.log(key); // 被装饰的名字 335 | console.log(index); // 序列化 336 | } 337 | } 338 | class Person { 339 | public name: string | undefined 340 | public age: number | 0 341 | 342 | constructor(name, age) { 343 | this.name = name 344 | this.age = age 345 | } 346 | 347 | say(@desc('参数装饰器') age: number) { 348 | console.log('说的方法') 349 | } 350 | } 351 | 352 | let p = new Person('Felixlu', 20) 353 | console.log(p) 354 | p.say(20) 355 | ``` 356 | 357 | * 为类的原型上添加一些东西 358 | 359 | ``` 360 | function desc(params: string) { 361 | return function (target: any, key, index) { 362 | console.log(target); // 类的原型 363 | console.log(key); // 被装饰的名字 364 | console.log(index); // 序列化 365 | target.message = params; 366 | } 367 | } 368 | class Person { 369 | public name: string | undefined; 370 | public age: number | 0; 371 | 372 | constructor(name, age) { 373 | this.name = name; 374 | this.age = age; 375 | } 376 | 377 | say(@desc('参数装饰器') age: number) { 378 | console.log('说的方法') 379 | } 380 | } 381 | 382 | 383 | let p: any = new Person('哈哈', 20); 384 | console.log(p); 385 | p.say(20); 386 | console.log(p.message) 387 | ``` 388 | 389 | ### 几种装饰器的执行顺序 390 | 391 | ``` 392 | function logCls(params: string) { 393 | return function (target: any) { 394 | console.log('4.类的装饰器'); 395 | } 396 | } 397 | 398 | function logMethod(params: string) { 399 | return function (target: any, key: string, descriptor: {[propsName: string]: any}) { 400 | console.log('3.类的函数装饰器'); 401 | } 402 | } 403 | 404 | function logParams(params: string) { 405 | return function (target: any, name: string) { 406 | console.log('1.类属性装饰器'); 407 | } 408 | } 409 | 410 | function logQuery(params: string) { 411 | return function (target: any, key: string, index: number) { 412 | console.log('2.函数参数装饰器'); 413 | } 414 | } 415 | 416 | @logCls('类的装饰器') 417 | class Person{ 418 | @logParams('属性装饰器') 419 | public name: string | undefined; 420 | 421 | @logMehod('函数装饰器') 422 | getData(@logQuery('函数参数装饰器') age: number, @logQuery('函数参数装饰器') gender: string) { 423 | console.log('----'); 424 | } 425 | } 426 | ``` -------------------------------------------------------------------------------- /basics/classes.md: -------------------------------------------------------------------------------- 1 | # 类 2 | 3 | 传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 `class`。 4 | 5 | TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。 6 | 7 | 这一节主要介绍类的用法,下一节再介绍如何定义类的类型。 8 | 9 | ## 类的概念 10 | 11 | 虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。 12 | 13 | - 类(Class):定义了一件事物的抽象特点,包含它的属性和方法 14 | - 对象(Object):类的实例,通过 `new` 生成 15 | - 面向对象(OOP)的三大特性:封装、继承、多态 16 | - 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据 17 | - 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性 18 | - 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 `Cat` 和 `Dog` 都继承自 `Animal`,但是分别实现了自己的 `eat` 方法。此时针对某一个实例,我们无需了解它是 `Cat` 还是 `Dog`,就可以直接调用 `eat` 方法,程序会自动判断出来应该如何执行 `eat` 19 | - 存取器(getter & setter):用以改变属性的读取和赋值行为 20 | - 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 `public` 表示公有属性或方法 21 | - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现 22 | - 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口 23 | 24 | ## ES6 中类的用法 25 | 26 | 下面我们先回顾一下 ES6 中类的用法。 27 | 28 | ### 属性和方法 29 | 30 | 使用 `class` 定义类,使用 `constructor` 定义构造函数。 31 | 32 | 通过 `new` 生成新实例的时候,会自动调用构造函数。 33 | 34 | ```js 35 | class Animal { 36 | constructor(public name) { 37 | this.name = name 38 | } 39 | sayHi() { 40 | return `My name is ${this.name}` 41 | } 42 | } 43 | 44 | let a = new Animal('Jack') 45 | console.log(a.sayHi()) // My name is Jack 46 | ``` 47 | 48 | ### 类的继承 49 | 50 | 使用 `extends` 关键字实现继承,子类中使用 `super` 关键字来调用父类的构造函数和方法。 51 | 52 | ```js 53 | class Cat extends Animal { 54 | constructor(name) { 55 | super(name) // 调用父类的 constructor(name) 56 | console.log(this.name) 57 | } 58 | sayHi() { 59 | return 'Meow, ' + super.sayHi() // 调用父类的 sayHi() 60 | } 61 | } 62 | 63 | let c = new Cat('Tom') // Tom 64 | console.log(c.sayHi()) // Meow, My name is Tom 65 | ``` 66 | 67 | ### 存取器 68 | 69 | 使用 getter 和 setter 可以改变属性的赋值和读取行为: 70 | 71 | ```js 72 | class Animal { 73 | constructor(name) { 74 | this.name = name 75 | } 76 | get name() { 77 | return 'Jack' 78 | } 79 | set name(value) { 80 | console.log('setter: ' + value) 81 | } 82 | } 83 | 84 | let a = new Animal('Kitty') // setter: Kitty 85 | a.name = 'Tom' // setter: Tom 86 | console.log(a.name) // Jack 87 | ``` 88 | 89 | ### 静态方法 90 | 91 | 使用 `static` 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用: 92 | 93 | ```js 94 | class Animal { 95 | static isAnimal(a) { 96 | return a instanceof Animal 97 | } 98 | } 99 | 100 | let a = new Animal('Jack') 101 | Animal.isAnimal(a) // true 102 | a.isAnimal(a) // TypeError: a.isAnimal is not a function 103 | ``` 104 | 105 | ## ES7 中类的用法 106 | 107 | ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。 108 | 109 | ### 实例属性 110 | 111 | ES6 中实例的属性只能通过构造函数中的 `this.xxx` 来定义,ES7 提案中可以直接在类里面定义: 112 | 113 | ```js 114 | class Animal { 115 | name = 'Jack' 116 | 117 | constructor() { 118 | // ... 119 | } 120 | } 121 | 122 | let a = new Animal() 123 | console.log(a.name) // Jack 124 | ``` 125 | 126 | ### 静态属性 127 | 128 | ES7 提案中,可以使用 `static` 定义一个静态属性: 129 | 130 | ```js 131 | class Animal { 132 | static num = 42 133 | 134 | constructor() { 135 | // ... 136 | } 137 | } 138 | 139 | console.log(Animal.num) // 42 140 | ``` 141 | 142 | ## TypeScript 中类的用法 143 | 144 | ### public private 和 protected 145 | 146 | TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 `public`、`private` 和 `protected`。 147 | 148 | - `public` 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 `public` 的 149 | - `private` 修饰的属性或方法是私有的,不能在声明它的类的外部访问 150 | - `protected` 修饰的属性或方法是受保护的,它和 `private` 类似,区别是它在子类中也是允许被访问的 151 | 152 | 下面举一些例子: 153 | 154 | ```ts 155 | class Animal { 156 | public name 157 | public constructor(name) { 158 | this.name = name 159 | } 160 | } 161 | 162 | let a = new Animal('Jack') 163 | console.log(a.name) // Jack 164 | a.name = 'Tom' 165 | console.log(a.name) // Tom 166 | ``` 167 | 168 | 上面的例子中,`name` 被设置为了 `public`,所以直接访问实例的 `name` 属性是允许的。 169 | 170 | 很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 `private` 了: 171 | 172 | ```ts 173 | class Animal { 174 | private name 175 | public constructor(name) { 176 | this.name = name 177 | } 178 | } 179 | 180 | let a = new Animal('Jack') 181 | console.log(a.name) // Jack 182 | a.name = 'Tom' 183 | 184 | // Property 'name' is private and only accessible within class 'Animal'. 185 | // Property 'name' is private and only accessible within class 'Animal'. 186 | ``` 187 | 188 | 需要注意的是,TypeScript 编译之后的代码中,并没有限制 `private` 属性在外部的可访问性。 189 | 190 | 上面的例子编译后的代码是: 191 | 192 | ```js 193 | var Animal = (function () { 194 | function Animal(name) { 195 | this.name = name 196 | } 197 | return Animal 198 | }()) 199 | var a = new Animal('Jack') 200 | console.log(a.name) 201 | a.name = 'Tom' 202 | ``` 203 | 204 | 使用 `private` 修饰的属性或方法,在子类中也是不允许访问的: 205 | 206 | ```ts 207 | class Animal { 208 | private name 209 | public constructor(name) { 210 | this.name = name 211 | } 212 | } 213 | 214 | class Cat extends Animal { 215 | constructor(name) { 216 | super(name) 217 | console.log(this.name) 218 | } 219 | } 220 | 221 | // Property 'name' is private and only accessible within class 'Animal'. 222 | ``` 223 | 224 | 而如果是用 `protected` 修饰,则允许在子类中访问: 225 | 226 | ```ts 227 | class Animal { 228 | protected name 229 | public constructor(name) { 230 | this.name = name 231 | } 232 | } 233 | 234 | class Cat extends Animal { 235 | constructor(name) { 236 | super(name) 237 | console.log(this.name) 238 | } 239 | } 240 | ``` 241 | 242 | 当构造函数修饰为 `private` 时,该类不允许被继承或者实例化: 243 | 244 | ```ts 245 | class Animal { 246 | public name 247 | private constructor (name) { 248 | this.name = name 249 | } 250 | } 251 | class Cat extends Animal { 252 | constructor (name) { 253 | super(name) 254 | } 255 | } 256 | 257 | let a = new Animal('Jack') 258 | 259 | // Cannot extend a class 'Animal'. Class constructor is marked as private. 260 | // Constructor of class 'Animal' is private and only accessible within the class declaration. 261 | ``` 262 | 263 | 当构造函数修饰为 `protected` 时,该类只允许被继承: 264 | 265 | ```ts 266 | class Animal { 267 | public name 268 | protected constructor (name) { 269 | this.name = name 270 | } 271 | } 272 | class Cat extends Animal { 273 | constructor (name) { 274 | super(name) 275 | } 276 | } 277 | 278 | let a = new Animal('Jack') 279 | 280 | // Constructor of class 'Animal' is protected and only accessible within the class declaration. 281 | ``` 282 | 283 | 修饰符还可以使用在构造函数参数中,等同于类中定义该属性,使代码更简洁。 284 | 285 | ```ts 286 | class Animal { 287 | // public name: string 288 | public constructor (public name) { 289 | this.name = name 290 | } 291 | } 292 | ``` 293 | 294 | ### readonly 295 | 296 | 只读属性关键字,只允许出现在属性声明或索引签名中。 297 | 298 | ```ts 299 | class Animal { 300 | readonly name 301 | public constructor(name) { 302 | this.name = name 303 | } 304 | } 305 | 306 | let a = new Animal('Jack') 307 | console.log(a.name) // Jack 308 | a.name = 'Tom' 309 | 310 | // Cannot assign to 'name' because it is a read-only property. 311 | ``` 312 | 313 | 注意如果 `readonly` 和其他访问修饰符同时存在的话,需要写在其后面。 314 | 315 | ```ts 316 | class Animal { 317 | // public readonly name 318 | public constructor(public readonly name) { 319 | this.name = name 320 | } 321 | } 322 | ``` 323 | 324 | ### 抽象类 325 | 326 | `abstract` 用于定义抽象类和其中的抽象方法。 327 | 328 | 什么是抽象类? 329 | 330 | 首先,抽象类是不允许被实例化的: 331 | 332 | ```ts 333 | abstract class Animal { 334 | public name 335 | public constructor(name) { 336 | this.name = name 337 | } 338 | public abstract sayHi() 339 | } 340 | 341 | let a = new Animal('Jack') 342 | 343 | // Cannot create an instance of an abstract class. 344 | ``` 345 | 346 | 上面的例子中,我们定义了一个抽象类 `Animal`,并且定义了一个抽象方法 `sayHi`。在实例化抽象类的时候报错了。 347 | 348 | 其次,抽象类中的抽象方法必须被子类实现: 349 | 350 | ```ts 351 | abstract class Animal { 352 | public name 353 | public constructor(name) { 354 | this.name = name 355 | } 356 | public abstract sayHi() 357 | } 358 | 359 | class Cat extends Animal { 360 | public eat() { 361 | console.log(`${this.name} is eating.`) 362 | } 363 | } 364 | 365 | let cat = new Cat('Tom') 366 | 367 | // Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'. 368 | ``` 369 | 370 | 上面的例子中,我们定义了一个类 `Cat` 继承了抽象类 `Animal`,但是没有实现抽象方法 `sayHi`,所以编译报错了。 371 | 372 | 下面是一个正确使用抽象类的例子: 373 | 374 | ```ts 375 | abstract class Animal { 376 | public name 377 | public constructor(name) { 378 | this.name = name 379 | } 380 | public abstract sayHi() 381 | } 382 | 383 | class Cat extends Animal { 384 | public sayHi() { 385 | console.log(`Meow, My name is ${this.name}`) 386 | } 387 | } 388 | 389 | let cat = new Cat('Tom') 390 | ``` 391 | 392 | 上面的例子中,我们实现了抽象方法 `sayHi`,编译通过了。 393 | 394 | 需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是: 395 | 396 | ```js 397 | var __extends = (this && this.__extends) || function (d, b) { 398 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p] 399 | function __() { this.constructor = d } 400 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()) 401 | } 402 | var Animal = (function () { 403 | function Animal(name) { 404 | this.name = name 405 | } 406 | return Animal 407 | }()) 408 | var Cat = (function (_super) { 409 | __extends(Cat, _super) 410 | function Cat() { 411 | _super.apply(this, arguments) 412 | } 413 | Cat.prototype.sayHi = function () { 414 | console.log('Meow, My name is ' + this.name) 415 | } 416 | return Cat 417 | }(Animal)) 418 | var cat = new Cat('Tom') 419 | ``` 420 | 421 | ## 类的类型 422 | 423 | 给类加上 TypeScript 的类型很简单,与接口类似: 424 | 425 | ```ts 426 | class Animal { 427 | name: string 428 | constructor(name: string) { 429 | this.name = name 430 | } 431 | sayHi(): string { 432 | return `My name is ${this.name}` 433 | } 434 | } 435 | 436 | let a: Animal = new Animal('Jack') 437 | console.log(a.sayHi()) // My name is Jack 438 | ``` -------------------------------------------------------------------------------- /advanced/variable-declarations.md: -------------------------------------------------------------------------------- 1 | # 变量声明 2 | ## 变量声明 3 | let和const是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,它能阻止对一个变量再次赋值。 4 | 5 | 因为TypeScript是JavaScript的超集,所以它本身就支持let和const。下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var。 6 | 7 | ## var 声明 8 | 一直以来我们都是通过var关键字定义JavaScript变量。 9 | 10 | ``` 11 | var a = 10 12 | ``` 13 | 大家都能理解,这里定义了一个名为a值为10的变量。 14 | 我们也可以在函数内部定义变量: 15 | ``` 16 | function f() { 17 | var message = "千锋教育·好程序员" 18 | return message 19 | } 20 | ``` 21 | 并且我们也可以在其它函数内部访问相同的变量。 22 | ``` 23 | function f() { 24 | var a = 10 25 | return function g() { 26 | var b = a + 1 27 | return b 28 | } 29 | } 30 | var g = f() 31 | g() // returns 11 32 | ``` 33 | 上面的例子里,g可以获取到f函数里定义的a变量。 每当g被调用时,它都可以访问到f里的a变量。 34 | 35 | 即使当g在f已经执行完后才被调用,它仍然可以访问及修改a。 36 | ``` 37 | function f() { 38 | var a = 1 39 | 40 | a = 2 41 | var b = g() 42 | a = 3 43 | 44 | return b 45 | 46 | function g() { 47 | return a 48 | } 49 | } 50 | 51 | f() // returns 2 52 | ``` 53 | 54 | ### 作用域规则 55 | 对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子: 56 | ``` 57 | function f(shouldInitialize: boolean) { 58 | if (shouldInitialize) { 59 | var x = 10 60 | } 61 | return x 62 | } 63 | 64 | f(true) // returns '10' 65 | f(false) // returns 'undefined' 66 | ``` 67 | 有些读者可能要多看几遍这个例子。变量x是定义在*if语句里面*,但是我们却可以在语句的外面访问它。这是因为 var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问,包含它的代码块对此没有什么影响。有些人称此为*var作用域或函数作用域*。函数参数也使用函数作用域。 68 | 69 | 这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错: 70 | ``` 71 | function sumMatrix(matrix: number[][]) { 72 | var sum = 0 73 | for (var i = 0; i < matrix.length; i++) { 74 | var currentRow = matrix[i] 75 | for (var i = 0; i < currentRow.length; i++) { 76 | sum += currentRow[i] 77 | } 78 | } 79 | return sum 80 | } 81 | ``` 82 | 这里很容易看出一些问题,里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。 83 | 84 | ### 捕获变量怪异之处 85 | 快速的猜一下下面的代码会返回什么: 86 | ``` 87 | for (var i = 0; i < 10; i++) { 88 | setTimeout(function() { console.log(i) }, 100 * i) 89 | } 90 | ``` 91 | 介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。 92 | 93 | 好吧,看一下结果: 94 | ``` 95 | 10 96 | 10 97 | 10 98 | 10 99 | 10 100 | 10 101 | 10 102 | 10 103 | 10 104 | 10 105 | ``` 106 | 很多JavaScript程序员对这种行为已经很熟悉了。但也有大多数人可能期望输出结果是这样: 107 | ``` 108 | 0 109 | 1 110 | 2 111 | 3 112 | 4 113 | 5 114 | 6 115 | 7 116 | 8 117 | 9 118 | ``` 119 | 还记得我们上面提到的捕获变量吗? 120 | 121 | 我们传给setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i。 122 | 123 | 让我们花点时间思考一下这是为什么。 setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。 for循环结束后,i的值为10。 所以当函数被调用的时候,它会打印出 10! 124 | 125 | 一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值: 126 | ``` 127 | for (var i = 0 i < 10 i++) { 128 | // 捕获当前i的状态 129 | // 应用当前的值作为参数来执行函数 130 | (function(i) { 131 | setTimeout(function() { console.log(i) }, 100 * i) 132 | })(i) 133 | } 134 | ``` 135 | 136 | ## let 声明 137 | 现在你已经知道了var存在一些问题,这恰好说明了为什么用let语句来声明变量。 除了名字不同外, let与var的写法一致。 138 | ``` 139 | let hello = "Hello!" 140 | ``` 141 | 主要的区别不在语法上,而是语义,我们接下来会深入研究。 142 | 143 | ### 块作用域 144 | 当用let声明一个变量,它使用的是词法作用域或块作用域。不同于使用var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。 145 | ``` 146 | function f(input: boolean) { 147 | let a = 100 148 | 149 | if (input) { 150 | // 这里仍然可以引用a 151 | let b = a + 1 152 | return b 153 | } 154 | 155 | // 报错: 'b' 在这里不存在 156 | return b 157 | } 158 | ``` 159 | 这里我们定义了2个变量a和b。a的作用域是在f函数体内,而b的作用域是在if语句块里。 160 | 161 | 在catch语句里声明的变量也具有同样的作用域规则。 162 | ``` 163 | try { 164 | throw "oh no!" 165 | } 166 | catch (e) { 167 | console.log("Oh well.") 168 | } 169 | 170 | // 错误: 'e' 在这里不存在 171 | console.log(e) 172 | ``` 173 | 拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。它只是用来说明我们不能在let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。 174 | ``` 175 | a++ // 在声明a之前引用a是非法的 176 | let a 177 | ``` 178 | 179 | ### 重定义及屏蔽 180 | 我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。 181 | ``` 182 | function f(x) { 183 | var x 184 | var x 185 | 186 | if (true) { 187 | var x 188 | } 189 | } 190 | ``` 191 | 在上面的例子里,所有x的声明实际上都引用一个相同的x,并且这是完全有效的代码。这经常会成为bug的来源。好的是,let声明就不会这么宽松了。 192 | ``` 193 | let x = 10 194 | let x = 20 // 错误,不能在1个作用域里多次声明`x` 195 | ``` 196 | 并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。 197 | ``` 198 | function f(x) { 199 | let x = 100 // error: interferes with parameter declaration 200 | } 201 | 202 | function g() { 203 | let x = 100 204 | var x = 100 // error: can't have both declarations of 'x' 205 | } 206 | ``` 207 | 208 | 并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。 209 | ``` 210 | function f(condition, x) { 211 | if (condition) { 212 | let x = 100 213 | return x 214 | } 215 | 216 | return x 217 | } 218 | 219 | f(false, 0) // returns 0 220 | f(true, 0) // returns 100 221 | ``` 222 | 在一个嵌套作用域里引入一个新名字的行为称做屏蔽。它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用let重写之前的sumMatrix函数。 223 | ``` 224 | function sumMatrix(matrix: number[][]) { 225 | let sum = 0 226 | for (let i = 0 i < matrix.length i++) { 227 | var currentRow = matrix[i] 228 | for (let i = 0 i < currentRow.length i++) { 229 | sum += currentRow[i] 230 | } 231 | } 232 | return sum 233 | } 234 | ``` 235 | 这个版本的循环能得到正确的结果,因为内层循环的i可以屏蔽掉外层循环的i。 236 | 通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 237 | 238 | ### 块级作用域变量的获取 239 | 在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的环境。就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。 240 | ``` 241 | function theCityThatAlwaysSleeps() { 242 | let getCity 243 | if (true) { 244 | let city = "Seattle" 245 | getCity = function() { 246 | return city 247 | } 248 | } 249 | 250 | return getCity() 251 | } 252 | ``` 253 | 因为我们已经在city的环境里获取到了city,所以就算if语句执行结束后我们仍然可以访问它。 254 | 255 | 回想一下前面setTimeout的例子,我们最后需要使用立即执行的函数表达式来获取每次for循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。 256 | 257 | 当let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout例子里我们仅使用let声明就可以了。 258 | ``` 259 | for (let i = 0 i < 10 i++) { 260 | setTimeout(function() {console.log(i) }, 100 * i) 261 | } 262 | ``` 263 | 会输出与预料一致的结果: 264 | ``` 265 | 0 266 | 1 267 | 2 268 | 3 269 | 4 270 | 5 271 | 6 272 | 7 273 | 8 274 | 9 275 | ``` 276 | ## const 声明 277 | const 声明是声明变量的另一种方式。 278 | ``` 279 | const numLivesForCat = 9 280 | ``` 281 | 它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。换句话说,它们拥有与let相同的作用域规则,但是不能对它们重新赋值。 282 | 283 | 这很好理解,它们引用的值是不可变的。 284 | ``` 285 | const numLivesForCat = 9 286 | const kitty = { 287 | name: "Aurora", 288 | numLives: numLivesForCat, 289 | } 290 | 291 | // 错误 292 | kitty = { 293 | name: "Danielle", 294 | numLives: numLivesForCat 295 | } 296 | 297 | // 以下全部正确 298 | kitty.name = "Rory" 299 | kitty.name = "Kitty" 300 | kitty.name = "Cat" 301 | kitty.numLives-- 302 | ``` 303 | 除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。 304 | 305 | ## let vs. const 306 | 现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。与大多数泛泛的问题一样,答案是:依情况而定。 307 | 308 | 使用最小特权原则,所有变量除了你计划去修改的都应该使用const。基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。使用 const也可以让我们更容易的推测数据的流动。 309 | 310 | ## 解构 311 | ### 解构数组 312 | 最简单的解构莫过于数组的解构赋值了: 313 | ``` 314 | let input = [1, 2] 315 | let [first, second] = input 316 | console.log(first) // outputs 1 317 | console.log(second) // outputs 2 318 | ``` 319 | 这创建了2个命名变量 first 和 second。 相当于使用了索引,但更为方便: 320 | ``` 321 | first = input[0] 322 | second = input[1] 323 | ``` 324 | 解构作用于已声明的变量会更好: 325 | ``` 326 | // 变量交换 327 | [first, second] = [second, first] 328 | ``` 329 | 作用于函数参数: 330 | ``` 331 | function f([first, second]: [number, number]) { 332 | console.log(first) 333 | console.log(second) 334 | } 335 | f(input) 336 | ``` 337 | 你可以在数组里使用...语法创建剩余变量: 338 | ``` 339 | let [first, ...rest] = [1, 2, 3, 4] 340 | console.log(first) // outputs 1 341 | console.log(rest) // outputs [ 2, 3, 4 ] 342 | ``` 343 | 当然,由于是JavaScript, 你可以忽略你不关心的尾随元素: 344 | ``` 345 | let [first] = [1, 2, 3, 4] 346 | console.log(first) // outputs 1 347 | ``` 348 | 或其它元素: 349 | ``` 350 | let [, second, , fourth] = [1, 2, 3, 4] 351 | ``` 352 | 353 | ### 对象解构 354 | 你也可以解构对象: 355 | ``` 356 | let o = { 357 | a: "foo", 358 | b: 12, 359 | c: "bar" 360 | } 361 | let { a, b } = o 362 | ``` 363 | 这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。 364 | 365 | 就像数组解构,你可以用没有声明的赋值: 366 | ``` 367 | ({ a, b } = { a: "baz", b: 101 }) 368 | ``` 369 | 注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。 370 | 371 | 你可以在对象里使用...语法创建剩余变量: 372 | ``` 373 | let { a, ...passthrough } = o 374 | let total = passthrough.b + passthrough.c.length 375 | ``` 376 | ***属性重命名*** 377 | 你也可以给属性以不同的名字: 378 | ``` 379 | let { a: newName1, b: newName2 } = o 380 | ``` 381 | 这里的语法开始变得混乱。 你可以将 a: newName1 读做 "a 作为 newName1"。 方向是从左到右,好像你写成了以下样子: 382 | 383 | let newName1 = o.a 384 | let newName2 = o.b 385 | 令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型,仍然需要在其后写上完整的模式。 386 | ``` 387 | let {a, b}: {a: string, b: number} = o 388 | ``` 389 | 390 | ***默认值*** 391 | 默认值可以让你在属性为 undefined 时使用缺省值: 392 | ``` 393 | function keepWholeObject(wholeObject: { a: string, b?: number }) { 394 | let { a, b = 1001 } = wholeObject 395 | } 396 | ``` 397 | 现在,即使 b 为 undefined,keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。 398 | 399 | ### 函数声明 400 | 解构也能用于函数声明。看以下简单的情况: 401 | ``` 402 | type C = { a: string, b?: number } 403 | function f({ a, b }: C): void { 404 | // ... 405 | } 406 | ``` 407 | 但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。 408 | ``` 409 | function f({ a="", b=0 } = {}): void { 410 | // ... 411 | } 412 | f() 413 | ``` 414 | 上面的代码是一个类型推断的例子 415 | 416 | 其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性: 417 | ``` 418 | function f({ a, b = 0 } = { a: "" }): void { 419 | // ... 420 | } 421 | f({ a: "yes" }) // 正确, 提供了参数,a值为字符串"yes",默认 b = 0 422 | f() // 正确, 如果不提供参数,默认a值为空字符串 {a: ""}, b默认值为0:b = 0 423 | f({}) // 错误, 如果提供了参数,a必须要给值 424 | ``` 425 | 要小心使用解构。从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。解构表达式要尽量保持小而简单。你自己也可以直接使用解构将会生成的赋值表达式。 426 | 427 | ### 展开 428 | 展开操作符正与解构相反。它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如: 429 | ``` 430 | let first = [1, 2] 431 | let second = [3, 4] 432 | let bothPlus = [0, ...first, ...second, 5] 433 | ``` 434 | 这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了first和second的一份浅拷贝。它们不会被展开操作所改变。 435 | 436 | 你还可以展开对象: 437 | ``` 438 | let defaults = { food: "spicy", price: "$$", ambiance: "noisy" } 439 | let search = { ...defaults, food: "rich" } 440 | ``` 441 | search的值为{ food: "rich", price: "$$", ambiance: "noisy" }。 对象的展开比数组的展开要复杂的多。像数组展开一样,它是从左至右进行处理,但结果仍为对象。这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话: 442 | ``` 443 | let defaults = { food: "spicy", price: "$$", ambiance: "noisy" } 444 | let search = { food: "rich", ...defaults } 445 | ``` 446 | 那么,defaults里的food属性会重写food: "rich",在这里这并不是我们想要的结果。 447 | 448 | 对象展开还有其它一些意想不到的限制。 首先,它仅包含对象自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法: 449 | ``` 450 | class C { 451 | p = 12 452 | m() { 453 | } 454 | } 455 | let c = new C() 456 | let clone = { ...c } 457 | clone.p // 正确 458 | clone.m() // 错误! 459 | ``` 460 | 其次,TypeScript编译器不允许展开泛型函数上的类型参数。这个特性会在TypeScript的未来版本中考虑实现。 -------------------------------------------------------------------------------- /advanced/declaration-files.md: -------------------------------------------------------------------------------- 1 | # 声明文件 2 | 3 | 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。 4 | 5 | ## 新语法索引 6 | 7 | 由于本章涉及大量新语法,故在本章开头列出新语法的索引,方便大家在使用这些新语法时能快速查找到对应的讲解: 8 | 9 | - [`declare var`](#declare-var) 声明全局变量 10 | - [`declare function`](#declare-function) 声明全局方法 11 | - [`declare class`](#declare-class) 声明全局类 12 | - [`declare enum`](#declare-enum) 声明全局枚举类型 13 | - [`declare namespace`](#declare-namespace) 声明(含有子属性的)全局对象 14 | - [`interface` 和 `type`](#interface-he-type) 声明全局类型 15 | - [`export`](#export) 导出变量 16 | - [`export namespace`](#export-namespace) 导出(含有子属性的)对象 17 | - [`export default`](#export-default) ES6 默认导出 18 | - [`export =`](#export-1) commonjs 导出模块 19 | - [`export as namespace`](#export-as-namespace) UMD 库声明全局变量 20 | - [`declare global`](#declare-global) 扩展全局变量 21 | - [`declare module`](#declare-module) 扩展模块 22 | - [`/// `](#san-xie-xian-zhi-ling) 三斜线指令 23 | 24 | ## 什么是声明语句 25 | 26 | 假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 `