1、概述 -- 解决 JavaScript 类型系统的问题
TS 是一门基于 JS 之上的编程语言是 JS 超集 (superset),所谓超集就是在 JS 基础之上多了一些扩展特性,多出来的其实上就是一套更强大的类型系统以及对 ES 新特性的支持。它最终会被编译为原始的 JS ,也就是说我们去使用 TS 过后开发者在开发过程当中就可以直接去使用 TS 所提供的一些新特性以及在 TS 当中更强大的类型系统,开发工作完成过后将代码编译成能够在生产环境直接去运行的 JS 代码。它的作用也就非常明显了,TS 中的类型系统的优势就是帮我们在开发过程中避免有可能会出现的类型异常,从而提高我们编码的效率以及我们代码的可靠程度。对于新特性的支持也不用多说,ES 近几年迭代了很多新东西很多时候在一些陈旧的环境当中都会有一些兼容性的问题,TS 支持自动去转换新特性所以说就可以立即使用这些新特性在 TS 当中。那也就是说即便不需要类型系统,通过 TS 的去使用 ES 的新特性也是一个很好的选择这方面和 Babel 是类似的。它重点解决了 JS 语言自由类型系统的不足,通过使用TS可以大大提高代码的可靠程度。这里重点要探讨的是 JS 自有类型系统的问题,以及如何去借助一些优秀的技术方案去解决这些问题。TS 只是在这个过程当中涉及到的一门语言,因为 TS 这门语言目前可以说是此类问题的最终级解决方案。
2、类型系统 -- 强类型与弱类型
在具体介绍 JS 类型系统问题之前,先解释两组在区分不同编程语言时经常提及的名词:
- 强类型与弱类型(类型安全)
- 静态类型与动态类型(类型检查)
它们分别是从类型安全和类型检查这两个维度去区分了不同的编程语言,首先来看类型安全这样一个维度。从类型安全的角度来说,编程语言分为强类型和弱类型。强弱类型的概念最早是1974年的时候美国有两个计算机专家提出来的,当时对强类型这样一个概念定义就是,在语言层面就限制了函数的实参类型就必须与形参类型相同。而弱类型怎完全相反,它在语言层面不会限制实参的类型。由于这种强弱类型之分根本不是某一个权威机构的定义,而且当时这两位计算机专家也没有给出一个具体的规则,就导致了后人对这种界定方式的细节出现了一些不一样的理解。但是整体上大家的界定方式都是在描述强类型有更强的类型约束,而弱类型中几乎没有什么约束。强类型语言当中不允许任意的隐试类型转换,而在弱类型语言当中则允许任意的数据隐试类型转换。我们所说的强类型是指在语言的语法层面,就限制了不允许传入不同类型的值。如果传入了不同类型的值,在编译阶段会报出错误而不是等到运行阶段再通过逻辑判断去限制。在 JS 中所有报出的类型错误,都是在代码层面然后在运行时通过逻辑判断手动抛出的。变量类型允许随时改变的特点,不是强弱类型的差异。
3、类型系统 -- 静态类型与动态类型
除了类型安全的角度有强类型和弱类型之分,在类型检查的角度还可以将编程语言分为静态类型和动态类型。关于静态类型语言和动态类型语言之间的差异并没有什么争议,大家都很统一。
静态类型语言主要的表现就是,一个变量声明是它的类型就是明确的。而且在这个变量声明过后,它的类型就不允许再被修改了。相反动态类型语言的特点就是在运行阶段才能够明确一个变量的类型,而且变量的类型也可以随时发生变化。在动态类型语言当中,它的变量是没有类型的,而变量当中所存放的值是有类型的。
总结:从类型安全的角度来说一般将编程语言分为:强类型语言和弱类型语言,两者之间的区别就是是否允许随意的隐式类型转换;从类型检查的角度来看一般分为静态类型和动态类型,它们两者之间的区别就是是否允许随时去修改变量的类型。
4、JavaScript 类型系统特征 (弱类型且动态类型)
由于 JS 是一门弱类型而且是动态类型的语言,语言本身的类型系统是非常薄弱的,甚至我们也可以说 JS 根本就没有一个类型系统。这种语言的特征用一个比较流行的词来说就是“任性”。因为它几乎没有任何的类型限制,所以说 JS 这门语言是及其灵活多变的但是在这种灵活多变的表象背后丢失掉的是类型系统的可靠性。我们在代码当中每每遇到一个变量,我们都需要去担心它到底是不是我们想要的类型,整体的感受用另外一个流行词来说就是“不靠谱”。可能有人会问为什么 JS 不能设计成一门强类型的或者说静态类型的这种更靠谱的语言呢?这个原因自然跟 JS 的背景有关,首先在早前根本就没有想到 JS 的应用会发展到今天这种规模,最早的 JS 应用根本不会太复杂需求都非常简单。很多时候几百行代码甚至是几十行代码就搞定了,在这种一眼就能够看到头的情况下类型系统限制就显得很多余或者说很麻烦。其次 JS 是一门脚本语言,脚本语言的特点就是不需要编译,就直接在运行环境当中去运行。换句话说,JS 是没有编译环节的,即便把它设计成一个静态类型的语言也没有什么意义。因为静态类型语言需要在编译阶段去做类型检查,而 JS 根本就没有这样一个环节。根据以上这样一些原因 js 就选择成为了一门更灵活,更多变的弱类型、动态类型语言。放在当时的那样一个环境当中这并没有什么问题,甚至也可以说这些特点都是 JS 的一大优势。而现如今我们前端应用的规模已经完全不同,遍地都是一些大规模的应用。JS 代码也变得越来越复杂、开发周期也越来越长,在这种情况下之前 JS 的弱类型、动态类型这些优势也就自然变成了短板。这个道理其实很好理解,以前我们只是杀鸡用小刀子就可以了,而且小刀子更灵活更方便。但是现在要拿这把小刀子去杀牛就显得非常吃力,在这里我们的吃力具体体现在什么地方,接下来可以从一些具体的情况当中去体现出来。
5、弱类型的问题
JS 这种弱类型语言在去应对大规模应用开发时,有可能会出现的一些常见问题:
-
必须要等到代码运行阶段才能够发现代码当中的一些类型异常,而且如果不是立即去执行,而是在某一个特定的时间才去执行,这样就会在代码上线后留下隐患。如果是强类型语言的话这里我们直接去调用对象当中一个不存在的成员,这里语法上就会报出错误。根本就不用等到去运行这行代码。
const obj = {} obj.foo() -
JS 缺少为函数的传参类型进行判断的机制,当传参的类型不对或者漏传时。在开发阶段都是不容易被发现的。虽然通过约定的方式也可以规避相应的问题,但是约定是根本没有任何保障的,特别是在多人协同开发的情况下。根本不能保证每个人都能遵循所有的约定,而如果使用的是一种强类型的语言,这种情况就会被彻底避免掉。因为在强类型语言当中如果我们要求传入的是数字,如果传入的是其他类型的值在语法上就行不通。
function sum (a, b) { return a + b } sum(100, '200') sum(10) -
通过索引器的语法去给对象添加属性 ,对象的属性名只能够是字符串或者是 symbol 。但是由于 JS 是弱类型的,就导致在索引器中可以使用任意类型的值去作为属性,而在内部会自动转换成字符串。 如果说开发者不知道对象属性名会自动转换成字符串的特点,这里就会感觉很奇怪。这种奇怪的根源就是,我们用的是一个比较随意的弱类型语言。如果是强类型语言的话,这种问题也可以彻底避免。因为在强类型情况下这里索引器它明确有类型要求,不满足类型要求这样一个成员,在语法上就行不通。
const obj = {} obj[true] = 100 console.log(obj['true'])
总结:第一个例子当中因为弱类型的关系,我们程序当中的一些类型异常,需要等到运行时才能够发现;第二个例子当中,因为弱类型的关系,类型不明确就会造成函数功能有可能发生改变;第三个例子当中因为弱类型的关系,就出现了开发者对对象索引器的一种错误用法。综上弱类型这种语言,它的弊端是十分明显的只是在代码量小的情况下这些问题都可以通过约定的方式来规避。而对于一些开发周期特别长的大规模项目这种约定的方式仍然会存在隐患。只有在语法层面的强制要求才能够提供更可靠的保障,所以说强类型语言的代码在代码可靠程度上是有明显优势的。使用强类型语言就可以提前消灭一大部分有可能会存在的类型异常,而不必等到运行过程当中才会发现的 bug 。
6、强类型的优势
通过对 JS 这种弱类型语言弊端的分析,强类型的优势已经体现出来了。关于强类型的优势可以总结四个大点:
-
错误可以更早的暴露,也就是可以在编码阶段提前去消灭一大部分有可能会存在的类型异常。因为在编码阶段语言本身就会把这些类型异常暴露出来,所以就不用等到运行阶段再去查找这种错误。
-
代码更智能,编码更准确。这是一个开发者最容易感受到的,在开发过程中的智能提示可以大大提高开发的效率。
-
重构更牢靠。重构一般是指对我们的代码有破坏性的改造,例如删除对象当中的某个成员、或者是修改某个已经存在的成员名称。
-
减少不必要的类型判断。
function (a, b) { // 弱类型 if (typeof a !== 'number' || typeof b !== 'number') { throw new TypeError('arguments must be a number') } return a + b } function (a:number, b:number): number { // 强类型 return a + b }
7、配置文件
tsc 这个命令不仅仅可以去编译指定的某个 ts 文件,它还可以用来编译整个项目,前提是要有一个负责编译的 TS 配置文件。使用 yarn tsc --init 命令自动生成配置文件,执行过后就会在项目的根目录下就会添加一个 tsconfig.json 的配置文件。在这个文件当中默认只有一个 compilerOptions 的属性,这个属性就是 TS 编译器所对应的一些配置选项。其中绝大多数的一些选项都被注释掉了,而且在每个选项上都会配有一些简易的说明。首相来了解一些比较重要的属性:
- target: 用来去设置编译后的 JS 所采用的 ES 标准,"ES5"|"ES2015" ...;
- module: 输出代码采用什么方式去进行模块化,"commonjs"|"amd" ...;
- outDir: 用来设置编译结果输出的目录;
- sourceMap: 是否开启源代码映射,开启的话在调试的时候就能够使用 sourceMap 去调试 TS 源代码;
- rootDir: 用来配置源代码的路径;
- strict: 是否开启严格代码检查,开启的话对类型的检查会非常严格,代码中采用隐式推断的地方将无法通过;
配置完成可以直接使用 yarn tsc 命令进行编译
8、原始类型 Primitive Type
TS 支持 ECMAscript 的六种原始类型:
-
string:存放字符串
const a:string = 'foo'; -
number: 存放数字、NaN、Infinity
const b:number = 1 -
boolean: 存放布尔值 true | false
const c:boolean = true -
void: 函数没有返回值的时候去标记这个函数返回值类型 null | undefined
function():void { ...} -
null: 空值 null
const f:null = null -
undefined: 空值 undefined
const g:undefined = undefined -
symbol: Symbol()
string、number、boolean 三个原始类型,在 TS 中默认是可以设置为空值的。也就是说可以给它们赋值 null、undefined
const a:string = null
8、标准库声明 内置对象类型
Symbol 实际上就是 JS 当中内置的一个标准对象,和 Array 、Object 性质是相同的。只不过 Symbol 是 ES6 新增的,对于这种内置的对象它自身也是有类型的。而且这些内置对象的类型都在 TS 中都定义好了,并且定义文件的 target 和类型库是相对应的。解决不能兼容高版本内置标准对象的方法,一个是将 target 设置兼容的 ES 版本、二设置 lib 属性在 lib 属性数组中添加可兼容的版本名称。
以上通过解决代码当中找不到 Symbol 这样一个问题,介绍了一下在 TS 中标准库的概念。所谓的标准库实际上就是内置对象所对应的声明文件,在代码中使用内置对象就必须要引用对应的标准库否则 TS 就找不到所对应的类型就会报错。
9、中文错误信息
介绍一个对中文开发者非常有帮助的小技巧,就是让 TS 去显示中文的错误消息。TS 本身支持多语言话的错误消息,默认会根据操作系统和开发工具的语言设置选择一个错误消息的语言。如果想要强制使用中文,可以使用 yarn tsc --locale zh-CN 命令。对于 vscode 当中的错误消息,可以在配置选项中去配置。
10、作用域问题
在使用 TS 的过程当中,肯定会涉及到在不同文件当中使用 TS 的不同特性,这种情况下就可能会遇到不同文件当中会有相同变量名称的这种情况。解决这个问题的办法自然是把它们分别装入不同的或者是单独的作用域中,比如一个立即执行函数或者使用export 导出一下也就是使用一下 ESModule,这样我们这个文件就会作为一个模块,模块有单独的模块作用域这个文件当中所有的成员就变成了这个模块作用域当中的局部成员了,也就不会在出现冲突的问题了。这样一个问题实际上在实际开发时,一般不会用到因为在绝大多是情况下,每个文件都会以模块的形式去工作。
// 作用域问题
// 方案 1
(function(){
const a = 123
})()
// 方案 2
const a = 123;
export { }// 这里的花括号只是 export 的语法,并不是导出一个空对象
11、Object 类型
TS 中的 Object 并不单指普通的对象类型,而是泛指所有的非原始类型(对象、数组、函数)。
const foo:object = {}
const bar:object = []
const baz:object = function () {}
如果需要普通的对象类型,需要去使用类似对象字面量的语法去标记,但更专业的方法是用接口。
const obj: {foo: number, bar: string} = {
foo: 5,
bar: 'good'
}
12、数组类型 Array Types
TS 定义数组的方式有两种:
-
泛型方式 Array<元素类型>
-
元素类型[]
// 数组类型 const arr1:Array<number> = [1, 2, 3] const arr2:string[] = ['foo', 'bar', 'baz'] //---------------------------------- function sum (...args: number[]) { return args.reduce((prev, current) => preve + current, 0) } sum(1, 2, 3)
13、元祖类型 Tuple Type
元祖类型是一种特俗的数据结构,其实元祖就是一个明确元素数量以及每个元素类型的一个数组。各个元素的类型,不必要完全的相同。在 TS 中可以使用类似数组字面量的这种语法去定义,如果想去访问元祖中的某个元素仍然可以使用数组下标的方式去访问。
const tuple:[number, string] = [18, 'leo'];
const age = tuple[0];
const name = tuple[1];
或者
const [age, name] = tuple
元祖一般用来在一个函数当中返回多个返回值,这种类型在现在越来越常见,比如在React当中使用 hooks 还有在 ES2017 中使用 Object.entries() 获取一个对象的键值数组。
const [state, setState] = useState();
const obj = {
foo: 123,
bar: 456,
};
Object.entries(obj) // ['foo', 123], ['bar', 456]
14、枚举类型 Enum Types
在应用开发中经常会涉及到用某几个数值去代表某种状态,比如下例中用数字表示文章的发布状态:0:‘草稿’、1:‘未发布’,2: ‘已经发布’。也就是说这个状态属性的取值,也只有 0、1、2这三个值。如果直接在代码中使用 0、1、2这种字面量去表示状态的话,时间久了就有可能搞不清楚这里所写的这个数字它到底对应的是那个状态,并且时间久了还有可能混进来一些其他的数值。这种情况下使用枚举类型是最合适的了,因为枚举类型有两个特点:
-
为一组数值分别取上更好理解的名字
-
一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性
const post = { title: 'Hello TS', content: 'TS is a typed superset of JS', status: 0 }
在很多传统的编程语言当中,枚举是一个非常常见的数据结构。不过在JS当中并没有这种数据结构,很多时候都是使用一个对象去模拟实现枚举。
const postStatus = {
Draft: 0,
Unpublished: 1,
Published: 2,
}
enum PostStatus {
Draft = 0,
Unpublished = 1,
Published = 2,
}
const post = {
title: '...',
content: '...',
status: PostStatus.Draft
}
枚举类型会入侵到运行时的代码,它会影响编译后的结果在 TS 中使用的大多数类型在经过编译后,最终都会被移除掉因为它们的左右只是为了在开发过程中提供类型检查。而枚举不会被删除,它会被编译成一个双向的键值对对象。所谓的双向键值对,就是可以通过键获取值也可以通过值获取键。目的就是为了可以动态的根据枚举值获取枚举的名称,也就是说可以在代码当中通过索引器的这种方式去访问对应的枚举名称。如果说可以确认代码当中不会使用索引器的方式去访问枚举,就建议大家去使用常量枚举。
// 编译后
var PostStatus;
(function(PostStatus){
PostStatus[PostStatus['Daft'] = 0] = 'Draft';
PostStatus[PostStatus['Unpublished'] = 1] = 'Unpublished';
PostStatus[PostStatus['Published'] = 2] = 'Published'
})(PostStatus||(PostStatus = {}))
console.log(PostStatus)
// {
// 0: 'Draft',
// 1: 'Unpublished',
// 2: 'Published',
// Draft: 0,
// Unpublished: 1,
// Published: 2,
// }
常量枚举的用法就是在枚举的 enum 关键词前面加上 const。
// 编译前
enum PostStatus {
Draft = 0,
Unpublished = 1,
Published = 2,
}
const post = {
title: '...',
content: '...',
status: PostStatus.Draft
}
// 编译后
var post = {
...
status: 0
}
15、函数类型 Function Types
函数的类型约束,无外乎就是对函数的输入、输出进行类型限制。输入就是函数的传参,输出指的就是函数的返回值。在 JS 当中有两种函数定义方式,分别是函数声明、函数表达式。所以要从这两种情况下了解,如何对函数进行类型约束。
-
在函数声明的方式下,要保证新参和实参的类型、个数完全相同。如果需要某一个参数是可选的,就可以使用可选参数这样一个特性,或者使用 ES2015新增的参数默认值。可选参数也好或者默认参数也好,都必须要出现在参数列表的最后。这一点的原因其实很简单,因为参数会按照位置进行传递,如果说可选参数出现再了必选参数前面。这个时候必选参数是没有办法拿到正常的对应值,如果想要得到任意个数的参数就可以使用 ES2015 的 rest 操作符。
function func1 (a:number, b:number, c?: number):string { // 函数声明的方式 return 'func1' } func(100, 200) // 要保证形参和实参的类型、个数完全相同 -
函数表达式所对应的类型限制,函数表达式它也可以使用相同的方式去限制函数的参数和返回值的类型。不过函数表达式最终是要放到一个变量当中,接收这个函数的变量也应该有类型的。一般 TS 都能根据函数表达式推断出来变量的类型,如果把一个函数作为一个参数传递,也就是回调函数这种方式。对于回调函数这种情况下,就必须要去约束我们这个回调函数的参数,也就是形参的类型。这种情况下就可以使用这种类似箭头函数的方式去表示这个参数它可以接收什么样的一个函数。这种方式在定义接口的时候经常用到,
const func2:(a:number, b:number) => string = function (a:number, b:number):string { return 'func2' }
16、任意类型 Any Types
由于 JS 的自身是弱类型的关系,很多内置的 API 它本身就支持接收任意类型的参数。而 TS 又是基于 JS 基础之上的,所以说难免会在代码当中需要去用一个变量去接收任意类型的数据。
function stringfy (value:any):tring {
return JSON.stringfy(value)
}
stringfy(100);
stringfy('string');
stringfy(true)
any 类型仍然属于动态类型,它的特点跟普通的 JS 变量是一样的。也就是可以用来接收任意类型的值,并且可以在运行过程当中接收其他类型的值。就是一位它有可能存放任意类型的值,所以说 TS 不会对 any 这种类型做类型检查,这也就意味着我们仍然可以像之前在 JS 中一样在它上面去调用任意的成员语法上都不会报错。就是因为 any 类型不会有任何类型检查,所以说它仍然会存在类型安全问题。所以说轻易不要去使用 any 类型,但是有的时候需要去兼容一些老的代码的时候,就会难免会用到这样一个 any 的类型。
let foo:any = 'string';
foo = 100;
foo.bar();
17、隐式类型推断 Type Inference
在 TS 当中如果没有明确通过类型注释去标记一个变量的类型,TS 会根据变量的使用情况去推断这个变量的类型。这样一个特性叫做隐式类型推断
let age = 18; // number
age = 'string'; // Type '"string"' is not assignable to type 'number'
如果说 TS 无法去推断一个变量具体的类型,这个时候就会将这个类型标记为 any 。
let foo; // any
foo = 100;
foo = 'string'
18、类型断言 Type assertions
在有些特殊的情况下 TS 无法去推断出来一个变量的具体类型,而作为开发者根据代码的使用情况是可以明确知道这个变量到底是什么类型的。
// 假设这个 nums 来自一个明确的接口
const nums = [100, 120, 119, 112];
const res = nums.find(i => i > 0);
const square = res as number ** 2;
console.log(square)
类型断言的方式有两种:
-
使用 as 关键词
const num1 = res as number -
在变量前面使用 <数据类型>
const num2 = <number>res // JSX 下不能使用
类型断言并不是类型转化,也就是说这里并不是把一个类型转化成另一个类型。因为类型转换是代码在运行阶段的一个概念,而这个地方类型断言只是在编译过程当中的一个概念。当代码编译过后这个断言就不会存在,所以说它跟类型转换是有本质上的差异的。
19、接口 interfaces
接口可以理解成一种规范或者说一种契约,是一种抽象的概念可以用来去约定对象的结构。去使用一个接口就必须要去,遵循这个接口全部的约定。在 TS 当中接口最直观的体现就是可以约定一个对象当中具体应该有哪些成员,而且这些成员的类型又是什么样的。
interface Post {
title: string;
content: string;
}
function printPost (post:Post):void {
console.log(post.title);
console.log(post.content)
}
printPost({
title: 'Hello TypeScript',
content: 'A typescript superset'
})
总结:接口就是用来去约束对象的结构,一个对象去实现一个接口就必须要去拥有这个接口当中所约束的所有成员。
对于接口中约定的成员,还有一些特殊的用法:
-
可选成员:在对象当中某一个成员是可有可无的,这样的话对于约束这个对象的接口来说可以使用可选成员这个特性。
interface Post { title: string; content: string; subtitle?: string; // 等价于 subtitle: string | undefined } -
只读成员:设置只读属性的成员,不可再进行编辑
interface Post { title: sting; content: string; subtitle: string; readyonly summary: sring; } const hello:Post = { title: 'Hello TypeScript'; content: 'A javascript superset'; summary: 'A javascript ...' } hello.summary = 'other' // Cannot assign to 'summary' because it is a read-only property -
动态成员:一般适用于一些具有动态成员的对象,例如程序当中的缓存对象。它在运行过程当中,就会出现一些动态的键值。
interface Cache { [key:string]: string } const cache:Cache = {} cache.foo = 'value1' cache.bar = 'value2'
20、类 Classes
类可以说是面向对象编程中一个最重要的概念,关于类的作用这里再简单描述下。类就是用来描述一类具体事物的抽象特征,以生活角度去举例。例如手机就属于类型,这个类型的特征就是能够打电话、发短信。在了个类型下面有很多的子类,这些子类一定会满足父类的所有特征然后再多出来一些额外的特征。例如智能手机它除了可以打电话、发短信它还能够使用一些 app。类是不能够被直接使用的,而是去使用属于这个类的具体事物。类比到程序当中类也是一样的它可以用来去描述,一类具体对象的一些抽象程序。在 ES 6以前 JS 都是通过函数配合原型去模拟实现的类,从 ES6 开始 JS 中就有了专门的 class 。而在 TS 中我们除了可以使用所有 ES 标准当中所有类的功能,它还添加了一些额外的功能和用法。例如对类的成员有特殊的访问修饰符,还有一些抽象类的概念。在 TS 中额外多出来的新的类的一些特性有:
-
在 TS 当中我们需要明确在类当中去声明所拥有的一些属性,而不是直接在构造函数当中动态通过 this 去添加。在类中去声明属性的方式就是直接在类当中去定义,这个语法是 ES2016 中定义的。这么做的目的就是,为了给属性去做一些类型的标注。除此之外仍然可以按照 ES6 标准当中的语法为类声明一些方法,在声明的方法当中仍然可以使用函数类型注解的方式去限制参数的类型和返回值的类型。在方法的内部也同样可以使用 this 去访问 当前实例对象也就可以访问到对应的属性。
calss Person { name: string age: number constructor (name:string, age:number) { this.name = name this.age = age } sayHi (msg:string):void { console.log(
Hi, my name is ${this.name}, ${msg}) } }
21、类的访问修饰符
类当中成员的访问修饰符:
-
private: 私有属性修饰符表示该属性只能在类内部被访问到;
-
public:公有成员,在TS中类成员的访问修饰符 默认就是 public ;
-
protected:受保护的成员,只允许在子类当中去访问对应的成员
class Person { public name: string private age: number protected gender: boolean construnctor (name: string, age: number) { this.name = name this.age = age this.gender = true } sayHi (mag: string): void { console.log(
my name is ${name}, ${msg}) } } class Student extends Person { constructor (name: string, age: number) { super(name, age) console.log(this.gender) // true } } const tom = new Person('tom', 8) console.log(tom.name) console.log(tom.age) // Property 'age' is private and only accessible within class 'Person' console.log(tom.gender) // Property 'gender' is protected and only accissible within class 'Person' and its subclasses
总结:以上就是 TS 当中对于类额外添加的三个访问修饰符 private、protected、public 它们的作用可以用来去控制类当中的一些成员的可访问级别。这里还有一个要注意的点就是对于构造函数的访问修饰符,构造函数的访问修饰符默认也是 public 。如果说把它设置成为 private 这个类型就不能够在外部被实例化了,也不能够被继承。在这样一种情况下,就只能够在这个类的内部去添加一个静态方法,然后在静态方法当中去创建这个类的实例。因为private 只允许在内部去访问。
class Student extends Person {
private constructor (name:string, age:number) {
super(name, age)
}
static create (name:sting, age:number) {
return new Student(name, age)
}
}
cosnt jack = Student.create('jack', 18)
22、类的只读属性
对属性成员除了可以用 private、protected 去控制它的访问级别,还可以使用 readonly 关键词把成员设置成只读的。如果说属性已经有了访问修饰符 readonly 应该跟在访问修饰符的后面,对于只读属性我们可以选择在类型声明的时候直接通过等号的方式去初始化,也可以在构造函数当中去初始化,两者只能选其一。并且初始化以后就不能被再修改了,不管是在内部还是在外部都是不能被再修改的。
class Person {
public name: string
private age: number
protected readonly gender: boolean
construnctor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (mag: string): void {
console.log(`my name is ${name}, ${msg}`)
}
}
23、类与接口
相比于类接口的概念要更为抽象一点,我们继续以手机的例子作比。手机作为一个类型,这个类型的实例都是能够打电话、发短信的。因为手机这个类的特征就是打电话、发短信。但是能够打电话不仅仅是手机,以前还有比较常见的座机。但是座机并不属于手机这个类目,而是一个单独的类目,因为它不能够发短信。在这种情况下就会出现不同的类与类之间也会有一些共同的特征,对于这些公共的特征一般会使用接口去抽象。可以理解为手机可以打电话因为它实现了能够打电话的协议,而座机也能够打电话因为它也实现了这个相同的协议。这里所说的协议在程序当中称为接口,使用接口约束类之间公共的能力。
interface EatAndRun {
eat (foot:string): void;
run (distance:number): void;
}
class Person implements EatAndRun {
eat (food:string):void {
console.log(`优雅的进餐:${food}`)
}
run (distance: number):void {
console.log(`直立行走:${distance}`)
}
}
class Animal implements EatAndRun {
eat (food:string):void {
console.log(`呼噜呼噜的吃:${food}`)
}
run (distance: number):void {
console.log(`爬行:${distance}`)
}
}
这里需要注意一点的就是,在 C# 和 Java 这样的语言当中,建议我们尽可能的让每个接口的定义更加简单、更加细化。更为合理的设计就是一个接口只去约束一个能力,让后让一个类型同时去实现多个接口。
interface Eat {
eat (foot:string):void
}
interface Run {
run (destance: number):void
}
class Person implaments Eat, Run {}
class Animal implaments Eat, Run {}
24、抽象类
抽象类在某种程度上来说跟接口有点类似,它也是可以用来约束子类当中必须要有的某一个成员。不同的是抽象类可以包含一些具体的实现,而接口它只能够我是成员的一个抽象不包含具体的实现。一般比较大的类都建议使用抽象类,当一个类被定义成一个抽象类过后。它就只能够被继承,不能够再使用 new 的方式去创建对应的实例对象。在这种情况下就必须使用子类去继承,另外在抽象类中也可以定义一些抽象方法,需要注意的是抽象方法不需要方法体。当父类当中有抽象方法时,子类就必须要去实现这样一个方法。此时使用子类创建的对象时,就会同时拥有父类当中的一些实例方法以及自身所实现的方法。
abstract class Animal {
eat (food: string):void {
console.log(`呼噜呼噜的吃:${food}`)
}
abstract run (distance: number): void
}
class Dog extends Animal {
run (distance:number):void {}
}
25、泛型 Generics
泛型就是指在定义函数、接口、类的时候没有指定具体的类型,等到使用的时候在去指定具体类型的一种特征。以函数为例,简单点说就是在声明这个函数时不去指定具体的类型。等到调用它的时候,才去传递一个具体的类型。这样做的目的就是为了极大程度的复用代码,
// 创建一个指定长度的数组
function createNumberArray (length: number, value: number):number[] {
const arr = Array<number>(length).fill(value);
return arr
}
const res1 = createNumberArray(3, 100)
如果我们还需要它可以创建一个,字符串类型的数组上面的函数就做不到了。这里最笨的办法就是再去定义一个叫做 creatStringArray 的函数,函数的逻辑都是一样的只不过 value 的类型是 spring 。
// 创建指定长度的数组
function createStringArray (length: number, value: string):string[] {
const arr = Array<srting>(length).fill(value);
return arr
}
const res2 = createStringArray(3,‘100’)
这样就可以看到两个函数实现的都是相同逻辑,重写两边就是冗余。更合理的办法就是使用泛型,说白了就是把这里的 string 或者 number 去变成一个参数。就是把类型变成一个参数,在调用的时候再去传递这个类型。去使用泛型具体就是在函数名后面去使用<泛型参数>,一般泛型参数都会以 T 作为名称,然后把函数当中不明确的类型都该用 T 去代表。
// 创建指定长度的数组
finction createArray<T> (length:number, value: T):T[] {
const arr = Array<T>(length).fill(value)
return arr
}
const string_arr = creatArray<string>(3, 'foo')
const number_arr = creatArray<number>(3, 100)
总结:总的来说泛型就是把定义时不能够明确的类型,变成一个参数然我们在使用的时候,再去传递这样的一个类型参数。
26、类型声明 Type Declaration
在实际的项目开发过程中难免会遇到第三方的 npm 模块,而这些 npm 模块并不一定都是通过 TS 编写的,所以说它所提供的成员就不会有强类型的体验。
import { cameCase } from 'lodash' // Could not find a declaration file from module 'lodash'. Try `npm install @types/lodash` if it exists or add a new declaration (.d.ts) file)
declare function camelCase (input: string):string // 类型声明
camelCase('hello typeScript') // (alias) camelCase(input: string):string
类型声明就是一个成员在定义的时候因为种种原因没有声明一个明确的类型,在使用的时候单独为它再做出一个明确的声明。这种用法存在的原因就是为了要考虑兼容一些普通的 JS 模块,由于 TS 的社区非常强大。目前一些绝大多数比较常用的 npm 模块都已经提供了对应的声明,只需要安装一下它所对应的类型声明模块。
// 安装 lodash 所对应的类型声明模块
yarn add @types/lodash -D
总结:在 TS 当中引用第三方模块,如果这个模块当中不包含所对饮的类型声明文件。就可以尝试去安装一个所对应的类型声明模块,这个类型声明模块的形式就是 @types/模块名称 。如果说也没有这样一个对应的类型声明模块,这种情况下就只能够自己使用 declare 语句去声明所对应的模块类型。