1.面试官:说说你对 TypeScript 中类的理解?应用场景?
一、是什么
类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础
类是一种用户定义的引用数据类型,也称类类型
传统的面向对象语言基本都是基于类的,JavaScript
基于原型的方式让开发者多了很多理解成本
在 ES6
之后,JavaScript
拥有了 class
关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多
但是 JavaScript
的class
依然有一些特性还没有加入,比如修饰符和抽象类
TypeScript
的 class
支持面向对象的所有特性,比如 类、接口等
二、使用方式
定义类的关键字为 class
,后面紧跟类名,类可以包含以下几个模块(类的数据成员):
字段 : 字段是类里面声明的变量。字段表示对象的有关数据。
构造函数 : 类实例化时调用,可以为类的对象分配内存。
如下例子:
class Car { // 字段 engine:string ; // 构造函数 constructor (engine:string ) { this .engine = engine } // 方法 disp():void { console .log("发动机为 : " +this .engine) } }
继承
类的继承使用过extends
的关键字
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();
Dog
是一个 派生类,它派生自 Animal
基类,派生类通常被称作子类,基类通常被称作 超类
Dog
类继承了Animal
类,因此实例dog
也能够使用Animal
类move
方法
同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过super
关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,如下:
class PrinterClass { doPrint():void { console .log("父类的 doPrint() 方法。" ) } }class StringPrinter extends PrinterClass { doPrint():void { super .doPrint() // 调用父类的函数 console .log("子类的 doPrint()方法。" ) } }
修饰符
可以看到,上述的形式跟ES6
十分的相似,typescript
在此基础上添加了三种修饰符:
公共 public:可以自由的访问类程序里定义的成员
受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问
私有修饰符
只能够在该类的内部进行访问,实例对象并不能够访问
并且继承该类的子类并不能访问,如下图所示:
受保护修饰符
跟私有修饰符很相似,实例对象同样不能访问受保护的属性,如下:
有一点不同的是 protected
成员在子类中仍然可以访问
除了上述修饰符之外,还有只读修饰符
只读修饰符
通过readonly
关键字进行声明,只读属性必须在声明时或构造函数里被初始化,如下:
除了实例属性之外,同样存在静态属性
静态属性
这些属性存在于类本身上面而不是类的实例上,通过static
进行定义,访问这些属性需要通过 类型.静态属性 的这种形式访问,如下所示:
class Square { static width = '100px' }console .log(Square.width) // 100px
上述的类都能发现一个特点就是,都能够被实例化,在 typescript
中,还存在一种抽象类
抽象类
抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法,如下所示:
abstract class Animal { abstract makeSound(): void ; move(): void { console .log('roaming the earch...' ); } }
这种类并不能被实例化,通常需要我们创建子类去继承,如下:
class Cat extends Animal { makeSound() { console .log('miao miao' ) } }const cat = new Cat() cat.makeSound() // miao miao cat.move() // roaming the earch...
三、应用场景
除了日常借助类的特性完成日常业务代码,还可以将类(class)也可以作为接口,尤其在 React
工程中是很常用的,如下:
export default class Carousel extends React.Component<Props, State> {}
由于组件需要传入 props
的类型 Props
,同时有需要设置默认 props
即 defaultProps
,这时候更加适合使用class
作为接口
先声明一个类,这个类包含组件 props
所需的类型和初始值:
// props的类型 export default class Props { public children: Array <React.ReactElement<any >> | React.ReactElement<any > | never[] = [] public speed: number = 500 public height: number = 160 public animation: string = 'easeInOutQuad' public isAuto: boolean = true public autoPlayInterval: number = 4500 public afterChange: () => {} public beforeChange: () => {} public selesctedColor: string public showDots: boolean = true }
当我们需要传入 props
类型的时候直接将 Props
作为接口传入,此时 Props
的作用就是接口,而当需要我们设置defaultProps
初始值的时候,我们只需要:
public static defaultProps = new Props()
Props
的实例就是 defaultProps
的初始值,这就是 class
作为接口的实际应用,我们用一个 class
起到了接口和设置初始值两个作用,方便统一管理,减少了代码量
参考文献
https://www.tslang.cn/docs/handbook/classes.html
https://www.runoob.com/typescript/ts-class.html
2.面试官:说说你对 TypeScript 的理解?与 JavaScript 的区别?
一、是什么
TypeScript
是 JavaScript
的类型的超集,支持ES6
语法,支持面向对象编程的概念,如类、接口、继承、泛型等
超集,不得不说另外一个概念,子集,怎么理解这两个呢,举个例子,如果一个集合 A 里面的的所有元素集合 B 里面都存在,那么我们可以理解集合 B 是集合 A 的超集,集合 A 为集合 B 的子集
其是一种静态类型检查的语言,提供了类型注解,在代码编译阶段就可以检查出数据类型的错误
同时扩展了 JavaScript
的语法,所以任何现有的 JavaScript
程序可以不加改变的在 TypeScript
下工作
为了保证兼容性,TypeScript
在编译阶段需要编译器编译成纯 JavaScript
来运行,是为大型应用之开发而设计的语言,如下:
ts
文件如下:
const hello: string = "Hello World!" ;console .log(hello);
编译文件后:
const hello = "Hello World!" ;console .log(hello);
二、特性
TypeScript
的特性主要有如下:
类型推断 :ts 中没有批注变量类型会自动推断变量的类型
类型擦除 :在编译过程中批注的内容和接口会在运行时利用工具擦除
名字空间 :名字只在该区域内有效,其他区域可重复使用该名字而不冲突
元组 :元组合并了不同类型的对象,相当于一个可以装不同类型数据的数组
类型批注
通过类型批注提供在编译时启动类型检查的静态类型,这是可选的,而且可以忽略而使用 JavaScript
常规的动态类型
function Add(left: number, right: number): number {
return left + right;
}
对于基本类型的批注是 number
、bool
和 string
,而弱或动态类型的结构则是 any
类型
类型推断
当类型没有给出时,TypeScript 编译器利用类型推断来推断类型,如下:
let str = "string" ;
变量 str
被推断为字符串类型,这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时
如果缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 any
类型
接口
接口简单来说就是用来描述对象的类型 数据的类型有 number
、null
、 string
等数据格式,对象的类型就是用接口来描述的
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: "Tom",
age: 25,
};
三、区别
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法
TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译
TypeScript 文件的后缀名 .ts (.ts,.tsx,.dts),JavaScript 文件是 .js
在编写 TypeScript 的文件的时候就会自动编译成 js 文件
更多的区别如下图所示:
参考文献
3.面试官:说说 typescript 的数据类型有哪些?
一、是什么
typescript
和 javascript
几乎一样,拥有相同的数据类型,另外在javascript
基础上提供了更加实用的类型供开发使用
在开发阶段,可以为明确的变量定义为某种类型,这样typescript
就能在编译阶段进行类型检查,当类型不合符预期结果的时候则会出现错误提示
二、有哪些
typescript
的数据类型主要有如下:
boolean
布尔类型
let flag:boolean = true;
// flag = 123; // 错误
flag = false; //正确
number
数字类型,和javascript
一样,typescript
的数值类型都是浮点数,可支持二进制、八进制、十进制和十六进制
let num:number = 123;
// num = '456'; // 错误
num = 456; //正确
进制表示:
let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
string
字符串类型,和JavaScript
一样,可以使用双引号("
)或单引号('
)表示字符串
let str:string = 'this is ts';
str = 'test';
作为超集,当然也可以使用模版字符串``进行包裹,通过 ${} 嵌入变量
let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }
array
数组类型,跟javascript
一致,通过[]
进行包裹,有两种写法:
方式一:元素类型后面接上 []
let arr:string[] = ['12', '23'];
arr = ['45', '56'];
方式二:使用数组泛型,Array<元素类型>
:
let arr:Array<number> = [1, 2];
arr = ['45', '56'];
tuple
元祖类型,允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
let tupleArr:[number, string, boolean];
tupleArr = [12, '34', true]; //ok
typleArr = [12, '34'] // no ok
赋值的类型、位置、个数需要和定义(生明)的类型、位置、个数一致
enum
enum
类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
any
可以指定任何类型的值,在编程阶段还不清楚类型的变量指定一个类型,不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查,这时候可以使用any
类型
使用any
类型允许被赋值为任意类型,甚至可以调用其属性、方法
let num:any = 123;
num = 'str';
num = true;
定义存储各种类型数据的数组时,示例代码如下:
let arrayList: any[] = [1, false, 'fine'];
arrayList[1] = 100;
null 和 和 undefined
在 JavaScript
中 null
表示 "什么都没有",是一个只有一个值的特殊类型,表示一个空对象引用,而undefined
表示一个没有设置值的变量
默认情况下null
和undefined
是所有类型的子类型, 就是说你可以把 null
和 undefined
赋值给 number
类型的变量
let num:number | undefined; // 数值类型 或者 undefined
console.log(num); // 正确
num = 123;
console.log(num); // 正确
但是ts
配置了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自
void
用于标识方法返回值的类型,表示该方法没有返回值。
function hello(): void {
alert("Hello Runoob");
}
never
never
是其他类型 (包括null
和 undefined
)的子类型,可以赋值给任何类型,代表从不会出现的值
但是没有类型是 never 的子类型,这意味着声明 never
的变量只能被 never
类型所赋值。
never
类型一般用来指定那些总是会抛出异常、无限循环
let a:never;
a = 123; // 错误的写法
a = (() => { // 正确的写法
throw new Error('错误');
})()
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
object
对象类型,非原始类型,常见的形式通过{}
进行包裹
let obj:object;
obj = {name: 'Wang', age: 25};
三、总结
和javascript
基本一致,也分成:
在基础类型上,typescript
增添了void
、any
、emum
等原始类型
参考文献
https://www.tslang.cn/docs/handbook/basic-types.html
4.面试官:说说你对 TypeScript 中枚举类型的理解?应用场景?
一、是什么
枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型
通俗来说,枚举就是一个对象的所有可能取值的集合
在日常生活中也很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就可以看成是一个枚举
枚举的说明与结构和联合相似,其形式为:
enum 枚举名{
标识符①[=整型常数],
标识符②[=整型常数],
...
标识符N[=整型常数],
}枚举变量;
二、使用
枚举的使用是通过enum
关键字进行定义,形式如下:
enum xxx { ... }
声明关键字为枚举类型的方式如下:
// 声明d为枚举类型Direction let d: Direction;
类型可以分成:
数字枚举
当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:
enum Direction { Up, // 值默认为 0 Down, // 值默认为 1 Left, // 值默认为 2 Right // 值默认为 3 }console .log(Direction.Up === 0 ); // true console .log(Direction.Down === 1 ); // true console .log(Direction.Left === 2 ); // true console .log(Direction.Right === 3 ); // true
如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加1:
enum Direction { Up = 10 , Down, Left, Right }console .log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13
字符串枚举
枚举类型的值其实也可以是字符串类型:enum Direction { Up = 'Up' , Down = 'Down' , Left = 'Left' , Right = 'Right' }console .log(Direction['Right' ], Direction.Up); // Right Up
如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:
enum Direction { Up = 'UP' , Down, // error TS1061: Enum member must have initializer Left, // error TS1061: Enum member must have initializer Right // error TS1061: Enum member must have initializer }
异构枚举
即将数字枚举和字符串枚举结合起来混合起来使用,如下:
enum BooleanLikeHeterogeneousEnum { No = 0 , Yes = "YES" , }
通常情况下我们很少会使用异构枚举
本质
现在一个枚举的案例如下:
enum Direction { Up, Down, Left, Right }
通过编译后,javascript
如下:
var Direction; (function (Direction ) { Direction[Direction["Up" ] = 0 ] = "Up" ; Direction[Direction["Down" ] = 1 ] = "Down" ; Direction[Direction["Left" ] = 2 ] = "Left" ; Direction[Direction["Right" ] = 3 ] = "Right" ; })(Direction || (Direction = {}));
上述代码可以看到, Direction[Direction["Up"] = 0] = "Up"
可以分成
所以定义枚举类型后,可以通过正反映射拿到对应的值,如下:
enum Direction { Up, Down, Left, Right }console .log(Direction.Up === 0 ); // true console .log(Direction[0 ]); // Up
并且多处定义的枚举是可以进行合并操作,如下:
enum Direction { Up = 'Up' , Down = 'Down' , Left = 'Left' , Right = 'Right' }enum Direction { Center = 1 }
编译后,js
代码如下:
var Direction; (function (Direction ) { Direction["Up" ] = "Up" ; Direction["Down" ] = "Down" ; Direction["Left" ] = "Left" ; Direction["Right" ] = "Right" ; })(Direction || (Direction = {})); (function (Direction ) { Direction[Direction["Center" ] = 1 ] = "Center" ; })(Direction || (Direction = {}));
可以看到,Direction
对象属性回叠加
三、应用场景
就拿回生活的例子,后端返回的字段使用 0 - 6 标记对应的日期,这时候就可以使用枚举可提高代码可读性,如下:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};console .log(Days["Sun" ] === 0 ); // true console .log(Days["Mon" ] === 1 ); // true console .log(Days["Tue" ] === 2 ); // true console .log(Days["Sat" ] === 6 ); // true
包括后端日常返回0、1 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护
参考文献
https://zh.wikipedia.org/wiki/%E6%9E%9A%E4%B8%BE
https://www.jianshu.com/p/b9e1caa4dd98
https://juejin.cn/post/6844904112669065224#heading-30
5.面试官:说说你对 TypeScript 中接口的理解?应用场景?
一、是什么
接口 是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类 去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法
简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法
typescript
的核心功能之一就是对类型做检测,虽然这种检测方式是“鸭式辨型法”,而接口的作用就是为为这些类型命名和为你的代码或第三方代码定义一个约定
二、使用方式
接口定义如下:
interface interface_name { }
例如有一个函数,这个函数接受一个 User
对象,然后返回这个 User
对象的 name
属性:
const getUserName = (user ) => user.name
可以看到,参数需要有一个user
的name
属性,可以通过接口描述user
参数的结构
interface User { name: string age: number }const getUserName = (user: User ) => user.name
这些属性并不一定全部实现,上述传入的对象必须拥有name
和age
属性,否则typescript
在编译阶段会报错,如下图:
如果不想要age
属性的话,这时候可以采用可选属性 ,如下表示:
interface User { name: string age?: number }
这时候age
属性则可以是number
类型或者undefined
类型
有些时候,我们想要一个属性变成只读属性,在typescript
只需要使用readonly
声明,如下:
interface User { name: string age?: number readonly isMale: boolean }
当我们修改属性的时候,就会出现警告,如下所示:
这是属性中有一个函数,可以如下表示:
interface User { name: string age?: number readonly isMale: boolean say: (words: string ) => string }
如果传递的对象不仅仅是上述的属性,这时候可以使用:
interface User { name: string age: number } const getUserName = (user: User) => user.name getUserName({color: 'yellow' } as User)
interface User { name: string age: number [propName: string ]: any ; }
接口还能实现继承,如下图:
也可以继承多个,父类通过逗号隔开,如下:
interface Father { color: String }interface Mother { height: Number }interface Son extends Father,Mother{ name: string age: Number }
三、应用场景
例如在javascript
中定义一个函数,用来获取用户的姓名和年龄:
const getUserInfo = function (user ) { // ... return name: ${user.name}, age : ${user.age} }
如果多人开发的都需要用到这个函数的时候,如果没有注释,则可能出现各种运行时的错误,这时候就可以使用接口定义参数变量:
// 先定义一个接口 interface IUser { name: string ; age: number ; }const getUserInfo = (user: IUser): string => { return `name: ${user.name} , age: ${user.age} ` ; };// 正确的调用 getUserInfo({name: "koala" , age: 18 });
包括后面讲到类的时候也会应用到接口
参考文献
https://www.tslang.cn/docs/handbook/interfaces.html
6.面试官:说说你对 TypeScript 中类的理解?应用场景?
一、是什么
类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础
类是一种用户定义的引用数据类型,也称类类型
传统的面向对象语言基本都是基于类的,JavaScript
基于原型的方式让开发者多了很多理解成本
在 ES6
之后,JavaScript
拥有了 class
关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多
但是 JavaScript
的class
依然有一些特性还没有加入,比如修饰符和抽象类
TypeScript
的 class
支持面向对象的所有特性,比如 类、接口等
二、使用方式
定义类的关键字为 class
,后面紧跟类名,类可以包含以下几个模块(类的数据成员):
字段 : 字段是类里面声明的变量。字段表示对象的有关数据。
构造函数 : 类实例化时调用,可以为类的对象分配内存。
如下例子:
class Car { // 字段 engine:string ; // 构造函数 constructor (engine:string ) { this .engine = engine } // 方法 disp():void { console .log("发动机为 : " +this .engine) } }
继承
类的继承使用过extends
的关键字
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();
Dog
是一个 派生类,它派生自 Animal
基类,派生类通常被称作子类,基类通常被称作 超类
Dog
类继承了Animal
类,因此实例dog
也能够使用Animal
类move
方法
同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过super
关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,如下:
class PrinterClass { doPrint():void { console .log("父类的 doPrint() 方法。" ) } }class StringPrinter extends PrinterClass { doPrint():void { super .doPrint() // 调用父类的函数 console .log("子类的 doPrint()方法。" ) } }
修饰符
可以看到,上述的形式跟ES6
十分的相似,typescript
在此基础上添加了三种修饰符:
公共 public:可以自由的访问类程序里定义的成员
受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问
私有修饰符
只能够在该类的内部进行访问,实例对象并不能够访问
并且继承该类的子类并不能访问,如下图所示:
受保护修饰符
跟私有修饰符很相似,实例对象同样不能访问受保护的属性,如下:
有一点不同的是 protected
成员在子类中仍然可以访问
除了上述修饰符之外,还有只读修饰符
只读修饰符
通过readonly
关键字进行声明,只读属性必须在声明时或构造函数里被初始化,如下:
除了实例属性之外,同样存在静态属性
静态属性
这些属性存在于类本身上面而不是类的实例上,通过static
进行定义,访问这些属性需要通过 类型.静态属性 的这种形式访问,如下所示:
class Square { static width = '100px' }console .log(Square.width) // 100px
上述的类都能发现一个特点就是,都能够被实例化,在 typescript
中,还存在一种抽象类
抽象类
抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法,如下所示:
abstract class Animal { abstract makeSound(): void ; move(): void { console .log('roaming the earch...' ); } }
这种类并不能被实例化,通常需要我们创建子类去继承,如下:
class Cat extends Animal { makeSound() { console .log('miao miao' ) } }const cat = new Cat() cat.makeSound() // miao miao cat.move() // roaming the earch...
三、应用场景
除了日常借助类的特性完成日常业务代码,还可以将类(class)也可以作为接口,尤其在 React
工程中是很常用的,如下:
export default class Carousel extends React.Component<Props, State> {}
由于组件需要传入 props
的类型 Props
,同时有需要设置默认 props
即 defaultProps
,这时候更加适合使用class
作为接口
先声明一个类,这个类包含组件 props
所需的类型和初始值:
// props的类型 export default class Props { public children: Array <React.ReactElement<any >> | React.ReactElement<any > | never[] = [] public speed: number = 500 public height: number = 160 public animation: string = 'easeInOutQuad' public isAuto: boolean = true public autoPlayInterval: number = 4500 public afterChange: () => {} public beforeChange: () => {} public selesctedColor: string public showDots: boolean = true }
当我们需要传入 props
类型的时候直接将 Props
作为接口传入,此时 Props
的作用就是接口,而当需要我们设置defaultProps
初始值的时候,我们只需要:
public static defaultProps = new Props()
Props
的实例就是 defaultProps
的初始值,这就是 class
作为接口的实际应用,我们用一个 class
起到了接口和设置初始值两个作用,方便统一管理,减少了代码量
参考文献
https://www.tslang.cn/docs/handbook/classes.html
https://www.runoob.com/typescript/ts-class.html
7.面试官:说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?
一、是什么
函数是 JavaScript
应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块
在 TypeScript
里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScript
为 JavaScript
函数添加了额外的功能,丰富了更多的应用场景
函数类型在 TypeScript
类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块
二、使用方式
跟javascript
定义函数十分相似,可以通过funciton
关键字、箭头函数等形式去定义,例如下面一个简单的加法函数:
const add = (a: number , b: number ) => a + b
上述只定义了函数的两个参数类型,这个时候整个函数虽然没有被显式定义,但是实际上 TypeScript
编译器是能够通过类型推断到这个函数的类型,如下图所示:
当鼠标放置在第三行add
函数名的时候,会出现完整的函数定义类型,通过:
的形式来定于参数类型,通过 =>
连接参数和返回值类型
当我们没有提供函数实现的情况下,有两种声明函数类型的方式,如下所示:
// 方式一 type LongHand = { (a: number ): number ; };// 方式二 type ShortHand = (a: number ) => number ;
当存在函数重载时,只能使用方式一的形式
可选参数
当函数的参数可能是不存在的,只需要在参数后面加上 ?
代表参数可能不存在,如下:
const add = (a: number , b?: number ) => a + (b ? b : 0 )
这时候参数b
可以是number
类型或者undefined
类型,即可以传一个number
类型或者不传都可以
剩余类型
剩余参数与JavaScript
的语法类似,需要用 ...
来表示剩余参数
如果剩余参数 rest
是一个由number
类型组成的数组,则如下表示:
const add = (a: number , ...rest: number [] ) => rest.reduce(((a, b ) => a + b ), a )
函数重载
允许创建数项名称相同但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力
关于typescript
函数重载,必须要把精确的定义放在前面,最后函数实现时,需要使用 |
操作符或者?
操作符,把所有可能的输入类型全部包含进去,用于具体实现
这里的函数重载也只是多个函数的声明,具体的逻辑还需要自己去写,typescript
并不会真的将你的多个重名 function
的函数体进行合并
例如我们有一个add函数,它可以接收 string
类型的参数进行拼接,也可以接收 number
类型的参数进行相加,如下:
// 上边是声明 function add (arg1: string , arg2: string ): string function add (arg1: number , arg2: number ): number // 因为我们在下边有具体函数的实现,所以这里并不需要添加 declare 关键字 // 下边是实现function add (arg1: string | number , arg2: string | number ) { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2 if (typeof arg1 === 'string ' && typeof arg2 === 'string ' ) { return arg1 + arg2 } else if (typeof arg1 === 'number ' && typeof arg2 === 'number ' ) { return arg1 + arg2 } }
三、区别
从上面可以看到:
从定义的方式而言,typescript 声明函数需要定义参数类型或者声明返回值类型
typescript 在参数中,添加可选参数供使用者选择
typescript 增添函数重载功能,使用者只需要通过查看函数声明的方式,即可知道函数传递的参数个数以及类型
参考文献
https://www.tslang.cn/docs/handbook/functions.html
https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD
https://jkchao.github.io/typescript-book-chinese/typings/functions.html#%E9%87%8D%E8%BD%BD
8.面试官:说说你对 TypeScript 中泛型的理解?应用场景?
一、是什么
泛型程序设计(generic programming)是程序设计语言的一种风格或范式
泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型 在typescript
中,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性
假设我们用一个函数,它可接受一个 number
参数并返回一个 number
参数,如下写法:
function returnItem (para: number ): number { return para }
如果我们打算接受一个 string
类型,然后再返回 string
类型,则如下写法:
function returnItem (para: string ): string { return para }
上述两种编写方式,存在一个最明显的问题在于,代码重复度比较高
虽然可以使用 any
类型去替代,但这也并不是很好的方案,因为我们的目的是接收什么类型的参数返回什么类型的参数,即在运行时传入参数我们才能确定类型
这种情况就可以使用泛型,如下所示:
function returnItem <T >(para: T ): T { return para }
可以看到,泛型给予开发者创造灵活、可重用代码的能力
二、使用方式
泛型通过<>
的形式进行表述,可以声明:
函数声明
声明函数的形式如下:
function returnItem <T >(para: T ): T { return para }
定义泛型的时候,可以一次定义多个类型参数 ,比如我们可以同时定义泛型 T
和 泛型 U
:
function swap <T , U >(tuple: [T, U] ): [U , T ] { return [tuple[1 ], tuple[0 ]]; } swap([7 , 'seven' ]); // ['seven', 7]
接口声明
声明接口的形式如下:
interface ReturnItemFn<T> { (para: T): T }
那么当我们想传入一个number作为参数的时候,就可以这样声明函数:
const returnItem: ReturnItemFn<number > = para => para
类声明
使用泛型声明类的时候,既可以作用于类本身,也可以作用与类的成员函数
下面简单实现一个元素同类型的栈结构,如下所示:
class Stack<T> { private arr: T[] = [] public push(item: T) { this .arr.push(item) } public pop() { this .arr.pop() } }
使用方式如下:
const stack = new Stacn<number >()
如果上述只能传递 string
和 number
类型,这时候就可以使用 <T extends xx>
的方式猜实现约束泛型 ,如下所示:
除了上述的形式,泛型更高级的使用如下:
例如要设计一个函数,这个函数接受两个参数,一个参数为对象,另一个参数为对象上的属性,我们通过这两个参数返回这个属性的值
这时候就设计到泛型的索引类型和约束类型共同实现
索引类型、约束类型
索引类型 keyof T
把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U 被约束在这个联合类型中,如下所示:
function getValue <T extends object , U extends keyof T >(obj: T, key: U ) { return obj[key] // ok }
上述为什么需要使用泛型约束,而不是直接定义第一个参数为 object
类型,是因为默认情况 object
指的是{}
,而我们接收的对象是各种各样的,一个泛型来表示传入的对象类型,比如 T extends object
使用如下图所示:
多类型约束
例如如下需要实现两个接口的类型约束:
interface FirstInterface { doSomething(): number }interface SecondInterface { doSomethingElse(): string }
可以创建一个接口继承上述两个接口,如下:
interface ChildInterface extends FirstInterface, SecondInterface { }
正确使用如下:
class Demo<T extends ChildInterface> { private genericProperty: T constructor (genericProperty: T ) { this .genericProperty = genericProperty } useT() { this .genericProperty.doSomething() this .genericProperty.doSomethingElse() } }
通过泛型约束就可以达到多类型约束的目的
三、应用场景
通过上面初步的了解,后述在编写 typescript
的时候,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性的时候,这种情况下就可以使用泛型
灵活的使用泛型定义类型,是掌握typescript
必经之路
参考文献
https://www.tslang.cn/docs/handbook/generics.html
9.面试官:说说你对 TypeScript 中高级类型的理解?有哪些?
一、是什么
除了string
、number
、boolean
这种基础类型外,在 typescript
类型声明中还存在一些高级的类型应用
这些高级类型,是typescript
为了保证语言的灵活性,所使用的一些语言特性。这些特性有助于我们应对复杂多变的开发场景
二、有哪些
常见的高级类型有如下:
交叉类型
通过 &
将多个类型合并为一个类型,包含了所需的所有类型的特性,本质上是一种并的操作
语法如下:
T & U
适用于对象合并场景,如下将声明一个函数,将两个对象合并成一个对象并返回:
function extend <T , U >(first: T, second: U ) : T & U { let result: <T & U> = {} for (let key in first) { result[key] = first[key] } for (let key in second) { if (!result.hasOwnProperty(key)) { result[key] = second[key] } } return result }
联合类型
联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个,本质上是一个交的关系
语法如下:
T | U
例如 number
| string
| boolean
的类型只能是这三个的一种,不能共存
如下所示:
function formatCommandline (command: string [] | string ) { let line = '' ; if (typeof command === 'string' ) { line = command.trim(); } else { line = command.join(' ' ).trim(); } }
类型别名
类型别名会给一个类型起个新名字,类型别名有时和接口很像,但是可以作用于原始值、联合类型、元组以及其它任何你需要手写的类型
可以使用 type SomeName = someValidTypeAnnotation
的语法来创建类型别名:
type some = boolean | string const b: some = true // ok const c: some = 'hello' // ok const d: some = 123 // 不能将类型“123”分配给类型“some”
此外类型别名可以是泛型:
type Container<T> = { value: T };
也可以使用类型别名来在属性里引用自己:
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; }
可以看到,类型别名和接口使用十分相似,都可以描述一个对象或者函数
两者最大的区别在于,interface
只能用于定义对象类型,而 type
的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛
类型索引
keyof
类似于 Object.keys
,用于获取一个接口中 Key 的联合类型。
interface Button { type : string text: string }type ButtonKeys = keyof Button// 等效于 type ButtonKeys = "type" | "text"
类型约束
通过关键字 extend
进行约束,不同于在 class
后使用 extends
的继承作用,泛型内使用的主要作用是对泛型加以约束
type BaseType = string | number | boolean // 这里表示 copy 的参数 // 只能是字符串、数字、布尔这几种基础类型 function copy <T extends BaseType >(arg: T ): T { return arg }
类型约束通常和类型索引一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extends
和 keyof
进行约束。
function getValue <T , K extends keyof T >(obj: T, key: K ) { return obj[key] }const obj = { a: 1 }const a = getValue(obj, 'a' )
映射类型
通过 in
关键字做类型的映射,遍历已有接口的 key
或者是遍历联合类型,如下例子:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };interface Obj { a: string b: string }type ReadOnlyObj = Readonly<Obj>
上述的结构,可以分成这些步骤:
keyof T:通过类型索引 keyof 的得到联合类型 'a' | 'b'
P in keyof T 等同于 p in 'a' | 'b',相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b'
所以最终ReadOnlyObj
的接口为下述:
interface ReadOnlyObj { readonly a: string ; readonly b: string ; }
条件类型
条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。
T extends U ? X : Y
上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y
三、总结
可以看到,如果只是掌握了 typeScript
的一些基础类型,可能很难游刃有余的去使用 typeScript
,需要了解一些typescript
的高阶用法
并且typescript
在版本的迭代中新增了很多功能,需要不断学习与掌握
参考文献
https://www.tslang.cn/docs/handbook/advanced-types.html
https://juejin.cn/post/6844904003604578312
https://zhuanlan.zhihu.com/p/103846208
10.面试官:说说你对 TypeScript 装饰器的理解?应用场景?
一、是什么
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上
是一种在不改变原类和使用继承的情况下,动态地扩展对象功能
同样的,本质也不是什么高大上的结构,就是一个普通的函数,@expression
的形式其实是Object.defineProperty
的语法糖
expression
求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入
二、使用方式
由于typescript
是一个实验性特性,若要使用,需要在tsconfig.json
文件启动,如下:
{ "compilerOptions" : { "target" : "ES5" , "experimentalDecorators" : true } }
typescript
装饰器的使用和javascript
基本一致
类的装饰器可以装饰:
类装饰
例如声明一个函数 addAge
去给 Class 的属性 age
添加年龄.
function addAge (constructor: Function ) { constructor .prototype.age = 18; }@addAge class Person{ name: string ; age!: number ; constructor ( ) { this .name = 'huihui' ; } }let person = new Person();console .log(person.age); // 18
上述代码,实际等同于以下形式:
Person = addAge(function Person ( ) { ... });
上述可以看到,当装饰器作为修饰类的时候,会把构造器传递进去。 constructor.prototype.age
就是在每一个实例化对象上面添加一个 age
属性
方法/属性装饰
同样,装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:
可以看到,这三个属性实际就是Object.defineProperty
的三个参数,如果是类的属性,则没有传递第三个参数
如下例子:
// 声明装饰器修饰方法/属性 function method (target: any , propertyKey: string , descriptor: PropertyDescriptor ) { console .log(target); console .log("prop " + propertyKey); console .log("desc " + JSON .stringify(descriptor) + "\n\n" ); descriptor.writable = false ; };function property (target: any , propertyKey: string ) { console .log("target" , target) console .log("propertyKey" , propertyKey) }class Person{ @property name: string ; constructor ( ) { this .name = 'huihui' ; } @method say(){ return 'instance method' ; } @method static run(){ return 'static method' ; } }const xmz = new Person();// 修改实例方法say xmz.say = function ( ) { return 'edit' }
输出如下图所示:
参数装饰
接收3个参数,分别是:
function logParameter (target: Object , propertyName: string , index: number ) { console .log(target); console .log(propertyName); console .log(index); }class Employee { greet(@logParameter message: string ): string { return `hello ${message} ` ; } }const emp = new Employee(); emp.greet('hello' );
输入如下图:
访问器装饰
使用起来方式与方法装饰一致,如下:
function modification (target: Object , propertyKey: string , descriptor: PropertyDescriptor ) { console .log(target); console .log("prop " + propertyKey); console .log("desc " + JSON .stringify(descriptor) + "\n\n" ); };class Person{ _name: string ; constructor ( ) { this ._name = 'huihui' ; } @modification get name() { return this ._name } }
装饰器工厂
如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可,如下:
function addAge (age: number ) { return function (constructor: Function ) { constructor .prototype.age = age } } @addAge(10 ) class Person{ name: string ; age!: number ; constructor ( ) { this .name = 'huihui' ; } }let person = new Person();
执行顺序
当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用,例如如下:
function f ( ) { console .log("f(): evaluated" ); return function (target, propertyKey: string , descriptor: PropertyDescriptor ) { console .log("f(): called" ); } }function g ( ) { console .log("g(): evaluated" ); return function (target, propertyKey: string , descriptor: PropertyDescriptor ) { console .log("g(): called" ); } }class C { @f () @g () method() {} }// 输出 f(): evaluated g(): evaluated g(): called f(): called
三、应用场景
可以看到,使用装饰器存在两个显著的优点:
后面的使用场景中,借助装饰器的特性,除了提高可读性之后,针对已经存在的类,可以通过装饰器的特性,在不改变原有代码情况下,对原来功能进行扩展
参考文献
https://www.tslang.cn/docs/handbook/decorators.html
https://juejin.cn/post/6844903876605280269#heading-5
11.面试官:说说对 TypeScript 中命名空间与模块的理解?区别?
一、模块
TypeScript
与 ECMAScript
2015 一样,任何包含顶级 import
或者 export
的文件都被当成一个模块
相反地,如果一个文件不带有顶级的import
或者export
声明,那么它的内容被视为全局可见的
例如我们在在一个 TypeScript
工程下建立一个文件 1.ts
,声明一个变量a
,如下:
const a = 1
然后在另一个文件同样声明一个变量a
,这时候会出现错误信息
提示重复声明a
变量,但是所处的空间是全局的
如果需要解决这个问题,则通过import
或者export
引入模块系统即可,如下:
const a = 10 ;export default a
在typescript
中,export
关键字可以导出变量或者类型,用法与es6
模块一致,如下:
export const a = 1 export type Person = { name: String }
通过import
引入模块,如下:
import { a, Person } from './export' ;
二、命名空间
命名空间一个最明确的目的就是解决重名问题
命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的
这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中
TypeScript
中命名空间使用 namespace
来定义,语法格式如下:
namespace SomeNameSpaceName { export interface ISomeInterfaceName { } export class SomeClassName { } }
以上定义了一个命名空间 SomeNameSpaceName
,如果我们需要在外部可以调用 SomeNameSpaceName
中的类和接口,则需要在类和接口添加 export
关键字
使用方式如下:
SomeNameSpaceName.SomeClassName
命名空间本质上是一个对象,作用是将一系列相关的全局变量组织到一个对象的属性,如下:
namespace Letter { export let a = 1 ; export let b = 2 ; export let c = 3 ; // ... export let z = 26 ; }
编译成js
如下:
var Letter; (function (Letter ) { Letter.a = 1 ; Letter.b = 2 ; Letter.c = 3 ; // ... Letter.z = 26 ; })(Letter || (Letter = {}));
三、区别
命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象,使用起来十分容易。但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中
像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖
在正常的TS项目开发过程中并不建议用命名空间,但通常在通过 d.ts 文件标记 js 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用
参考文献
https://www.tslang.cn/docs/handbook/modules.html
https://www.tslang.cn/docs/handbook/namespaces.html
https://www.tslang.cn/docs/handbook/namespaces-and-modules.html