前端面试大全TypeScript

前端面试题大全(TypeScript)

前端面试题类目分类

  • HTML5 + CSS3
  • JavaScript
  • Vue + Vue3
  • React
  • Webpack
  • 服务端

考点频率 :♥︎ 、 ♥︎ ♥︎、 ♥︎ ♥︎ ♥︎、 ♥︎ ♥︎ ♥︎ ♥︎、 ♥︎ ♥︎ ♥︎ ♥︎ ♥︎

TypeScript

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 typescript 的理解?与 javascript 的区别?

一、是什么

TypeScriptJavaScript 的类型的超集,支持ES6语法,支持面向对象编程的概念,如类、接口、继承、泛型等

超集,不得不说另外一个概念,子集,怎么理解这两个呢,举个例子,如果一个集合A里面的的所有元素集合B里面都存在,那么我们可以理解集合B是集合A的超集,集合A为集合B的子集

其是一种静态类型检查的语言,提供了类型注解,在代码编译阶段就可以检查出数据类型的错误

同时扩展了 JavaScript 的语法,所以任何现有的 JavaScript 程序可以不加改变的在 TypeScript 下工作

为了保证兼容性,typescript在编译阶段需要编译器编译成纯Javascript来运行,是为大型应用之开发而设计的语言,如下:

tsx文件如下:

const hello : string = "Hello World!"
console.log(hello)

编译文件后:

const hello = "Hello World!"
console.log(hello)

二、特性

typescript的特性主要有如下:

  • 类型批注和编译时类型检查 :在编译时批注变量类型
  • 类型推断:ts中没有批注变量类型会自动推断变量的类型
  • 类型擦除:在编译过程中批注的内容和接口会在运行时利用工具擦除
  • 接口:ts中用接口来定义对象类型
  • 枚举:用于取值被限定在一定范围内的场景
  • Mixin:可以接受任意类型的值
  • 泛型编程:写代码时使用一些以后才指定的类型
  • 名字空间:名字只在该区域内有效,其他区域可重复使用该名字而不冲突
  • 元组:元组合并了不同类型的对象,相当于一个可以装不同类型数据的数组
类型批注

通过类型批注提供在编译时启动类型检查的静态类型,这是可选的,而且可以忽略而使用JavaScript常规的动态类型

function Add(left: number, right: number): number {
 return left + right;
}

对于基本类型的批注是numberboolstring,而弱或动态类型的结构则是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 文件

更多的区别如下图所示:

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ typescript 的数据类型有哪些?

一、是什么

typescriptjavascript几乎一样,拥有相同的数据类型,另外在javascript基础上提供了更加实用的类型供开发使用

在开发阶段,可以为明确的变量定义为某种类型,这样typescript就能在编译阶段进行类型检查,当类型不合符预期结果的时候则会出现错误提示

二、有哪些

typescript 的数据类型主要有如下:

  • boolean(布尔类型)
  • number(数字类型)
  • string(字符串类型)
  • array(数组类型)
  • tuple(元组类型)
  • enum(枚举类型)
  • any(任意类型)
  • null 和 undefined 类型
  • Symbol 类型
  • bigint类型
  • void 类型
  • never 类型
  • object 对象类型
  • unknown
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

JavaScriptnull 表示 “什么都没有”,是一个只有一个值的特殊类型,表示一个空对象引用,而undefined表示一个没有设置值的变量

默认情况下nullundefined是所有类型的子类型, 就是说你可以把 null undefined 赋值给 number 类型的变量

let num:number | undefined; // 数值类型 或者 undefined
console.log(num); // 正确
num = 123;
console.log(num); // 正确

但是ts配置了--strictNullChecks标记,nullundefined只能赋值给void和它们各自

void

用于标识方法返回值的类型,表示该方法没有返回值。

function hello(): void {
    alert("Hello Runoob");
}
Symbol

symbol我们平时用的比较少,所以可能了解也不是很多,这里就详细来说说symbol。

(1)symbol 基本使用

symbol 是 ES6 新增的一种基本数据类型,它用来表示独一无二的值,可以通过 Symbol 构造函数生成。

const s = Symbol(); 
typeof s; // symbol

注意:Symbol 前面不能加 new关键字,直接调用即可创建一个独一无二的 symbol 类型的值。

可以在使用 Symbol 方法创建 symbol 类型值的时候传入一个参数,这个参数需要是一个字符串。如果传入的参数不是字符串,会先自动调用传入参数的 toString 方法转为字符串:

const s1 = Symbol("TypeScript"); 
const s2 = Symbol("Typescript"); 
console.log(s1 === s2); // false

上面代码的第三行可能会报一个错误:This condition will always return ‘false’ since the types ‘unique symbol’ and ‘unique symbol’ have no overlap. 这是因为编译器检测到这里的 s1 === s2 始终是false,所以编译器提醒这代码写的多余,建议进行优化。

