性能优化
1.webpack打包文件体积过大?(最终打包为一个js文件)
2.如何优化webpack构建的性能
3.移动端的性能优化
4.Vue的SPA 如何优化加载速度
5.移动端300ms延迟
6.页面的重构
所有的知识点都有详细的解答,我整理成了280页PDF《前端校招面试真题精编解析》。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。
以上的枚举示例经编译后,对应的 ES5 代码如下:
“use strict”;
var Direction;
(function (Direction) {
Direction[(Direction[“NORTH”] = 0)] = “NORTH”;
Direction[(Direction[“SOUTH”] = 1)] = “SOUTH”;
Direction[(Direction[“EAST”] = 2)] = “EAST”;
Direction[(Direction[“WEST”] = 3)] = “WEST”;
})(Direction || (Direction = {}));
var dir = Direction.NORTH;
当然我们也可以设置 NORTH 的初始值,比如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
2.字符串枚举
在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
NORTH = “NORTH”,
SOUTH = “SOUTH”,
EAST = “EAST”,
WEST = “WEST”,
}
以上代码对应的 ES5 代码如下:
“use strict”;
var Direction;
(function (Direction) {
Direction[“NORTH”] = “NORTH”;
Direction[“SOUTH”] = “SOUTH”;
Direction[“EAST”] = “EAST”;
Direction[“WEST”] = “WEST”;
})(Direction || (Direction = {}));
通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction0; // NORTH
let dirVal = Direction[“NORTH”]; // 0
另外,对于纯字符串枚举,我们不能省略任何初始化程序。而数字枚举如果没有显式设置值时,则会使用默认规则进行初始化。
3.常量枚举
除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用const
关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript。为了更好地理解这句话,我们来看一个具体的例子:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
以上代码对应的 ES5 代码如下:
“use strict”;
var dir = 0 /* NORTH */;
4.异构枚举
异构枚举的成员值是数字和字符串的混合:
enum Enum {
A,
B,
C = “C”,
D = “D”,
E = 8,
F,
}
以上代码对于的 ES5 代码如下:
“use strict”;
var Enum;
(function (Enum) {
Enum[Enum[“A”] = 0] = “A”;
Enum[Enum[“B”] = 1] = “B”;
Enum[“C”] = “C”;
Enum[“D”] = “D”;
Enum[Enum[“E”] = 8] = “E”;
Enum[Enum[“F”] = 9] = “F”;
})(Enum || (Enum = {}));
通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:
console.log(Enum.A) //输出:0
console.log(Enum0) // 输出:A
2.7 Any 类型
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。
let notSure: any = 666;
notSure = “semlinker”;
notSure = false;
any
类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any
类型的值执行任何操作,而无需事先执行任何形式的检查。比如:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value0; // OK
在许多场景下,这太宽松了。使用 any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any
类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any
带来的问题,TypeScript 3.0 引入了 unknown
类型。
2.8 Unknown 类型
就像所有类型都可以赋值给 any
,所有类型也都可以赋值给 unknown
。这使得 unknown
成为 TypeScript 类型系统的另一种顶级类型(另一种是 any
)。下面我们来看一下unknown
类型的使用示例:
let value: unknown;
value = true; // OK
value = 42; // OK
value = “Hello World”; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol(“type”); // OK
对 value
变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为unknown
的值赋值给其他类型的变量时会发生什么?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
unknown
类型只能被赋值给 any
类型和 unknown
类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown
类型的值。毕竟我们不知道变量 value
中存储了什么类型的值。
现在让我们看看当我们尝试对类型为 unknown
的值执行操作时会发生什么。以下是我们在之前 any
章节看过的相同操作:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value0; // Error
将 value
变量类型设置为 unknown
后,这些操作都不再被认为是类型正确的。通过将any
类型改变为 unknown
类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
2.9 Tuple 类型
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:
let tupleType: [string, boolean];
tupleType = [“semlinker”, true];
在上面代码中,我们定义了一个名为 tupleType
的变量,它的类型是一个类型数组 [string, boolean]
,然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:
console.log(tupleType0); // semlinker
console.log(tupleType1); // true
在元组初始化的时候,如果出现类型不匹配的话,比如:
tupleType = [true, “semlinker”];
此时,TypeScript 编译器会提示以下错误信息:
很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:
tupleType = [“semlinker”];
此时,TypeScript 编译器会提示以下错误信息:
Property ‘1’ is missing in type ‘[string]’ but required in type ‘[string, boolean]’.
2.10 Void 类型
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
// 声明函数返回值为void
function warnUser(): void {
console.log(“This is my warning message”);
}
以上代码编译生成的 ES5 代码如下:
“use strict”;
function warnUser() {
console.log(“This is my warning message”);
}
需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为undefined
或 null
:
let unusable: void = undefined;
2.11 Null 和 Undefined 类型
TypeScript 里,undefined
和 null
两者有各自的类型分别为 undefined
和 null
。
let u: undefined = undefined;
let n: null = null;
默认情况下 null
和 undefined
是所有类型的子类型。就是说你可以把 null
和 undefined
赋值给 number
类型的变量。然而,如果你指定了--strictNullChecks
标记,null
和 undefined
只能赋值给 void
和它们各自的类型。
2.12 object, Object 和 {} 类型
1.object 类型
object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// …
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create(“oops”); // Error
2.Object 类型
Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:
- Object 接口定义了 Object.prototype 原型对象上的属性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
- ObjectConstructor 接口定义了 Object 类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via new
*/
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
Object 类的所有实例都继承了 Object 接口中的所有属性。
3.{} 类型
{} 类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。
// Type {}
const obj = {};
// Error: Property ‘prop’ does not exist on type ‘{}’.
obj.prop = “semlinker”;
但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:
// Type {}
const obj = {};
// “[object Object]”
obj.toString();
2.13 Never 类型
never
类型表示的是那些永不存在的值的类型。例如,never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === “string”) {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === “number”) {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean;
然而他忘记同时修改 controlFlowAnalysisWithNever
方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean
类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保
controlFlowAnalysisWithNever
方法总是穷尽了 Foo 的所有可能类型。通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
三、TypeScript 断言
3.1 类型断言
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式:
1.“尖括号” 语法
let someValue: any = “this is a string”;
let strLength: number = (someValue).length;
2.as 语法
let someValue: any = “this is a string”;
let strLength: number = (someValue as string).length;
3.2 非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。
那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。
1.忽略 undefined 和 null 类型
function myFunc(maybeString: string | undefined | null) {
// Type ‘string | null | undefined’ is not assignable to type ‘string’.
// Type ‘undefined’ is not assignable to type ‘string’.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
2.调用函数时忽略 undefined 类型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly ‘undefined’.(2532)
// Cannot invoke an object which is possibly ‘undefined’.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
因为 !
非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
以上 TS 代码会编译生成以下 ES5 代码:
“use strict”;
const a = undefined;
const b = a;
console.log(b);
虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!;
语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,!
非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined
。
3.3 确定赋值断言
在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:
let x: number;
initialize();
// Variable ‘x’ is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
通过 let x!: number;
确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
四、类型守卫
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:
4.1 in 关键字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if (“privileges” in emp) {
console.log("Privileges: " + emp.privileges);
}
if (“startDate” in emp) {
console.log("Start Date: " + emp.startDate);
}
}
4.2 typeof 关键字
function padLeft(value: string, padding: string | number) {
if (typeof padding === “number”) {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === “string”) {
return padding + value;
}
throw new Error(Expected string or number, got '${padding}'.
);
}
typeof
类型保护只支持两种形式:typeof v === "typename"
和 typeof v !== typename
,"typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
。但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
4.3 instanceof 关键字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 ‘SpaceRepeatingPadder’
}
4.4 自定义类型保护的类型谓词
function isNumber(x: any): x is number {
return typeof x === “number”;
}
function isString(x: any): x is string {
return typeof x === “string”;
}
五、联合类型和类型别名
5.1 联合类型
联合类型通常与 null
或 undefined
一起使用:
const sayHello = (name: string | undefined) => {
/* … */
};
例如,这里 name
的类型是 string | undefined
意味着可以将 string
或undefined
的值传递给sayHello
函数。
sayHello(“semlinker”);
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = ‘click’ | ‘scroll’ | ‘mousemove’;
以上示例中的 1
、2
或 'click'
被称为字面量类型,用来约束取值只能是某几个值中的一个。
5.2 可辨识联合
TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。
这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
1.可辨识
可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: “motorcycle”; // discriminant
make: number; // year
}
interface Car {
vType: “car”; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: “truck”; // discriminant
capacity: number; // in tons
}
在上述代码中,我们分别定义了 Motorcycle
、 Car
和 Truck
三个接口,在这些接口中都包含一个 vType
属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
2.联合类型
基于前面定义了三个接口,我们可以创建一个 Vehicle
联合类型:
type Vehicle = Motorcycle | Car | Truck;
现在我们就可以开始使用 Vehicle
联合类型,对于 Vehicle
类型的变量,它可以表示不同类型的车辆。
3.类型守卫
下面我们来定义一个 evaluatePrice
方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: “truck”, capacity: 9.5 };
evaluatePrice(myTruck);
对于以上代码,TypeScript 编译器将会提示以下错误信息:
Property ‘capacity’ does not exist on type ‘Vehicle’.
Property ‘capacity’ does not exist on type ‘Motorcycle’.
原因是在 Motorcycle 接口中,并不存在 capacity
属性,而对于 Car 接口来说,它也不存在 capacity
属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice
方法,重构后的代码如下:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case “car”:
return vehicle.transmission * EVALUATION_FACTOR;
case “truck”:
return vehicle.capacity * EVALUATION_FACTOR;
case “motorcycle”:
return vehicle.make * EVALUATION_FACTOR;
}
}
在以上代码中,我们使用 switch
和 case
运算符来实现类型守卫,从而确保在 evaluatePrice
方法中,我们可以安全地访问 vehicle
对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。
5.3 类型别名
类型别名用来给一个类型起个新名字。
type Message = string | string[];
let greet = (message: Message) => {
// …
};
六、交叉类型
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 &
运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
在上面代码中我们先定义了 PartialPointX
类型,接着使用 &
运算符创建一个新的 Point
类型,表示一个含有 x 和 y 坐标的点,然后定义了一个 Point
类型的变量并初始化。
6.1 同名基础类型属性的合并
那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string
或 number
类型呢?比如下面的例子:
p = { c: 6, d: “d”, e: “e” };
q = { c: “c”, d: “d”, e: “e” };
为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never
呢?这是因为混入后成员 c 的类型为 string & number
,即成员 c 的类型既可以是 string
类型又可以是number
类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never
。
6.2 同名非基础类型属性的合并
在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: ‘semlinker’,
f: 666
}
};
console.log(‘abc:’, abc);
以上代码成功运行后,控制台会输出以下结果:
由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。
七、TypeScript 函数
7.1 TypeScript 函数与 JavaScript 函数的区别
| TypeScript | JavaScript |
| — | — |
| 含有类型 | 无类型 |
| 箭头函数 | 箭头函数(ES2015) |
| 函数类型 | 无函数类型 |
| 必填和可选参数 | 所有参数都是可选的 |
| 默认参数 | 默认参数 |
| 剩余参数 | 剩余参数 |
| 函数重载 | 无函数重载 |
7.2 箭头函数
1.常见语法
myBooks.forEach(() => console.log(‘reading’));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + ‘-’ + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + ‘-’ + title);
});
2.使用示例
// 未使用箭头函数
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
// 使用箭头函数
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
7.3 参数类型和返回类型
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 函数类型
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
7.5 可选参数及默认参数
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
// 默认参数
function createUserId(
name: string = “semlinker”,
id: number,
age?: number
): string {
return name + id;
}
在声明函数时,可以通过 ?
号来定义可选参数,比如 age?: number
这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。
7.6 剩余参数
function push(array, …items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
7.7 函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === ‘string’ || typeof b === ‘string’) {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === ‘string’ || typeof b === ‘string’) {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add(‘Semlinker’, ’ Kakuqo’);
这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。
八、TypeScript 数组
8.1 数组解构
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
8.2 数组展开运算符
let two_array = [0, 1];
let five_array = […two_array, 2, 3, 4];
8.3 数组遍历
let colors: string[] = [“red”, “green”, “blue”];
for (let i of colors) {
console.log(i);
}
九、TypeScript 对象
9.1 对象解构
let person = {
name: “Semlinker”,
gender: “Male”,
};
let { name, gender } = person;
9.2 对象展开运算符
let person = {
name: “Semlinker”,
gender: “Male”,
address: “Xiamen”,
};
// 组装对象
let personWithAge = { …person, age: 33 };
// 获取除了某些项外的其它项
let { name, …rest } = person;
十、TypeScript 接口
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
10.1 对象的形状
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: “semlinker”,
age: 33,
};
10.2 可选 | 只读属性
interface Person {
readonly name: string;
age?: number;
}
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray = a;
ro0 = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
10.3 任意属性
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
interface Person {
name: string;
age?: number;
}
const p1 = { name: “semlinker” };
const p2 = { name: “lolo”, age: 5 };
const p3 = { name: “kakuqo”, sex: 1 }
10.4 接口与类型别名的区别
1.Objects/Functions
接口和类型别名都可以用来描述对象的形状或函数签名:
接口
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
类型别名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
2.Other Types
与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
3.Extend
接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的。
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements
类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// interp of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
5.Declaration merging
与类型别名不同,接口可以定义多次,会被自动合并为单个接口。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
十一、TypeScript 类
11.1 类的属性与方法
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
在 TypeScript 中,我们可以通过 Class
关键字来定义一个类:
class Greeter {
// 静态属性
static cname: string = “Greeter”;
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return “Class name is Greeter”;
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter(“world”);
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下编译生成的 ES5 代码:
“use strict”;
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return “Class name is Greeter”;
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = “Greeter”;
return Greeter;
}());
var greeter = new Greeter(“world”);
11.2 ECMAScript 私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(Hello, my name is ${this.#name}!
);
}
}
let semlinker = new Person(“Semlinker”);
semlinker.#name;
// ~~~~~
// Property ‘#name’ is not accessible outside class ‘Person’
// because it has a private identifier.
与常规属性(甚至使用 private
修饰符声明的属性)不同,私有字段要牢记以下规则:
-
私有字段以
#
字符开头,有时我们称之为私有名称; -
每个私有字段名称都唯一地限定于其包含的类;
-
不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
-
私有字段不能在包含的类之外访问,甚至不能被检测到。
11.3 访问器
在 TypeScript 中,我们可以通过 getter
和 setter
方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = “Hello TypeScript”;
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == “Hello TypeScript”) {
this._fullName = newName;
} else {
console.log(“Error: Unauthorized update of employee!”);
}
}
}
let employee = new Employee();
employee.fullName = “Semlinker”;
if (employee.fullName) {
console.log(employee.fullName);
}
11.4 类的继承
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。
继承是一种 is-a 关系:
在 TypeScript 中,我们可以通过 extends
关键字来实现继承:
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.
);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 调用父类的构造函数
}
move(distanceInMeters = 5) {
console.log(“Slithering…”);
super.move(distanceInMeters);
}
}
let sam = new Snake(“Sammy the Python”);
sam.move();
11.5 抽象类
使用 abstract
关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
性能优化
1.webpack打包文件体积过大?(最终打包为一个js文件)
2.如何优化webpack构建的性能
3.移动端的性能优化
4.Vue的SPA 如何优化加载速度
5.移动端300ms延迟
6.页面的重构
所有的知识点都有详细的解答,我整理成了280页PDF《前端校招面试真题精编解析》。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】