笔记集合:
前端(HTML+CSS+JS+打包+环境+网络)面试题基础八股集合——2023 - 掘金 (juejin.cn)
TypeScript面试题八股集合——2023 - 掘金 (juejin.cn)
为什么要使用 TypeScript ? TypeScript 相对于 JavaScript 的优势是什么?
- TypeScript是JS的超集,包括所有的JS规范版本。同时拥有强大的类型系统,包括泛型。是一个面向对象的语言,提供类,接口和模块。
TS对JS的改进主要是静态类型检查(强类型语言)“静态类型更有利于构建大型应用”。 同时TS多了接口,泛型这些JS所没有的特性,内置的数据类型也比较多。
TypeScript的内置数据类型有哪些?
在Typescript中,内置的数据类型也称为原始数据类型。
boolean(布尔类型)
number(数字类型)
string(字符串类型)
void 类型
null 和 undefined 类型
array(数组类型)
tuple(元组类型):允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
enum(枚举类型):enum
类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字
any(任意类型)
never 类型
object 对象类型
TypeScript 中 const 和 readonly 的区别?
const可以防止变量的值被修改,在运行时检查,使用const变量保存的数组,可以使用push,pop等方法
readonly可以防止变量的属性被修改,在编译时检查,使用Readonly Array声明的数组不能使用push,pop等方法
TypeScript 中 any 类型的作用是什么?
为编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库(不确定用户输入值的类型,第三方代码库是如何工作的)。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。
any的问题
- 类型污染:any
类型的对象会导致后续的属性类型都会变成
any - 使用不存在的属性或方法而不报错
TypeScript 中 any、never、unknown、null & undefined 和 void 有什么区别?
any
: 动态的变量类型(失去了类型检查的作用)。never
: 永不存在的值的类型。例如:never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。unknown
: 任何类型的值都可以赋给 unknown 类型,但是 unknown 类型的值只能赋给 unknown 本身和 any 类型。null & undefined
: 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。当你指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。void
: 没有任何类型。例如:一个函数如果没有返回值,那么返回值可以定义为void。
TS中any和unknown有什么区别?
unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。
any 和 unknown 都是顶级类型,但是 unknown 更加严格,不像 any 那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量。
解释一下TypeScript中的枚举
枚举就是一个对象的所有可能取值的集合
enum Day {SUNDAY, MONDAY,TUESDAY, WEDNESDAY,THURSDAY,FRIDAY,SATURDAY}
TypeScript 中如何联合枚举类型的 Key?————用下标
enum Status {xiaoming, xiaohong, xiaogang,}
console.log(Status.xiaoming,Status[0]);
console.log(Status.xiaohong,Status[1]);
console.log(Status.xiaogang,Status[2]);
//输出: 0 xiaoming
// 1 xiaohong
// 2 xiaogang
keyof 和 typeof 关键字的作用?
①keyof 索引类型查询操作符, 获取一个类型的所有属性名组成的联合类型。
②typeof` 获取一个变量或对象的类型。
typeof
是一个类型查询操作符,它用于获取一个值的类型。
TS中的泛型是什么?
泛型允许我们在编写代码时使用一些以后才指定的类型,在定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性。
any和泛型的区别?
泛型有类型推论,编译器会根据传入的参数自动地帮助我们确定T的类型
any则是不检验
接口与Type:
TypeScript 中同名的 interface 或者同名的 interface 和 class 可以合并吗?
同名的interface会自动合并,同名的interface和class会自动聚合。
接口和类型别名的区别?
两者都可以用来描述对象或函数的类型。与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组。
TypeScript 中 type 和 interface 的区别?
相同点:
- 都可以描述 '对象' 或者 '函数'
- 都允许拓展(extends):interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。
不同点:
1.type 可以声明基本类型,联合类型,元组
2.type 可以使用 typeof 获取实例的类型进行赋值
-
多个相同的 interface 声明可以自动合并
使用 interface 描述‘数据结构’,使用 type 描述‘类型关系
一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。
TypeScript 中使用 Union Types 时有哪些注意事项?
属性或方法访问: 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。
TypeScript 中 interface 可以给 Function / Array / Class(Indexable)做声明吗?
/* 可以 */
// 函数声明
interface Say {
(name: string): viod;
}
let say: Say = (name: string):viod => {}
// Array 声明
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
// Class 声明
interface PersonalIntl {
name: string
sayHi (name: string): string
}
TypeScript 中可以使用 String、Number、Boolean、Symbol、Object 等给类型做声明吗?
类:
TS中什么是方法重载?
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。 基本上,它在派生类或子类中重新定义了基类方法。
方法覆盖规则:
- 该方法必须与父类中的名称相同。
- 它必须具有与父类相同的参数。
- 必须存在IS-A关系或继承。
说说TS中的类及其特性
TypeScript 引入了类,以便它们可以利用诸如封装和抽象之类的面向对象技术的好处。
类是 TypeScript 中的一种对象类型,它由属性和方法组成。在 TypeScript 中,类声明使用 class
关键字,类的名称通常采用首字母大写,并通过类的构造函数创建类的实例。下面是一个简单的类的例子:
class Person {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
greet() {
console.log(`Hello, my name is ${this.firstName} ${this.lastName}.`);
}
}
let person = new Person('John', 'Doe');
person.greet(); // 输出:Hello, my name is John Doe.
在这个例子中,我们定义了一个 Person
类,它有两个属性 firstName
和 lastName
,它们的类型是 string。类还有一个构造函数,它接受两个参数,并将它们分别赋值给属性。类还有一个方法 greet
,它使用属性打印出个人欢迎语。
除了属性和方法之外,TypeScript 中的类还支持以下特性:
- 类的成员访问修饰符
TypeScript 中的类成员可以有 public
、private
和 protected
访问修饰符。它们的作用分别是:
public
:默认的访问修饰符,公共成员可以在任何地方访问。private
:私有成员只能在当前类中访问,继承类和实例都不能访问。protected
:受保护的成员可以在当前类和继承类中访问,实例不能访问。
下面是一个使用访问修饰符的例子:
class Person {
private firstName: string;
protected lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
public greet() {
console.log(`Hello, my name is ${this.firstName} ${this.lastName}.`);
}
}
class Employee extends Person {
position: string;
constructor(firstName: string, lastName: string, position: string) {
super(firstName, lastName);
this.position = position;
}
public introduce() {
console.log(`My name is ${this.firstName} ${this.lastName}, and I'm a ${this.position}.`);
}
}
let employee = new Employee('John', 'Doe', 'Developer');
employee.greet(); // 错误:'firstName' 是私有成员,不能在类外部访问。
employee.introduce(); // 输出:My name is John Doe, and I'm a Developer.
在这个例子中,我们定义了一个 Person
类,它有一个私有属性 firstName
和一个受保护的属性 lastName
。然后我们又定义了一个 Employee
类,它继承自 Person
类,并有一个新的属性 position
。注意,虽然 lastName
是受保护的成员,但在 Employee
类中可以访问。在声明 employee
实例后,我们试图访问 firstName
属性,但 TypeScript 编译器会报错,因为 firstName
是私有成员,不能在类外部访问。
- 类的静态成员
在 TypeScript 中,类可以拥有静态成员,它们属于类本身,而不是属于类的实例。静态成员可以通过 static
关键字来定义。下面是一个使用静态成员的例子:
class Calculator {
static PI: number = 3.1415926;
static add(x: number, y: number): number {
return x + y;
}
static subtract(x: number, y: number): number {
return x - y;
}
}
console.log(Calculator.PI); // 输出:3.1415926
console.log(Calculator.add(1, 2)); // 输出:3
console.log(Calculator.subtract(4, 2)); // 输出:2
在这个例子中,我们定义了一个 Calculator
类,它有一个静态属性 PI
和两个静态方法 add
和 subtract
。这些成员可以在任何地方被调用,无需创建 Calculator
类的实例。
- 类的继承
与 JavaScript 中的类类似,TypeScript 中的类也支持继承,它通过关键字 extends
来实现。继承基类可以获得基类的属性、方法、静态成员和构造函数等。下面是一个使用继承的例子:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance} meter(s).`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distance: number = 5) { // 重写基类的 move 方法
console.log(`${this.name} slithered ${distance} meter(s).`);
}
}
class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distance: number = 45) { // 重写基类的 move 方法
console.log(`${this.name} galloped ${distance} meter(s).`);
}
}
let sam = new Snake('Sammy');
let tom: Animal = new Horse('Tommy');
sam.move(); // 输出:Sammy slithered 5 meter(s).
tom.move(34); // 输出:Tommy galloped 34 meter(s).
在这个例子中,我们定义了一个 Animal
基类,它有一个属性 name
和一个方法 move
。然后我们又定义了 Snake
和 Horse
两个子类,它们都继承自 Animal
类,并重写了基类的 move
方法。在声明 sam
和 tom
实例后,我们试图调用它们的 move
方法,当然,它们分别调用的是自身的 move
方法。
- 抽象类
在 TypeScript 中,抽象类用于作为其他类的基类,它们不能直接被实例化,而是用于派生子类。抽象类可以定义抽象方法,用于强制要求子类必须实现的方法。抽象类通过关键字 abstract
来定义。下面是一个抽象类的例子:
abstract class Animal {
abstract makeSound(): void;
move(distance: number = 0) {
console.log(`Animal moved ${distance} meter(s).`);
}
}
class Dog extends Animal {
makeSound() {
console.log('Woof! Woof!');
}
}
let dog = new Dog();
dog.makeSound(); // 输出:Woof! Woof!
dog.move(10); // 输出:Animal moved 10 meter(s).
在这个例子中,我们定义了一个 Animal
抽象类,它有一个抽象方法 makeSound
和一个实例方法 move
,因为 makeSound
是抽象方法,需要在子类中实现。然后我们又定义了一个 Dog
类,它继承自 Animal
类,并实现了 makeSound
方法。在声明 dog
实例后,我们试图调用它们的 makeSound
和 move
方法,当然,它们分别调用的是 Dog
类和 Animal
类的方法。
总的来说,类是 TypeScript 中非常重要的一部分,它提供了良好的封装能力并使代码更易于扩展和维护。在 TypeScript 中,类具有许多特性,
模块化与声明:
TypeScript 如何设计 Class 的声明?
在 TypeScript 中,Class 声明可以通过 class
关键字和类名来定义。类可以包含属性、方法、构造函数和访问修饰符等成员。
下面是一个简单的类声明示例:
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public getAge(): number {
return this.age;
}
public greet() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
在这个例子中,我们声明了一个名为 Person
的类,并定义了一个构造函数和两个方法。构造函数负责初始化属性,而 getAge
和 greet
方法分别返回实例的年龄和打招呼消息。
在类声明中,还可以使用访问修饰符来控制属性和方法的访问级别。公共成员可以通过 public
关键字来访问,私有成员可以通过 private
关键字来访问,受保护成员可以通过 protected
关键字来访问。例如,在上面的示例中,name
和 age
属性被声明为私有成员,因此它们只能在类内部访问。
最后,在类声明中还可以使用关键字 implements
来实现接口,或使用关键字 extends
来扩展另一个类。这可以使类具有更多的功能和行为。
类型的全局声明和局部声明
如果声明文件内不包含import、export
,那么这个文件声明的类型就会变成全局声明。反之,若是这个文件包含了import、export
,那么这个文件包含的类型声明则会是局部声明,不会影响到全局声明。
TypeScript中的Declare关键字有什么作用?
declare 关键字用来声明全局变量、全局函数、全局类或全局枚举类型等
为什么要申明:不声明的话,TypeScript编译器将无法识别上述的这些模块,就会提示相应的错误信息。
TypeScript中的模块是什么?
在 TypeScript 中,模块是可重用代码的基本单元,它将相关的代码封装在一起并导出它们,以便其他代码可以引用和使用它们。模块化是现代 Web 应用程序开发的一个重要方面,它可以将代码分解为独立的文件,并最大程度地减少代码的重复和冗余。
在 TypeScript 中,有两种主要类型的模块:内部模块(也称为命名空间)和外部模块(也称为文件模块)。
内部模块:一种简单的代码组织方式,可以将相关的代码封装在一个命名空间中,从而减少全局名称空间的污染。
外部模块:独立的文件单元,通常使用模块加载器加载,并通过导出和导入语句与其他模块通信。
TypeScript 的模块系统支持不同的模块加载器,例如 CommonJS、AMD 和 ES6。这意味着您可以使用不同的模块加载器来加载不同的模块,以适应不同的环境和需要。
TypeScript中命名空间与模块的理解和区别
在 TypeScript 中,命名空间(Namespace)和模块(Module)是用来组织代码的特殊语言结构,它们都具有一些相似的功能,但也有一些区别。
-
命名空间是编写内部模块时使用的一种方式,它提供了一种将所有相关对象封装在单个命名空间中的方法。命名空间是一个对象,可以包含函数、变量、类型等内容。可以将一组相关函数和数据放在一个命名空间内,从而避免命名冲突。
// 命名空间的定义和使用 namespace MyNamespace { export const a = 1; export function fn() { console.log("hello world") }; } MyNamespace.fn(); // "hello world" console.log(MyNamespace.a); // 1
-
模块是用来组织代码的另一种方式,它提供了将公共接口和具体实现分开的方法。模块是有作用域的,也就是说,它们内部定义的变量、函数等都不会污染全局作用域。可以使用 export 关键字将模块内的接口暴露出去,供其他模块使用。
// 模块的定义和使用 export const a = 1; export function fn() { console.log("hello world") }; import { a, fn } from './myModule'; fn(); // "hello world" console.log(a); // 1
在使用命名空间和模块时需要注意以下几点:
-
命名空间不存在变量名冲突问题,但命名空间的嵌套不应该过深,否则可能导致代码的可读性变差。
-
模块之间可以进行依赖管理,即一个模块可以依赖于其他模块,在使用模块时需要对依赖项进行管理。
-
在 TypeScript 中,模块是推荐使用的组织代码的方式,命名空间仅用于向后兼容性。如果要编写新的代码,应该使用模块而不是命名空间。
总之,命名空间和模块都是用来组织代码的方式,它们的相似点在于都可以用来避免命名冲突,但模块由于其更好的扩展性和可维护性被推荐使用。
TS原理:
简单介绍一下 TypeScript 模块的加载机制?
假设有一个导入语句 import { a } from "moduleA"
;
- 首先,编译器会尝试定位需要导入的模块文件,通过绝对或者相对的路径查找方式;
- 如果上面的解析失败了,没有查找到对应的模块,编译器会尝试定位一个
外部模块声明
(.d.ts); - 最后,如果编译器还是不能解析这个模块,则会抛出一个错误
error TS2307: Cannot find module 'moduleA'.
对 TypeScript 类型兼容性的理解?
TypeScript 类型兼容性指的是 TypeScript 中的类型系统能够自动进行类型检查和类型推导,以确保类型的兼容性。具体来说,如果两个类型 A 和 B 满足一定的关系,那么 A 类型的变量或参数可以赋值给 B 类型的变量或参数。
TypeScript 的类型兼容性规则基于结构子类型化的原则,即只要目标类型(被赋值的类型)的成员属性包含来源类型(待赋值的类型)的成员属性,或者来源类型可以转换为目标类型,就认为这两个类型是兼容的。
例如,下面代码中的 a
变量和 b
变量都是 Animal
类型,但是它们的属性不完全相同:
interface Animal {
name: string;
age: number;
}
class Cat implements Animal {
name = 'Tom';
age = 2;
type = 'mammal';
}
let a: Animal = { name: 'Kitty', age: 1 };
let b: Animal = new Cat();
尽管 Cat
类型比 Animal
类型具有更多的属性,但是由于其中的属性包含了 Animal
的所有属性,因此 Cat
是兼容于 Animal
的。因此,let b: Animal = new Cat()
是合法的。
在 TypeScript 中,类型兼容性是非常重要的,因为它允许我们编写更加灵活和可复用的代码,同时保证类型的安全性。在使用类型兼容性时,需要注意一些细节,如类型保护等,以确保程序的正确性。
TypeScript 中对象展开会有什么副作用吗?
在 TypeScript 中,对象展开符号({}或...)可以将一个或多个对象展开为另一个对象,从而简化代码。但是,如果使用不当,对象展开会导致一些副作用,例如:
- 可能存在重复属性覆盖的问题。如果展开的多个对象中包含相同的属性名称,那么后一个属性将覆盖前一个属性,这可能导致一些意想不到的问题。
const obj1 = { foo: 123 };
const obj2 = { foo: 'bar' };
const obj3 = { ...obj1, ...obj2 };
// obj3 的属性 foo 的值为 'bar',而不是期望的 123。
- 展开嵌套对象时,可能会对原始数据进行修改。对象展开会创建新对象,但是如果展开的对象中包含引用类型的属性,那么这些属性的修改会影响到原始对象。
const obj1 = { a: { b: 1 } };
const obj2 = { ...obj1 };
obj2.a.b = 2;
console.log(obj1); // 输出 { a: { b: 2 } },obj1 被修改了。
- 过度使用展开可能会导致代码可读性变差。如果过多地使用对象展开,会使代码变得更加冗长和难以理解。
因此,在使用对象展开符号时需要谨慎,避免出现不良后果。尽量避免展开具有相同属性名的对象,不要在展开前未将其进行深拷贝。此外,还需要注重代码的可读性和易于维护性。
TypeScript 中的 this 和 JavaScript 中的 this 有什么差异?
- TypeScript:noImplicitThis: true 的情况下,必须去声明 this 的类型,才能在函数或者对象中使用this。
- Typescript 中箭头函数的 this 和 ES6 中箭头函数中的 this 是一致的。
对 TypeScript 类中成员的 public、private、protected、readonly 修饰符的理解?
public
: 成员都默认为public
,被此限定符修饰的成员是可以被外部访问; private
: 被此限定符修饰的成员是只可以被类的内部访问; protected
: 被此限定符修饰的成员是只可以被类的内部以及类的子类访问; readonly
: 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
tsconfig.json有什么作用?
tsconfig.json文件是JSON格式的文件。
在tsconfig.json文件中,可以指定不同的选项来告诉编译器如何编译当前项目。
// 常用配置
{
/*
tsconfig.json是ts编译器的配置文件,ts可以根据它的信息来对待吗进行编译 可以再tsconfig中写注释
include : 用来指定哪些文件需要被编译
exclude : 用来指定哪些文件不需要被编译 :默认node_module
extends : 用来指定继承的配置文件
files : 用来指定被编译的文件列表,只有编译少量文件才使用
compilerOptions : 编译器的选项是配置文件中非常重要也是非常复杂的配置选项
*/
"include":[
// ** : 任意目录 , * : 任意文件
"./src/**/*"
],
"exclude": [
"./src/hello/**/*"
],
// "extends": "./configs/base",
"files": [
"1.ts",
// "2.ts"
],
"compilerOptions": {
// 用来指定 ES 版本 ESNext : 最新版。 'ES3', 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ESNext'
"target": "ES2020",
// 指定要使用模块化的规范 : 'None', 'CommonJS', 'AMD', 'System', 'UMD', 'ES6'/'ES2015', 'ES2020' or 'ESNext'
"module": "ESNext",
// 用来指定项目中要使用的库 'ES5', 'ES6', 'ES2015', 'ES7', 'ES2016', 'ES2017', 'ES2018', 'ESNext', 'DOM', 'DOM.Iterable',
// 'WebWorker', 'ScriptHost', 'ES2015.Core', 'ES2015.Collection', 'ES2015.Generator', 'ES2015.Iterable',
// 'ES2015.Promise', 'ES2015.Proxy', 'ES2015.Reflect', 'ES2015.Symbol', 'ES2015.Symbol.WellKnown',
// 'ES2016.Array.Include', 'ES2017.object', 'ES2017.Intl', 'ES2017.SharedMemory', 'ES2017.String',
// 'ES2017.TypedArrays', 'ES2018.Intl', 'ES2018.Promise', 'ES2018.RegExp', 'ESNext.AsyncIterable',
// 'ESNext.Array', 'ESNext.Intl', 'ESNext.Symbol'
// 运行在浏览器中不用设置,运行在node或其他中才需要设置
// "lib":[],
// 用来指定编译后文件的存放位置
"outDir":"./dist",
// 将代码合并为一个文件,设置之后所有的全局作用域中的代码会合并到同一个文件中 但是只能在 'amd' and 'system' 中才能使用
// "outFile": "./dist/app.js",
// 是否对js文件进行编译,默认false
"allowJs": false,
// 是否检查js代码是否符合语法规范,默认false
"checkJs": false,
// 是否移除注释,默认false
"removeComments":false,
// 是否不生成编译后文件,默认false
"noEmit": false,
// 当有错误时是否生成文件,默认false
"noEmitOnError": false,
// 是否生成sourceMap,默认false 这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码。
"sourceMap":false,
// 所有的严格检查的总开关,默认false
"strict": false,
// 编译后的文件是否开启严格模式,默认false
"alwaysStrict": false,
// 不允许隐式的any,默认false(允许)
"noImplicitAny": false,
// 不允许隐式的this,默认false(允许)
"noImplicitThis": false,
// 是否严格的检查空值,默认false 检查有可能为null的地方
"strictNullChecks": true,
// 是否严格检查bind、call和apply的参数列表,默认false 检查是否有多余参数
"strictBindCallApply":false,
// 是否严格检查函数的类型,
"strictFunctionTypes":false,
// 是否严格检查属性是否初始化,默认false
"strictPropertyInitialization":false,
// 是否检查switch语句包含正确的break,默认false
"noFallthroughCasesInSwitch":false,
// 检查函数没有隐式的返回值,默认false
"noImplicitReturns":false,
// 是否检查检查未使用的局部变量,默认false
"noUnusedLocals":false,
// 是否检查未使用的参数,默认false
"noUnusedParameters":false,
// 是否检查不可达代码报错,默认false true,忽略不可达代码 false,不可达代码将引起错误
"allowUnreachableCode":false
}
}