上面使用Symbol创建了两个symbol对象,方法中都传入了相同的字符串,但是两个symbol值仍然是false,这就说明了 Symbol 方法会返回一个独一无二的值。Symbol 方法传入的这个字符串,就是方便我们区分 symbol 值的。可以调用 symbol 值的 toString 方法将它转为字符串:

const s1 = Symbol("Typescript"); 
console.log(s1.toString());  // 'Symbol(Typescript)'
console.log(Boolean(s));     // true 
console.log(!s);             // false

在TypeScript中使用symbol就是指定一个值的类型为symbol类型:

let a: symbol = Symbol()

TypeScript 中还有一个 unique symbol 类型,它是symbol的子类型,这种类型的值只能由Symbol()Symbol.for()创建,或者通过指定类型来指定变量是这种类型。这种类型的值只能用于常量的定义和用于属性名。需要注意,定义unique symbol类型的值,必须用 const 而不能用let来声明。下面来看在TypeScript中使用Symbol值作为属性名的例子:

const key1: unique symbol = Symbol()
let key2: symbol = Symbol()
const obj = {
    [key1]: 'value1',
    [key2]: 'value2'
}
console.log(obj[key1]) // value1
console.log(obj[key2]) // error 类型“symbol”不能作为索引类型使用。
(2)symbol 作为属性名

在ES6中,对象的属性是支持表达式的,可以使用于一个变量来作为属性名,这对于代码的简化有很多用处,表达式必须放在大括号内:

let prop = "name"; 
const obj = { 
  [prop]: "TypeScript" 
};
console.log(obj.name); // 'TypeScript'

symbol 也可以作为属性名,因为symbol的值是独一无二的,所以当它作为属性名时,不会与其他任何属性名重复。当需要访问这个属性时,只能使用这个symbol值来访问(必须使用方括号形式来访问):

let name = Symbol(); 
let obj = { 
  [name]: "TypeScript" 
};
console.log(obj); // { Symbol(): 'TypeScript' }
console.log(obj[name]); // 'TypeScript' 
console.log(obj.name);  // undefined

在使用obj.name访问时,实际上是字符串name,这和访问普通字符串类型的属性名是一样的,要想访问属性名为symbol类型的属性时,必须使用方括号。方括号中的name才是我们定义的symbol类型的变量name。

(3)symbol 属性名遍历

使用 Symbol 类型值作为属性名,这个属性是不会被 for…in遍历到的,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 、 JSON.stringify() 等方法获取到:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript", 
  age: 18 
};
for (const key in obj) { 
  console.log(key); 
}  
// => 'age' 
console.log(Object.keys(obj));  // ['age'] 
console.log(Object.getOwnPropertyNames(obj));  // ['age'] 
console.log(JSON.stringify(obj)); // '{ "age": 18 }

虽然这些方法都不能访问到Symbol类型的属性名,但是Symbol类型的属性并不是私有属性,可以使用 Object.getOwnPropertySymbols 方法获取对象的所有symbol类型的属性名:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript", 
  age: 18 
};
const SymbolPropNames = Object.getOwnPropertySymbols(obj); 
console.log(SymbolPropNames); // [ Symbol(name) ] 
console.log(obj[SymbolPropNames[0]]); // 'TypeScript' 

除了这个方法,还可以使用ES6提供的 Reflect 对象的静态方法 Reflect.ownKeys ,它可以返回所有类型的属性名,Symbol 类型的也会返回:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript", 
  age: 18 
};
console.log(Reflect.ownKeys(obj)); // [ 'age', Symbol(name) ]
(4)symbol 静态方法

Symbol 包含两个静态方法, for 和 keyFor 。

1)Symbol.for()

用Symbol创建的symbol类型的值都是独一无二的。使用 Symbol.for 方法传入字符串,会先检查有没有使用该字符串调用 Symbol.for 方法创建的 symbol 值。如果有,返回该值;如果没有,则使用该字符串新创建一个。使用该方法创建 symbol 值后会在全局范围进行注册。

const iframe = document.createElement("iframe"); 
iframe.src = String(window.location); 
document.body.appendChild(iframe); 

iframe.contentWindow.Symbol.for("TypeScript") === Symbol.for("TypeScript"); // true // 注意:如果你在JavaScript环境中这段代码是没有问题的,但是如果在TypeScript开发环境中,可能会报错:类型“Window”上不存在属性“Symbol”。 // 因为这里编译器推断出iframe.contentWindow是Window类型,但是TypeScript的声明文件中,对Window的定义缺少Symbol这个字段,所以会报错,

上面代码中,创建了一个iframe节点并把它放在body中,通过这个 iframe 对象的 contentWindow 拿到这个 iframe 的 window 对象,在 iframe.contentWindow上添加一个值就相当于在当前页面定义一个全局变量一样。可以看到,在 iframe 中定义的键为 TypeScript 的 symbol 值在和在当前页面定义的键为’TypeScript’的symbol 值相等,说明它们是同一个值。

2)Symbol.keyFor()

该方法传入一个 symbol 值,返回该值在全局注册的键名:

const sym = Symbol.for("TypeScript"); 
console.log(Symbol.keyFor(sym)); // 'TypeScript'
bigint

BigInt是ES6中新引入的数据类型,它是一种内置对象,它提供了一种方法来表示大于 2- 1 的整数,BigInt可以表示任意大的整数。

使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了JavaScript构造函数 Number 能够表示的安全整数范围。

我们知道,在 JavaScript 中采用双精度浮点数,这导致精度有限,比如 Number.MAX_SAFE_INTEGER 给出了可以安全递增的最大可能整数,即2- 1,来看一个例子:

const max = Number.MAX_SAFE_INTEGER;
const max1 = max + 1
const max2 = max + 2
max1 === max2     // true

可以看到,最终返回了true,这就是超过精读范围造成的问题,而BigInt正是解决这类问题而生的:

const max = BigInt(Number.MAX_SAFE_INTEGER);
const max1 = max + 1n
const max2 = max + 2n
max1 === max2    // false

这里需要用 BigInt(number) 把 Number 转化为 BigInt,同时如果类型是 BigInt ,那么数字后面需要加 n

在TypeScript中,number 类型虽然和 BigInt 都表示数字,但是实际上两者类型是完全不同的:

declare let foo: number;
declare let bar: bigint;
foo = bar; // error: Type 'bigint' is not assignable to type 'number'.
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.
never

never是其他类型 (包括nullundefined)的子类型,可以赋值给任何类型,代表从不会出现的值

但是没有类型是 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};
unknown

unknown 是TypeScript在3.0版本新增的类型,主要用来描述类型并不确定的变量。它看起来和any很像,但是还是有区别的,unknown相对于any更安全。

对于any,来看一个例子:

let value: any
console.log(value.name)
console.log(value.toFixed())
console.log(value.length)

上面这些语句都不会报错,因为value是any类型,所以后面三个操作都有合法的情况,当value是一个对象时,访问name属性是没问题的;当value是数值类型的时候,调用它的toFixed方法没问题;当value是字符串或数组时获取它的length属性是没问题的。

当指定值为unknown类型的时候,如果没有缩小类型范围的话,是不能对它进行任何操作的。总之,unknown类型的值不能随便操作。那什么是类型范围缩小呢?下面来看一个例子:

function getValue(value: unknown): string {
  if (value instanceof Date) { 
    return value.toISOString();
  }
  return String(value);
}

这里由于把value的类型缩小为Date实例的范围内,所以进行了value.toISOString(),也就是使用ISO标准将 Date 对象转换为字符串。

使用以下方式也可以缩小类型范围:

let result: unknown;
if (typeof result === 'number') {
  result.toFixed();
}

关于 unknown 类型,在使用时需要注意以下几点:

  • 任何类型的值都可以赋值给 unknown 类型:
let value1: unknown;
value1 = "a";
value1 = 123;
  • unknown 不可以赋值给其它类型,只能赋值给 unknown 和 any 类型:
let value2: unknown;
let value3: string = value2; // error 不能将类型“unknown”分配给类型“string”
value1 = value2;
  • unknown 类型的值不能进行任何操作:
let value4: unknown;
value4 += 1; // error 对象的类型为 "unknown"
  • 只能对 unknown 进行等或不等操作,不能进行其它操作:
value1 === value2;
value1 !== value2;
value1 += value2;  // error
  • unknown 类型的值不能访问其属性、作为函数调用和作为类创建实例:
let value5: unknown;
value5.age;   // error
value5();     // error
new value5(); // error

在实际使用中,如果有类型无法确定的情况,要尽量避免使用 any,因为 any 会丢失类型信息,一旦一个类型被指定为 any,那么在它上面进行任何操作都是合法的,所以会有意想不到的情况发生。因此如果遇到无法确定类型的情况,要先考虑使用 unknown。

三、总结

javascript基本一致,也分成:

  • 基本类型
  • 引用类型

在基础类型上,typescript增添了voidanyemum等原始类型

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 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"可以分成

  • Direction[“Up”] = 0
  • Direction[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 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 TypeScript 中接口的理解?应用场景?

一、是什么

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法

简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法

typescript的核心功能之一就是对类型做检测,虽然这种检测方式是“鸭式辨型法”,而接口的作用就是为为这些类型命名和为你的代码或第三方代码定义一个约定

二、使用方式

接口定义如下:

interface interface_name {
}

例如有一个函数,这个函数接受一个 User 对象,然后返回这个 User 对象的 name 属性:

const getUserName = (user) => user.name

可以看到,参数需要有一个username属性,可以通过接口描述user参数的结构

interface User {
    name: string
    age: number
}

const getUserName = (user: User) => user.name

这些属性并不一定全部实现,上述传入的对象必须拥有nameage属性,否则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});

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 TypeScript 中类的理解?应用场景?

一、是什么

类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础

类是一种用户定义的引用数据类型,也称类类型

传统的面向对象语言基本都是基于类的,JavaScript 基于原型的方式让开发者多了很多理解成本

ES6 之后,JavaScript 拥有了 class 关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多

但是 JavaScriptclass依然有一些特性还没有加入,比如修饰符和抽象类

TypeScriptclass 支持面向对象的所有特性,比如 类、接口等

二、使用方式

定义类的关键字为 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也能够使用Animalmove方法

同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过super关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,如下:

class PrinterClass {
   doPrint():void {
      console.log("父类的 doPrint() 方法。")
   }
}

class StringPrinter extends PrinterClass {
   doPrint():void {
      super.doPrint() // 调用父类的函数
      console.log("子类的 doPrint()方法。")
   }
}
修饰符

可以看到,上述的形式跟ES6十分的相似,typescript在此基础上添加了三种修饰符:

  • 公共 public:可以自由的访问类程序里定义的成员
  • 私有 private:只能够在该类的内部进行访问
  • 受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问
私有修饰符

只能够在该类的内部进行访问,实例对象并不能够访问

并且继承该类的子类并不能访问,如下图所示:

受保护修饰符

跟私有修饰符很相似,实例对象同样不能访问受保护的属性,如下:

有一点不同的是 protected 成员在子类中仍然可以访问

除了上述修饰符之外,还有只读修饰符

只读修饰符

通过readonly关键字进行声明,只读属性必须在声明时或构造函数里被初始化,如下:

img

除了实例属性之外,同样存在静态属性

静态属性

这些属性存在于类本身上面而不是类的实例上,通过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 ,同时有需要设置默认 propsdefaultProps,这时候更加适合使用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 起到了接口和设置初始值两个作用,方便统一管理,减少了代码量

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 TypeScript 中函数的理解?与 JavaScript 函数的区别?

一、是什么

函数是 JavaScript 应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块

TypeScript 里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScriptJavaScript 函数添加了额外的功能,丰富了更多的应用场景

函数类型在 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 增添函数重载功能,使用者只需要通过查看函数声明的方式,即可知道函数传递的参数个数以及类型

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 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>()

如果上述只能传递 stringnumber 类型,这时候就可以使用 <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 必经之路

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 TypeScript 中高级类型的理解?有哪些?

一、是什么

除了stringnumberboolean 这种基础类型外,在 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
}

类型约束通常和类型索引一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extendskeyof 进行约束。

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在版本的迭代中新增了很多功能,需要不断学习与掌握

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 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 属性

方法/属性装饰

同样,装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:

  • target:对象的原型
  • propertyKey:方法的名称
  • descriptor:方法的属性描述符

可以看到,这三个属性实际就是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个参数,分别是:

  • target :当前对象的原型
  • propertyKey :参数的名称
  • index:参数数组中的位置
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

三、应用场景

可以看到,使用装饰器存在两个显著的优点:

  • 代码可读性变强了,装饰器命名相当于一个注释
  • 在不改变原有代码情况下,对原来功能进行扩展

后面的使用场景中,借助装饰器的特性,除了提高可读性之后,针对已经存在的类,可以通过装饰器的特性,在不改变原有代码情况下,对原来功能进行扩展

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 对 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 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 如何在Vue项目中应用TypeScript?

一、前言

与link类似

VUE项目中应用typescript,我们需要引入一个库vue-property-decorator

其是基于vue-class-component库而来,这个库vue官方推出的一个支持使用class方式来开发vue单文件组件的库

主要的功能如下:

  • methods 可以直接声明为类的成员方法
  • 计算属性可以被声明为类的属性访问器
  • 初始化的 data 可以被声明为类属性
  • data、render 以及所有的 Vue 生命周期钩子可以直接作为类的成员方法
  • 所有其他属性,需要放在装饰器中

二、使用

vue-property-decorator 主要提供了多个装饰器和一个函数:

  • @prop
  • @ProPsync
  • @model
  • @watch
  • @provide
  • @Inject
  • @ProvideReactive
  • @InjectReactive
  • @emit
  • @ref
  • @component (由 vue-class-component 提供)
  • Mixins (由 vue-class-component 提供)
@component

Component装饰器它注明了此类为一个Vue组件,因此即使没有设置选项也不能省略

如果需要定义比如 namecomponentsfiltersdirectives以及自定义属性,就可以在Component装饰器中定义,如下:

import {Component,Vue} from 'vue-property-decorator';
import {componentA,componentB} from '@/components';

 @Component({
    components:{
        componentA,
        componentB,
    },
    directives: {
        focus: {
            // 指令的定义
            inserted: function (el) {
                el.focus()
            }
        }
    }
})
export default class YourCompoent extends Vue{
   
}
computed、data、methods

这里取消了组件的data和methods属性,以往data返回对象中的属性、methods中的方法需要直接定义在Class中,当做类的属性和方法

@Component
export default class HelloDecorator extends Vue {
    count: number = 123 // 类属性相当于以前的 data

    add(): number { // 类方法就是以前的方法
        this.count + 1
    }

    // 获取计算属性
    get total(): number {
      return this.count + 1
    }

    // 设置计算属性
    set total(param:number): void {
      this.count = param
    }
}
@props

组件接收属性的装饰器,如下使用:

import {Component,Vue,Prop} from vue-property-decorator;

@Component
export default class YourComponent extends Vue {
    @Prop(String)
    propA:string;
    
    @Prop([String,Number])
    propB:string|number;
    
    @Prop({
     type: String, // type: [String , Number]
     default: 'default value', // 一般为String或Number
      //如果是对象或数组的话。默认值从一个工厂函数中返回
      // defatult: () => {
      //     return ['a','b']
      // }
     required: true,
     validator: (value) => {
        return [
          'InProcess',
          'Settled'
        ].indexOf(value) !== -1
     }
    })
    propC:string;
}
@watch

实际就是Vue中的监听器,如下:

import { Vue, Component, Watch } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @Watch('child')
  onChildChanged(val: string, oldVal: string) {}

  @Watch('person', { immediate: true, deep: true })
  onPersonChanged1(val: Person, oldVal: Person) {}

  @Watch('person')
  onPersonChanged2(val: Person, oldVal: Person) {}
}
@emit

vue-property-decorator 提供的 @Emit 装饰器就是代替Vue 中的事件的触发$emit,如下:

import {Vue, Component, Emit} from 'vue-property-decorator';
    @Component({})
    export default class Some extends Vue{
        mounted(){
            this.$on('emit-todo', function(n) {
                console.log(n)
            })
            this.emitTodo('world');
        }
        @Emit()
        emitTodo(n: string){
            console.log('hello');
        }
    }

三 、总结

可以看到上述typescript版本的vue class的语法与平时javascript版本使用起来还是有很大的不同,多处用到class与装饰器,但实际上本质是一致的,只有不断编写才会得心应手

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 如何在React项目中应用TypeScript?

一、前言

单独的使用 TypeScript 并不会导致学习成本很高,但是绝大部分前端开发者的项目都是依赖于框架的

例如与 VueReact 这些框架结合使用的时候,会有一定的门槛

使用 TypeScript 编写 React 代码,除了需要 TypeScript 这个库之外,还需要安装 @types/react@types/react-dom

npm i @types/react -s

npm i @types/react-dom -s

至于上述使用 @types 的库的原因在于,目前非常多的 JavaScript 库并没有提供自己关于 TypeScript 的声明文件

所以,ts 并不知道这些库的类型以及对应导出的内容,这里 @types 实际就是社区中的 DefinitelyTyped 库,定义了目前市面上绝大多数的 JavaScript 库的声明

所以下载相关的 JavaScript 对应的 @types 声明时,就能够使用使用该库对应的类型定义

二、使用方式

在编写 React 项目的时候,最常见的使用的组件就是:

  • 无状态组件
  • 有状态组件
  • 受控组件
无状态组件

主要作用是用于展示 UI,如果使用 js 声明,则如下所示:

import * as React from "React";

export const Logo = (props) => {
  const { logo, className, alt } = props;

  return <img src={logo} className={className} alt={alt} />;
};

但这时候 ts 会出现报错提示,原因在于没有定义 porps 类型,这时候就可以使用 interface 接口去定义 porps 即可,如下:

import * as React from "React";

interface IProps {
  logo?: string;
  className?: string;
  alt?: string;
}

export const Logo = (props: IProps) => {
  const { logo, className, alt } = props;

  return <img src={logo} className={className} alt={alt} />;
};

但是我们都知道 props 里面存在 children 属性,我们不可能每个 porps 接口里面定义多一个 children,如下:

interface IProps {
  logo?: string;
  className?: string;
  alt?: string;
  children?: ReactNode;
}

更加规范的写法是使用 React 里面定义好的 FC 属性,里面已经定义好 children 类型,如下:

export const Logo: React.FC<IProps> = (props) => {
  const { logo, className, alt } = props;

  return <img src={logo} className={className} alt={alt} />;
};
  • React.FC 显式地定义了返回类型,其他方式是隐式推导的
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全
  • React.FC 为 children 提供了隐式的类型(ReactElement | null)
有状态组件

可以是一个类组件且存在 propsstate 属性

如果使用 TypeScript 声明则如下所示:

import * as React from "React";

interface IProps {
  color: string;
  size?: string;
}
interface IState {
  count: number;
}
class App extends React.Component<IProps, IState> {
  public state = {
    count: 1,
  };
  public render() {
    return <div>Hello world</div>;
  }
}

上述通过泛型对 propsstate 进行类型定义,然后在使用的时候就可以在编译器中获取更好的智能提示

关于 Component 泛型类的定义,可以参考下 React 的类型定义文件 node_modules/@types/React/index.d.ts,如下所示:

class Component<P, S> {
  readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;

  state: Readonly<S>;
}

从上述可以看到,state 属性也定义了可读类型,目的是为了防止直接调用 this.state 更新状态

受控组件

受控组件的特性在于元素的内容通过组件的状态 state 进行控制

由于组件内部的事件是合成事件,不等同于原生事件,

例如一个 input 组件修改内部的状态,常见的定义的时候如下所示:

private updateValue(e: React.ChangeEvent<HTMLInputElement>) {
    this.setState({ itemText: e.target.value })
}

常用 Event 事件对象类型:

  • ClipboardEvent<T = Element> 剪贴板事件对象
  • DragEvent<T = Element> 拖拽事件对象
  • ChangeEvent<T = Element> Change 事件对象
  • KeyboardEvent<T = Element> 键盘事件对象
  • MouseEvent<T = Element> 鼠标事件对象
  • TouchEvent<T = Element> 触摸事件对象
  • WheelEvent<T = Element> 滚轮事件对象
  • AnimationEvent<T = Element> 动画事件对象
  • TransitionEvent<T = Element> 过渡事件对象

T 接收一个 DOM 元素类型

三、总结

上述只是简单的在 React 项目使用 TypeScript,但在编写 React 项目的时候,还存在 hooks、默认参数、以及 store 等等……

TypeScript 在框架中使用的学习成本相对会更高,需要不断编写才能熟练

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 使用 Typescript 有哪些优点和缺点

优点

提供可选的强静态类型

既然叫 TypeScript,那它的最大亮点自然就是提供静态类型(type)。我们可以对变量设置类型,比如你给 count 变量设置为数字类型(number),如果你从 input 元素提取 value (string 类型)赋予给 count,如果你忘记将其转换为数字,编译是不会通过的。

当然类型不是强求设置的,为了兼容 JavaScript,你也可以设置为 any 类型。所谓 any 类型是一个特殊的类型,任何类型的的值都可以赋予给它。

更早发现 BUG

TypeScript 要编译后才能使用。所以我们的类型错误会在编译过程中被编译器发现,更早发现 BUG。如果直接用 JavaScript 开发,需要在程序运行时,吭哧吭哧点来点去各种测试来判断行为是否正常,费时费力,开发体验极差。

比如原本应该是两个数字相加的,写错成两个数字形式的字符串相加,结果是差得十万八千里,且不易察觉。TypeScript 在编译时就给你找到了,你不改对别想过我编译。

代码可预测

声明的变量一旦指定类型,它的类型就再也不能修改。这样变量就具有可预测性。

JavaScript 的变量可以赋予任何类型的值。有时候,我们会看到一个变量在执行的过程中变成各种各样的类型,一会是字符串,一会是对象,非常不好预测,尤其是有复杂条件判断的时候。这其实是并不是好的开发习惯,但在 JavaScript 它就是可以这么干!

但如果你用 TypeScript,就没有这个烦恼,它直接给你一刀切了,你别想将字符串值赋予给一个数字类型变量,一旦声明就再也无法修改。

当然为了兼容,你也是可以将其设置 any 类型,但智能提升就没有了哦。

丰富的 IDE 支持

因为使用了类型,所以检测某个变量是什么类型、可以使用哪些方法就变得容易,在开发体验上就可以进行改善了。

目前在绝大多数 IDE(集成开发环境)中已经支持 TypeScript 的 智能提示、自动补全、代码导航 等功能,并能在编写时实时反馈类型错误并提供准确的建议,比如可以指出传入函数的对象缺了哪些属性。

方便重构

重构时,如果函数的参数修改了,调用它时如果不对,TypeScript 会提示你。这对重构代码提供了坚实的后盾。有了 TypeScript,就放心做重构吧,前提你不要到处用 any。

提供面向对象的写法

面向对象语言在实践中已经被证实是优秀的设计,拥有极高的可读性、可维护性。TypeScript 支持接口、抽象类、枚举等面向对象语言的特性,支持你更好地实现一些设计模式。TypeScript 还支持类型体操,有空多锻炼身体哈。

看了这么多优点,再看看 TypeScript 的缺点。

缺点

不是真正的静态类型

因为需要兼容 JavaScript 的缘故,TypeScript 的类型是可选的。你可以用 any 类型,也可以进行类型的强制转换,所以如果你在代码中写了太多这样的东西,甚至将其变成 AnyScript。那么实际运行还是可能会出现一些类型上的问题。

有一定的学习成本

学 TypeScript 其实和学习一门新语言差不多了,还是有不小的学习成本的。但因为兼容 JavaScript 的缘故,对前端开发者来说难度会低一点。

需要写更多的代码

主要是类型和接口声明的部分,但能够抵消掉你 debug 类型问题的时间,总体看还是物超所值的。另外,编译后类型和接口声明都会被移除,相比直接写 JavaScript,体积不会明显更大。

需要编译

浏览器和 Nodejs 并不支持 TypeScript,所以多了一步编译操作。对于普通项目来说通常不长,其实还好。但如果你用来写脚本的话,就需要多安装 tsc 编译工具,还要配置好 tsconfig.json 文件,还是有点麻烦。

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ Typescript中的内置类型有哪些?

Partial(部分的)

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

作用是让传入类型中的所有属性变成都是可选的

使用举例

export interface Student {
  name: string;
  age: number;
}

const student1: Student = {}

const student2: Partial<Student> = {}

变量student1的类型是Student,Student默认所有的属性都是不能为空的,所有会报错,student2就不会

Required(必须的)

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

跟Partial的作用是相反的,是让传入类型中的所有属性变成都是必填的

使用举例

export interface Student {
  name?: string;
  age?: number;
}

const student1: Student = {}

const student2: Required<Student> = {}

变量student1的类型是Student,Student默认所有的属性都是可以为空的,所有不会报错,student2会报错

Readonly(只读的)

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

作用是让传入类型中的所有属性变成都是只读的(不能修改属性)

使用举例

export interface Student {
  name: string;
  age: number;
}

const student1: Student = {
  name: '张三',
  age: 20
}
student1.age = 21

const student2: Readonly<Student> = {
  name: '李四',
  age: 20
}
student2.age = 21

给student1的属性age重新赋值不会报错,给student2的属性age重新赋值就会报错,因为student2所有的属性都是只读的

Pick(选择)

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

作用是选择传入类型中的部分属性组成新类型

使用举例

export interface Student {
  name: string;
  age: number;
}

const student1: Student = {
  name: '张三',
  age: 20
}

const student2: Pick<Student, 'name'> = {
  name: '李四'
}

const student3: Pick<Student, 'name'> = {
  name: '王五',
  age: 20
}

变量student1可以有所有属性name和age,变量student2就只能有属性name,变量student3加上属性age就会报错

Record(记录)

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

作用是构建一个类型,这个类型用来描述一个对象,这个对象的属性都具有相同的类型

使用举例

export const student1: Record<string, any> = {
  name: '张三',
  age: 20
}

Record应该是日常使用频率较高的内置类型了,主要用来描述对象,一般建议是不用Object来描述对象,而是用Record代替,Record<string, any>几乎可以说是万金油了

Exclude(排除)

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

针对联合类型(interface这种没用),用人话说,排除相同的,留下不同的

使用举例

export type PersonAttr = 'name' | 'age'

export type StudentAttr = 'name' | 'age' | 'class' | 'school'

const student1: Exclude<StudentAttr, PersonAttr>

student1就只能被赋值为’class’ 或者’school’

Extract(取出)

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

与Exclude相反,针对联合类型,排除不同的的,取出相同的

使用举例

export type PersonAttr = 'name' | 'age'

export type StudentAttr = 'name' | 'age' | 'class' | 'school'

const student1: Extract<StudentAttr, PersonAttr>

student1就只能被赋值为’name’或者’age’

Omit(省略)

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

传入一个类型,和这个类型的几个属性,把传入的属性省略掉,组成一个新类型

使用举例

export interface Student {
  name: string;
  age: number;
  class: string;
  school: string;
}

export type PersonAttr = 'name' | 'age'

export type StudentAttr = 'name' | 'age' | 'class' | 'school'

const student1: Omit<Student, PersonAttr> = {}

student1报错,提示没有属性’name’、’age’

NonNullable(不能为null)

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

字面意思,不能为空

使用举例

export interface Student {
  name: string;
  age: number;
}

const student1: NonNullable<Student | undefined | null> = null

student1赋值为null会报错(在tsconfig.json配置文件中开启类型检查,"skipLibCheck": false

Parameters(参数)

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

获取传入函数的参数组成的类型

使用举例

export interface Student {
  name: string;
  age: number;
}

export interface StudentFunc {
  (name: string, age: number): Student
}

const student1: Parameters<StudentFunc>

student1的类型为[name: string, age: number]

ConstructorParameters(构造参数)

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

获取传入构造函数的参数组成的类型

使用举例

export interface Student {
  name: string;
  age: number;
}

export interface StudentConstructor {
  new (name: string, age: number): Student
}

const student1: ConstructorParameters<StudentConstructor>

student1的类型为[name: string, age: number]

ReturnType(返回类型)

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

获取传入函数的返回类型

使用举例

export interface Student {
  name: string;
  age: number;
}

export interface StudentFunc {
  (name: string, age: number): Student
}

const student1: ReturnType<StudentFunc> = {}

student1的类型为Student

InstanceType(构造返回类型、实例类型)

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

获取传入构造函数的返回类型

使用举例

const Student = class {
  name: string;
  age: number;
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
  }
  showInfo () {
    console.log('name: ', this.name, 'age: ', this.age);
  }
}

const student1: InstanceType<typeof Student> = new Student('张三', 20)

个人认为这是一个非常好用的内置类型,目前在前端项目中,class是用的越来越多了,在TS中,class其实也是可以用作类型声明空间的,用来描述对象类型,但是一般来说好像很少这样用的,一般用interface或者type居多

export class Student {
  name: string;
  age: number;
}

所以一般就是直接把class用作变量声明空间,但是对于 class new 出的实例,怎么描述它的类型呢,就如上文的,直接const student1: Student那是铁定会报错的,因为Student用作变量声明空间,没有用作类型声明空间(听起来好绕),这时候就可以用到InstanceType,完美解决问题

Uppercase(大写)

/**
 * Convert string literal type to uppercase
 */
type Uppercase<S extends string> = intrinsic;

变大写

使用举例

export type StudentSexType = 'male' | 'female'

const studentSex: Uppercase<StudentSexType> = 'MALE'

Lowercase(小写)

/**
 * Convert string literal type to lowercase
 */
type Lowercase<S extends string> = intrinsic;

变小写

使用举例

export type StudentSexType = 'MALE' | 'FEMALE'

const studentSex: Lowercase<StudentSexType> = ''

Capitalize(首字母大写)

/**
 * Convert first character of string literal type to uppercase
 */
type Capitalize<S extends string> = intrinsic;

首字母变大写

使用举例

export type StudentSexType = 'male' | 'female'

const studentSex: Capitalize<StudentSexType> = ''

Uncapitalize(首字母小写)

/**
 * Convert first character of string literal type to lowercase
 */
type Uncapitalize<S extends string> = intrinsic;

首字母变小写

使用举例

export type StudentSexType = 'MALE' | 'FEMALE'

const studentSex: Uncapitalize<StudentSexType> = ''

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 能否将多个 .ts 文件组合成一个 .js 文件? 如果是,那么如何?

这是可以的。需要添加 —outFILE [OutputJSFileName] 编译选项。

$ tsc --outFile comman.js file1.ts file2.ts file3.ts

上述命令将编译所有三个.ts文件,结果将存储到单个comman.js文件中。在这种情况下,当没有像下面的命令那样提供输出文件名时。

$ tsc --outFile file1.ts file2.ts file3.ts

然后,file2.tsfile3.ts会被编译,输出会放在file1.ts中。所以现在的 file1.ts 包含 JavaScript 代码。

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 是否可以通过 .ts 文件中的实时更改自动编译 .ts

是的,可以通过 .ts 文件中的实时更改自动编译.ts。这可以通过使用 --watch 编译器选项来实现。

tsc --watch file1.ts

上面的命令首先编译 file1.js 中的 file1.ts 并观察文件的变化。如果检测到任何更改,它将再次编译该文件。在这里,需要确保在使用 --watch 选项运行时不能关闭命令提示符。

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ interface语句和type语句有什么区别?

相同点

都可以描述一个对象或者函数

interface

interface User {
  name: string
  age: number
}

interface SetUser {
  (name: string, age: number): void;
}

type

type User = {
  name: string
  age: number
};

type SetUser = (name: string, age: number)=> void;

都允许拓展(extends)

interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。

interface extends interface

interface Name { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}
复制代码

type extends type

type Name = { 
  name: string; 
}
type User = Name & { age: number  };
复制代码

interface extends type

type Name = { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}
复制代码

type extends interface

interface Name { 
  name: string; 
}
type User = Name & { 
  age: number; 
}
复制代码

不同点

type 可以而 interface 不行

  • type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

复制代码
  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
复制代码
  • 其他骚操作
type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>;  
type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
复制代码

interface 可以而 type 不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/
复制代码

♥︎ ♥︎ ♥︎ ♥︎ ♥︎ 如何在Proxy中代理apply方法

 {
            var target = function () {
                return 'I am the target';
            }
            var handler = {
                apply: function () {
                    return 'I am the proxy';
                }
            };
            var p = new Proxy(target,handler);
            console.log("p",p());
        }
//上面代码中,变量p是Proxy的实例,当他作为函数调用时(p())
//,就会被apply方法拦截,返回一个字符串
{
    var twice = {
                    //目标对象,目标对象的上下文对象,目标对象的参数数组
        apply:function (target,ctx,args) {
            //return console.log("target",target,"ctx",ctx,"args",args);
           return Reflect.apply(...arguments)*2;
        }
    };
    function sum (left,right) {
         return left*right;
    };
    var proxy = new Proxy(sum,twice);
    console.log("proxy1",proxy(1,2));
    console.log("proxy.call",proxy.call(null,5,6));
    console.log("proxy,apply",proxy.apply(null,[7,8]));
    console.log("proxy,apply",Reflect.apply(proxy,null,[7,8]));

}
//上面的实例执行顺序是当sum()执行完后再执行twice中的apply(),
//上面代码中每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截
//另外,直接调用Reflect.apply方法,也会被拦截。

参考文献:

[语音仓库]: “https://github.com/febobo/web-interview

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信