1、ts里面的泛型
TypeScript 中的泛型(Generics)是一种强大的类型工具,它允许你在定义函数、类、接口时不预先指定具体类型,而是在使用时动态传入类型,从而实现类型复用和类型安全。简单来说,泛型就像 “类型层面的函数参数”,让代码更灵活且保持类型约束。
一、为什么需要泛型?
没有泛型时,若想实现 “适用于多种类型” 的功能,通常有两种方案,但都有缺陷:
-
使用
any类型:丢失类型检查,无法保证输入输出类型一致。typescript
function identity(arg: any): any { return arg; } // 调用时,输入 number 但输出可能被误当作 string,TS 无法提示错误 const res: string = identity(123); // 不报错,但实际是 number -
为每种类型写重复代码:冗余且维护成本高。
typescript
function identityNumber(arg: number): number { return arg; } function identityString(arg: string): string { return arg; } // ... 更多类型
泛型的解决思路:用一个 “类型变量” 表示未知类型,在使用时指定具体类型,既灵活又保留类型检查。
二、泛型的基本用法
1. 泛型函数
在函数名后用 <T> 声明一个类型变量(T 是约定俗成的名称,也可用 U、V 等),表示 “传入的类型”,并在参数和返回值中使用该变量。
typescript
// 定义泛型函数:输入类型为 T,返回类型也为 T
function identity<T>(arg: T): T {
return arg;
}
// 使用时指定类型(可省略,TS 会自动推断)
const num: number = identity<number>(123); // 正确
const str: string = identity("hello"); // 正确(TS 自动推断 T 为 string)
const bool: boolean = identity(123); // 错误!输入是 number,输出不能是 boolean
泛型函数能保证 “输入类型” 和 “输出类型” 一致,且支持任意类型,避免冗余代码。
2. 泛型接口
接口中使用泛型,可让接口的属性或方法类型动态变化。
typescript
// 定义泛型接口:表示一个“容器”,值的类型为 T
interface Container<T> {
value: T;
getValue: () => T;
}
// 使用时指定具体类型
const numberContainer: Container<number> = {
value: 123,
getValue: () => 123,
};
const stringContainer: Container<string> = {
value: "hello",
getValue: () => "hello",
};
3. 泛型类
类的属性、方法参数 / 返回值可通过泛型动态指定类型。
typescript
// 定义泛型类:栈(Stack),元素类型为 T
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
// 使用时指定类型为 number
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push("2"); // 错误!只能 push number 类型
// 类型自动推断为 string
const stringStack = new Stack();
stringStack.push("a");
stringStack.pop(); // 返回 string | undefined
三、泛型约束:限制类型范围
默认情况下,泛型 T 可以是任意类型,但有时需要限制 T 必须包含某些属性或方法(例如要求 T 有 length 属性),这时需要用泛型约束。
1. 用接口约束泛型
typescript
// 定义约束接口:要求类型必须有 length 属性
interface HasLength {
length: number;
}
// 泛型 T 必须满足 HasLength 约束(即 T 必须有 length 属性)
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length); // 此时访问 length 不会报错
}
logLength("hello"); // 正确(string 有 length)
logLength([1, 2, 3]); // 正确(数组有 length)
logLength(123); // 错误!number 没有 length 属性
2. 约束泛型为另一个类型的子类型
typescript
// 要求 T 是 U 的子类型(T 的属性必须包含 U 的属性)
function merge<T extends U, U>(obj1: T, obj2: U): T {
return { ...obj1, ...obj2 };
}
const merged = merge(
{ name: "Alice", age: 20 }, // T 类型:{ name: string; age: number }
{ name: "Bob" } // U 类型:{ name: string }
);
// merged 类型为 { name: string; age: number },且 name 被 obj2 覆盖为 "Bob"
四、泛型默认值
和函数参数可以有默认值一样,泛型也可以指定默认类型,当使用时未指定类型且无法推断时,会使用默认值。
typescript
// 泛型 T 默认值为 string
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const arr1 = createArray(3, "a"); // T 为 string,返回 string[]
const arr2 = createArray<number>(3, 1); // T 为 number,返回 number[]
const arr3 = createArray(3, true); // 自动推断 T 为 boolean,返回 boolean[]
五、泛型的高级用法
1. 泛型工具类型
TS 内置了一些基于泛型的工具类型,用于快速处理类型转换,例如:
Partial<T>:将T的所有属性变为可选。Readonly<T>:将T的所有属性变为只读。Pick<T, K>:从T中挑选部分属性K组成新类型。
typescript
interface User {
name: string;
age: number;
}
// Partial<User>:{ name?: string; age?: number }
const partialUser: Partial<User> = { name: "Alice" }; // 允许只传部分属性
// Pick<User, "name">:{ name: string }
const pickedUser: Pick<User, "name"> = { name: "Bob" }; // 只包含 name 属性
2. 泛型与联合类型结合
泛型可以和 |(联合类型)配合,限制类型为多个指定类型中的一种。
typescript
// T 只能是 string 或 number
function handle<T extends string | number>(arg: T): T {
return arg;
}
handle("a"); // 正确
handle(1); // 正确
handle(true); // 错误!boolean 不在允许的类型范围内
六、核心作用总结
- 类型安全:避免
any导致的类型丢失,确保输入输出类型一致。 - 代码复用:一套逻辑支持多种类型,减少重复代码。
- 灵活性:在使用时动态指定类型,适应不同场景。
- 可读性:通过泛型变量(如
T)明确代码的类型意图,增强可维护性。
2、ts unknown的使用
在 TypeScript 中,unknown 是一种顶级类型(与 any 类似,可表示任何值),但它比 any 更安全 ——unknown 强制要求在使用前进行类型检查或类型断言,避免因 “任意类型” 导致的类型安全问题。
一、unknown vs any:核心区别
any 会关闭类型检查,变量可被随意赋值、调用任意方法(即使不存在也不报错),而 unknown 则要求 “先确认类型,再使用”。
typescript
// 1. 使用 any:无类型检查,隐藏风险
let valueAny: any = "hello";
valueAny.toUpperCase(); // 正确(实际有效)
valueAny.toFixed(2); // 不报错(但运行时会失败,因为 string 没有 toFixed)
valueAny = 123;
valueAny.split(","); // 不报错(运行时失败,number 没有 split)
// 2. 使用 unknown:必须先确认类型才能使用
let valueUnknown: unknown = "hello";
valueUnknown.toUpperCase(); // 报错!TS 不允许直接使用 unknown 类型的属性/方法
// 必须先做类型检查
if (typeof valueUnknown === "string") {
valueUnknown.toUpperCase(); // 正确(确认是 string 后,TS 允许调用 string 方法)
}
valueUnknown = 123;
if (typeof valueUnknown === "number") {
valueUnknown.toFixed(2); // 正确(确认是 number 后,允许调用 number 方法)
}
结论:unknown 是 “类型安全的 any”,适合替代 any 作为 “未知类型” 的默认选择,强制开发者处理类型逻辑,减少运行时错误。
二、unknown 的使用场景
1. 接收未知类型的输入
当函数参数、API 返回值或用户输入的类型不确定时,用 unknown 替代 any,确保使用前必须验证类型。
typescript
// 示例:处理 API 返回的未知类型数据
async function fetchData(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json(); // json() 返回值类型为 Promise<unknown>(TS 内置)
}
// 使用时必须先判断类型
async function handleData() {
const data = await fetchData("/api/data");
// 错误:直接使用 unknown 类型的属性
console.log(data.name); // 报错!
// 正确:先检查 data 是否为对象且包含 name 属性
if (typeof data === "object" && data !== null && "name" in data) {
console.log(data.name); // 此时 TS 仍无法确定 data.name 类型,可进一步断言
}
}
2. 类型守卫的配合使用
unknown 常与类型守卫(Type Guard)结合,通过条件判断缩小类型范围(类型收窄)。常见的类型守卫包括:
typeof检查基本类型(string/number/boolean 等)instanceof检查对象类型(如Date/Array)- 自定义类型守卫函数(返回
arg is Type)
typescript
// 1. 用 typeof 收窄基本类型
function handleUnknown(value: unknown) {
if (typeof value === "string") {
value.length; // 正确(已确认是 string)
} else if (typeof value === "number") {
value.toFixed(1); // 正确(已确认是 number)
}
}
// 2. 用 instanceof 收窄对象类型
function isDate(value: unknown): value is Date {
return value instanceof Date;
}
const value: unknown = new Date();
if (isDate(value)) {
value.getFullYear(); // 正确(已确认是 Date)
}
// 3. 自定义类型守卫检查接口
interface User {
name: string;
age: number;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof (value as User).name === "string" &&
"age" in value &&
typeof (value as User).age === "number"
);
}
const userData: unknown = { name: "Alice", age: 20 };
if (isUser(userData)) {
console.log(userData.name); // 正确(已确认是 User 类型)
}
3. 作为函数返回值的 “安全默认”
当函数可能返回多种类型,且无法提前确定时,用 unknown 迫使调用者处理类型判断,而不是直接使用 any 隐藏风险。
typescript
// 解析 JSON 字符串(可能返回任何类型)
function parseJson(jsonString: string): unknown {
return JSON.parse(jsonString); // JSON.parse 返回 any,但此处显式声明为 unknown 更安全
}
// 调用时必须检查类型
const result = parseJson('{"id": 1}');
if (typeof result === "object" && result !== null && "id" in result) {
const id = (result as { id: number }).id; // 类型断言确认后使用
}
三、unknown 的限制与注意事项
-
不能直接赋值给其他类型
unknown类型的变量不能直接赋值给非unknown/any类型的变量,除非先确认类型。typescript
let unknownVal: unknown = "test"; let str: string = unknownVal; // 报错!不能直接赋值 if (typeof unknownVal === "string") { str = unknownVal; // 正确(已确认类型) } -
不能直接调用方法 / 访问属性即使
unknown变量实际是某个类型,也必须先通过类型检查或断言,否则 TS 会报错(这是与any的核心区别)。 -
与
any的转换unknown和any可以互相赋值(但应尽量避免,保持unknown的安全性):typescript
let a: any = 123; let u: unknown = a; // 正确(any 可赋值给 unknown) u = "hello"; a = u; // 正确(unknown 可赋值给 any) -
类型断言的使用若确定
unknown变量的类型,可通过as进行类型断言(但需谨慎,错误的断言会导致运行时问题):typescript
const value: unknown = "hello"; const str = value as string; // 断言为 string(需确保实际类型正确) str.toUpperCase(); // 正确
四、何时使用 unknown 而非 any?
- 当你需要表示 “类型不确定”,但希望 TypeScript 强制进行类型检查时,用
unknown。 - 当你确实需要 “关闭类型检查”(例如兼容非类型化的 JS 代码),且愿意承担类型风险时,才用
any。
最佳实践:默认用 unknown 处理未知类型,仅在必要时使用 any,这能显著提升代码的类型安全性。
最佳实践总结
- 首选
unknownoverany:当你不确定类型时,总是优先使用unknown而不是any。 - 强制类型安全:
unknown强制你在使用值之前进行适当的类型检查,这能帮助你在编译时捕获潜在的错误。 - 合理使用类型断言:只有在你有绝对把握知道值的类型时才使用类型断言,并做好错误处理。
- 善用类型守卫:使用
typeof、instanceof或自定义类型守卫来安全地缩小unknown类型的范围。
一句话总结:unknown 保持了 TypeScript 的类型安全性,同时提供了处理动态内容的灵活性。它让你明确地表达"我不知道这个值的类型,所以在使用它之前我会先检查",这是编写健壮 TypeScript 代码的重要模式。
unknown 与 any 的对比
3、interface和type的区别
在 TypeScript 中,interface 和 type 都用于描述类型结构,但它们在功能、语法和使用场景上存在一些关键区别。理解这些区别有助于在不同场景下选择更合适的工具。
一、核心功能对比
二、具体区别详解
1. 定义范围不同
-
interface:只能描述对象类型或类的结构(包括对象的属性、方法,类的实例类型等),无法用于基本类型(如number、string)、联合类型、元组等。typescript
// 正确:描述对象类型 interface User { name: string; age: number; } // 正确:描述类的实例类型(配合 class 使用) interface Person { sayHi(): void; } class Student implements Person { sayHi() { console.log("Hi"); } } // 错误:不能描述基本类型 interface Age { number } // 语法错误 -
type:可描述任意类型,包括基本类型、联合类型、交叉类型、元组、函数类型等,适用范围更广。typescript
// 基本类型别名 type Age = number; // 联合类型 type Status = "success" | "error" | "pending"; // 交叉类型(合并多个类型) type User = { name: string } & { age: number }; // 元组类型 type Point = [x: number, y: number]; // 函数类型 type Add = (a: number, b: number) => number;
2. 扩展方式不同
-
interface:通过extends继承扩展,支持多继承,且语法更直观(类似面向对象的继承)。typescript
interface Animal { name: string; } // 继承 Animal interface Dog extends Animal { bark(): void; } // 多继承:同时继承 Animal 和 { age: number } interface Cat extends Animal, { age: number } { meow(): void; } const dog: Dog = { name: "Buddy", bark: () => console.log("Woof"), }; -
type:通过交叉类型(&)扩展,将多个类型合并为一个新类型。typescript
type Animal = { name: string }; // 交叉类型扩展(类似继承) type Dog = Animal & { bark(): void }; const dog: Dog = { name: "Buddy", bark: () => console.log("Woof"), };
3. 重复声明的处理不同
-
interface:支持重复声明并自动合并,适合在多人协作或第三方库扩展时,为已有接口添加新属性。typescript
// 第一次声明 interface User { name: string; } // 重复声明:自动合并(添加 age 属性) interface User { age: number; } // 合并后:User = { name: string; age: number } const user: User = { name: "Alice", age: 20, }; -
type:不允许重复声明,重复定义会直接报错(类型别名具有唯一性)。typescript
type User = { name: string }; type User = { age: number }; // 错误!标识符“User”重复
4. 与类的结合不同
-
interface可用于class的implements,强制类遵循接口定义的结构(更适合描述类的 “契约”)。typescript
interface Vehicle { speed: number; move(): void; } class Car implements Vehicle { speed: number = 0; move() { this.speed = 60; } // 必须实现 move 方法 } -
type也可用于implements,但当type包含联合类型等复杂结构时,类无法实现(会报错)。typescript
type Vehicle = { speed: number } | { weight: number }; // 错误!类不能实现联合类型的 type class Car implements Vehicle { speed: number = 0; }
5. 其他细节差异
-
interface不能直接定义字面量类型,而type可以:typescript
type Yes = "yes"; // 正确:字面量类型别名 interface Yes { "yes" } // 错误:语法不支持 -
type可使用typeof推导类型,interface不行:typescript
const obj = { a: 1, b: 2 }; type ObjType = typeof obj; // 正确:推导为 { a: number; b: number } interface ObjInterface = typeof obj; // 错误:语法不支持
三、使用场景建议
-
优先用
interface当:- 描述对象、类的结构(尤其是需要被其他类实现时)。
- 需要扩展或合并类型(如多人协作扩展同一接口)。
- 定义公共 API 或库的类型契约(更符合开发者直觉)。
-
优先用
type当:- 描述基本类型、联合类型、交叉类型、元组、函数类型等。
- 需要为类型创建别名(提高可读性,如
type Age = number)。 - 不需要重复声明或合并类型时。
总结
interface 更适合描述 “对象 / 类的结构契约”,支持扩展和合并;type 更灵活,可描述任意类型,适合复杂类型组合。在实际开发中,两者并非互斥,可根据具体场景混合使用(例如用 type 定义联合类型,再用 interface 扩展对象类型)。
4、awaite是宏任务还是微任务
在 JavaScript 中,await 本身并不直接是宏任务或微任务,但它的执行逻辑与微任务队列密切相关。具体来说,await 会暂停当前异步函数的执行,等待右侧表达式(通常是一个 Promise)完成,而等待期间的后续代码会被放入微任务队列。
详细执行逻辑:
-
当执行到
await promise时:- 首先执行
promise表达式(立即同步执行,直到返回一个 Promise 对象)。 - 然后,
await会将当前异步函数中await之后的代码(称为 “后续代码”)包装成一个微任务,放入微任务队列。 - 暂停当前异步函数的执行,跳出函数,继续执行主线程中后续的同步代码。
- 首先执行
-
当
promise状态变为fulfilled(成功)或rejected(失败)时:- 主线程同步代码执行完毕后,会优先清空微任务队列。
- 此时,之前被
await包装的 “后续代码” 会作为微任务被执行,异步函数恢复执行。
console.log('start');
async function asyncFn() {
console.log('asyncFn start');
await Promise.resolve(); // 这里的 await 会触发微任务
console.log('asyncFn end'); // 这行是 await 的后续代码,会放入微任务
}
asyncFn();
console.log('end');
执行顺序:
start(同步代码)asyncFn start(同步执行 asyncFn 中的同步部分)end(主线程同步代码继续执行)asyncFn end(微任务队列清空时执行)
可见,await 后的代码是作为微任务执行的。
结论:
await 本身是语法关键字,不是任务类型,但它会将后续代码注册为微任务。这也是为什么 await 之后的代码总是在主线程同步代码执行完毕、且微任务队列清空时才会执行。
5、如何做前端规范
前端规范是保障团队协作效率、代码质量和项目可维护性的核心手段,需要从代码风格、开发流程、性能优化、工程化配置等多维度制定规则,并通过工具自动化落地。以下是一套完整的前端规范实施方案:
一、代码规范:统一风格,减少争议
1. 基础语法规范
-
JavaScript/TypeScript:遵循 Airbnb JavaScript 风格指南(业界公认最全面),核心规则包括:
- 变量声明:优先用
const,其次let,禁止var。 - 函数:优先用箭头函数(除非需要
this绑定),函数名采用小驼峰camelCase。 - 对象 / 数组:简洁语法(
{ a: 1 }而非new Object()),末尾不加逗号。 - TypeScript 额外规则:接口名用大驼峰
PascalCase,类型定义避免any,优先用unknown。
- 变量声明:优先用
-
CSS/SCSS:遵循 BEM 命名规范(块
block- 元素element- 修饰符modifier),例如:scss
.header { /* 块 */ &__logo { /* 元素 */ } &--dark { /* 修饰符 */ } }其他规则:禁止
!important,使用rem/vw做响应式,避免嵌套超过 3 层。 -
HTML:标签小写,属性用双引号,语义化标签优先(
header/main/section替代div),避免内联样式和脚本。
2. 工具强制落地
通过自动化工具检测并修复不符合规范的代码,避免人工检查成本:
-
ESLint:检测 JS/TS 语法错误和风格问题,配合以下插件:
-
eslint-config-airbnb-base:集成 Airbnb 规则。 -
@typescript-eslint:支持 TypeScript 语法检查。 -
eslint-plugin-react/vue:针对框架的特殊规则(如 React Hooks 依赖项检查)。 -
配置示例(
.eslintrc.js):javascript
运行
module.exports = { extends: ["airbnb-base", "plugin:@typescript-eslint/recommended"], rules: { "no-console": "warn", // 允许 console 但警告 "linebreak-style": ["error", "unix"], // 强制 Unix 换行符 }, };
-
-
Prettier:专注代码格式化(缩进、换行、引号等),与 ESLint 配合使用(用
eslint-config-prettier解决规则冲突)。 -
StyleLint:检测 CSS/SCSS 风格问题,配合
stylelint-config-standard规则集。 -
Husky + lint-staged:在 Git 提交前自动执行代码检查和格式化,阻止不规范代码提交:
json
// package.json { "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts}": ["eslint --fix", "prettier --write"], "*.{css,scss}": ["stylelint --fix"] } }
二、目录结构规范:统一项目组织方式
根据项目类型(PC 端 / 移动端 / 库)约定目录结构,例如 React 项目:
plaintext
src/
├── api/ # 接口请求(按模块划分,如 user.ts, order.ts)
├── assets/ # 静态资源(images/fonts/styles,全局样式放此目录)
├── components/ # 公共组件(按功能划分,每个组件一个文件夹)
│ ├── Button/
│ │ ├── index.tsx
│ │ ├── Button.tsx
│ │ └── Button.module.scss
├── hooks/ # 自定义 Hooks(如 useRequest.ts, useStorage.ts)
├── pages/ # 页面组件(路由对应的页面,包含私有组件)
│ ├── Home/
│ └── User/
├── types/ # TypeScript 类型定义(全局类型或接口)
├── utils/ # 工具函数(工具类、常量、格式化函数等)
├── App.tsx
└── index.tsx
核心原则:按 “功能 / 模块” 划分,而非 “类型”(如避免所有组件堆在一个 components 文件夹);文件命名统一(组件用 PascalCase,工具函数用 camelCase,常量用 UPPER_CASE)。
三、开发流程规范:从需求到上线的标准化步骤
-
分支管理:采用 Git Flow 或简化版流程:
main/master:生产环境代码,禁止直接提交。develop:开发分支,集成各功能分支。feature/xxx:功能开发分支(从develop创建,完成后合并回develop)。hotfix/xxx:紧急修复分支(从main创建,修复后合并到main和develop)。
-
提交规范:使用 Conventional Commits 规范,提交信息格式:
plaintext
<类型>[可选作用域]: <描述>类型包括:
feat(新功能)、fix(修复)、docs(文档)、style(格式)、refactor(重构)等。工具:用commitlint配合 Husky 校验提交信息,commitizen提供交互式提交工具。 -
代码审查(Code Review) :
-
提交 PR/MR 前必须自我检查(运行 lint、测试用例)。
-
至少 1 名团队成员审核通过后才能合并,重点检查:
- 是否符合代码规范。
- 逻辑是否正确,是否有性能隐患。
- 是否有冗余代码或重复逻辑。
-
四、性能与质量规范:避免线上问题
-
性能优化规则:
-
加载性能:
- 图片:优先用 WebP 格式,按需加载(
lazyload),避免大图(单个图片不超过 200KB)。 - 资源:JS/CSS 压缩(
terser/css-minimizer),Tree-Shaking 移除无用代码,CDN 分发静态资源。
- 图片:优先用 WebP 格式,按需加载(
-
运行性能:
- 避免长任务(单个同步任务不超过 50ms),复杂计算用 Web Worker。
- 减少 DOM 操作,频繁操作用
documentFragment或虚拟 DOM。 - 合理使用缓存(
localStorage存储非敏感数据,避免存储过大值)。
-
-
可访问性(A11y) :
- 按钮、链接等可交互元素必须支持键盘访问(
tab聚焦)。 - 图片添加
alt属性,表单元素关联label。 - 颜色对比度符合 WCAG 标准(文本与背景对比度至少 4.5:1)。
- 按钮、链接等可交互元素必须支持键盘访问(
-
兼容性:
- 明确支持的浏览器版本(如 Chrome ≥ 80,iOS ≥ 14),用
browserslist配置。 - 新语法需通过 Babel 转译(如 ES6+ 转 ES5),CSS 新特性需加前缀(
autoprefixer)。
- 明确支持的浏览器版本(如 Chrome ≥ 80,iOS ≥ 14),用
五、文档规范:降低协作成本
-
项目文档:
- 根目录必备
README.md:包含项目介绍、启动命令、目录说明、环境配置。 - 接口文档:用 Swagger、Apifox 或 Markdown 维护,包含请求参数、响应格式、错误码。
- 根目录必备
-
代码注释:
-
公共组件 / 函数:必须写 JSDoc 注释(描述功能、参数、返回值):
typescript
/** * 格式化日期 * @param {Date} date - 日期对象 * @param {string} format - 格式字符串(如 'YYYY-MM-DD') * @returns {string} 格式化后的日期 */ function formatDate(date: Date, format: string): string { ... } -
复杂逻辑:在代码块前添加注释说明设计思路(避免 “解释代码”,而是 “解释为什么这么做”)。
-
六、规范落地与迭代
- 新人培训:将规范文档作为新人入职资料,确保全员理解。
- 定期复盘:每季度回顾规范执行情况,根据团队反馈调整(如新增常用组件规范、修改不合理的目录结构)。
- 工具链自动化:尽可能用工具替代人工检查(如 ESLint 规则、CI 流程中加入性能检测),减少 “人为协商成本”。
总结
前端规范的核心不是 “限制”,而是 “降低协作成本” 和 “规避常见问题”。制定时需结合团队规模(小团队可简化,大团队需更细致)、技术栈(React/Vue 各有侧重)和业务场景(ToB/ToC 性能要求不同),并通过 “工具强制 + 流程保障 + 持续迭代” 确保落地效果。
6、如何判断组件的颗粒度
判断组件颗粒度的核心是平衡复用性、维护性与性能,既避免将组件拆解得过细导致逻辑分散,也避免组件过大导致复用困难,最终目标是让每个组件 “职责单一、边界清晰”。
一、核心判断原则:从 “职责” 和 “复用” 切入
组件颗粒度没有绝对标准,但可通过以下 3 个核心原则快速判断,满足任意 1 条即可考虑拆分或合并。
1. 单一职责原则(最核心)
一个组件只负责一件事,若组件内包含 “不相关的功能模块”,则需要拆分。
- 反例:一个
UserCard组件内,既包含用户信息展示(头像、姓名),又包含 “关注用户” 按钮的点击逻辑、“发送消息” 的弹窗逻辑 —— 这 3 个功能可拆分为UserInfo(纯展示)、FollowButton(交互按钮)、MessageModal(弹窗)3 个组件。 - 正例:
Button组件只负责按钮的样式、状态(禁用 / 加载)和点击回调,不包含具体业务逻辑(如 “点击后提交表单” 的逻辑由父组件传递)。
2. 复用性原则
若某段 UI 或逻辑在 2 个及以上地方出现,且功能 / 样式高度相似,则应拆分为独立组件,避免重复代码。
- 场景 1:多个页面都有 “带图标的标题栏”(如 “我的订单”“我的收藏” 页面顶部),可拆为
IconTitle组件,通过 props 传递标题文字、图标类型。 - 场景 2:表单中多次出现 “带校验的输入框”(如手机号、邮箱输入),可拆为
FormInput组件,内置校验规则和错误提示逻辑。
3. 维护性原则
若组件代码量过大(如超过 500 行),或逻辑嵌套过深(如 JSX 嵌套超过 3 层),会导致后续修改困难,需按 “功能模块” 拆分。
- 判断标准:修改一个小需求时,若需要在组件内多处修改(如改一个按钮样式要找 100 行代码),说明颗粒度太粗;若修改一个功能需要联动修改多个组件(如改 “用户头像大小” 要改 3 个组件),说明颗粒度太细。
二、不同场景的颗粒度参考
根据组件的 “复用范围” 和 “业务关联性”,可将颗粒度分为 3 个层级,对应不同拆分策略。
| 颗粒度层级 | 适用范围 | 特点 | 示例 |
|---|---|---|---|
| 基础组件 | 全局复用(跨项目 / 跨页面) | 纯 UI / 通用逻辑,无业务关联 | Button、Input、Modal、Icon |
| 业务组件 | 业务内复用(跨页面) | 包含特定业务逻辑,可配置性强 | UserCard(用户卡片)、OrderItem(订单项)、FormInput(带业务校验的输入框) |
| 页面组件 | 单个页面内使用 | 聚合业务组件,负责页面逻辑 | HomePage(首页容器)、UserProfilePage(用户资料页容器) |
关键区别:
- 基础组件:不包含业务逻辑,比如
Button组件只关心 “点击事件传递”,不关心 “点击后是提交表单还是跳转页面”。 - 业务组件:绑定具体业务,比如
OrderItem组件会展示订单状态(待支付 / 已完成)、计算订单金额,这些都是业务相关的逻辑。 - 页面组件:不直接复用,主要作用是 “组合其他组件”,比如
HomePage会引入Banner、ProductList、Footer等组件,并处理页面级逻辑(如请求首页数据、控制弹窗显示)。
三、常见误区:避免过度拆分或过度耦合
1. 误区 1:过度拆分(颗粒度太细)
- 问题:将组件拆解得过细,导致组件数量激增,且组件间依赖复杂(如一个页面需要引入 10 + 小组件),反而增加维护成本。
- 反例:把
UserCard拆分为UserAvatar(头像)、UserName(姓名)、UserLevel(等级)3 个独立组件,而这 3 个组件从未在其他地方复用 —— 完全可以合并为UserCard的内部元素,通过 props 控制显示。 - 判断方法:若一个组件的 “复用次数为 1”,且只被父组件唯一引用,可考虑与父组件合并(除非父组件本身已过大)。
2. 误区 2:过度耦合(颗粒度太粗)
- 问题:将多个不相关的功能塞进一个组件,导致组件 “臃肿”,无法复用且修改风险高。
- 反例:一个
Dashboard组件内,既包含 “数据统计图表”,又包含 “用户列表”,还包含 “系统通知弹窗”—— 这 3 个模块无直接关联,应拆分为StatChart、UserList、NoticeModal3 个组件,由Dashboard作为容器组合。 - 判断方法:若组件内存在 “可以独立删除而不影响其他功能” 的模块(如删掉 “通知弹窗”,数据统计功能仍正常),则该模块应拆分为独立组件。
四、辅助判断工具:从 “使用场景” 反推
当不确定是否拆分时,可通过以下 2 个场景反推颗粒度是否合理:
1. 场景 1:复用测试
假设未来有一个新页面需要用到当前组件的 “某部分功能”,能否直接复用?
- 若能:说明当前颗粒度合适(如
FollowButton组件可直接在 “用户卡片” 和 “用户详情页” 复用)。 - 若不能:说明颗粒度太粗(如
UserCard包含 “关注” 和 “消息” 逻辑,新页面只需要 “关注” 功能,却要引入整个UserCard)。
2. 场景 2:修改测试
若需要修改组件的 “某部分功能”,是否会影响其他不相关的逻辑?
- 若不会:说明颗粒度合适(如修改
Button组件的禁用样式,不会影响点击回调逻辑)。 - 若会:说明颗粒度太粗(如修改
UserCard的 “关注按钮颜色”,却要动到 “用户信息展示” 的代码)。
总结
组件颗粒度的本质是 “权衡”——优先按 “单一职责” 拆分,再用 “复用性” 验证,最后用 “维护成本” 调整。小项目 / 简单页面可适当放宽(组件稍大也没关系),大项目 / 复杂业务需更精细(确保复用和维护效率)。
7、js设计模式
JavaScript 设计模式是解决常见编程问题的可复用方案,有助于提高代码的可维护性、可读性和扩展性。以下是一些常用的设计模式及其在 JS 中的实现特点:
1. 单例模式(Singleton)
-
核心:保证一个类仅有一个实例,并提供全局访问点。
-
场景:全局状态管理(如 Vuex/Redux)、弹窗管理等。
-
实现:
javascript
运行
class Singleton { constructor() { if (Singleton.instance) return Singleton.instance; Singleton.instance = this; } } const instance1 = new Singleton(); const instance2 = new Singleton(); console.log(instance1 === instance2); // true
2. 工厂模式(Factory)
-
核心:封装对象创建逻辑,通过工厂方法动态生成不同类型的对象。
-
场景:根据条件创建不同实例(如表单组件工厂)。
-
实现:
javascript
运行
class Car { constructor() { this.type = 'car'; } } class Bike { constructor() { this.type = 'bike'; } } const VehicleFactory = { create: (type) => { switch (type) { case 'car': return new Car(); case 'bike': return new Bike(); default: throw new Error('Unknown type'); } } }; const car = VehicleFactory.create('car');
3. 观察者模式(Observer)
-
核心:定义对象间的一对多依赖,当一个对象状态变化时,所有依赖者自动收到通知。
-
场景:事件监听、发布 - 订阅系统(如 DOM 事件、Vue 响应式)。
-
实现:
javascript
运行
class Subject { constructor() { this.observers = []; } addObserver(observer) { this.observers.push(observer); } notify(data) { this.observers.forEach(obs => obs.update(data)); } } class Observer { update(data) { console.log('Received:', data); } } const subject = new Subject(); subject.addObserver(new Observer()); subject.notify('Hello'); // 输出 "Received: Hello"
4. 装饰器模式(Decorator)
-
核心:动态给对象添加新功能,不改变其原有结构。
-
场景:日志增强、权限控制、React HOC。
-
实现:
javascript
运行
function withLogging(fn) { return (...args) => { console.log('Calling:', fn.name); return fn(...args); }; } const add = (a, b) => a + b; const addWithLog = withLogging(add); addWithLog(1, 2); // 输出 "Calling: add" 并返回 3
5. 代理模式(Proxy)
-
核心:为对象提供一个代理层,控制对原对象的访问。
-
场景:数据校验、缓存、懒加载(ES6
Proxy原生支持)。 -
实现:
javascript
运行
const target = { name: 'Alice' }; const proxy = new Proxy(target, { get: (obj, prop) => { console.log(`Accessing ${prop}`); return obj[prop]; } }); console.log(proxy.name); // 输出 "Accessing name" 和 "Alice"
6. 模块模式(Module)
-
核心:通过闭包封装私有变量和方法,暴露公共接口。
-
场景:模块化开发(ES6 模块前的实现方式)。
-
实现:
javascript
运行
const Module = (() => { const privateVar = 'secret'; return { getPublic: () => privateVar, setPublic: (val) => { /* ... */ } }; })();
7. 策略模式(Strategy)
-
核心:定义一系列算法,封装并使其可互换。
-
场景:表单验证规则、支付方式选择。
-
实现:
javascript
运行
const strategies = { add: (a, b) => a + b, multiply: (a, b) => a * b }; const calculate = (strategy, a, b) => strategies[strategy](a, b); calculate('add', 2, 3); // 5
8. 适配器模式(Adapter)
-
核心:转换一个接口为另一个接口,使不兼容的类可以协同工作。
-
场景:新旧 API 兼容、第三方库适配。
-
实现:
javascript
运行
// 旧接口 const oldAPI = { getUsers: () => [1, 2, 3] }; // 适配器 const adapter = { fetchUsers: () => oldAPI.getUsers().map(id => ({ id })) };
总结
- 设计模式不是银弹,需根据场景选择(如 React 中常用观察者、代理、装饰器模式)。
- JS 特性(如闭包、原型、
Proxy)简化了许多模式的实现。 - 现代框架(Vue/React)内部大量使用设计模式,理解模式有助于源码阅读。
8、async awaite实现原理
async/await 是 JavaScript 中用于简化异步操作的语法糖,其底层基于 Promise 和 Generator 函数 的设计思想实现,本质是对 Promise 链式调用的封装,让异步代码看起来更接近同步代码的逻辑。
核心原理
async/await 的工作机制可以拆解为以下几点:
-
async函数的返回值自动包装为 Promise无论async函数内部返回什么值(非 Promise 类型),都会被自动包装成一个 已 resolved 的 Promise。如果函数内部抛出错误,则返回一个 已 rejected 的 Promise。javascript
运行
async function fn() { return 123; // 等价于 return Promise.resolve(123) } fn().then(res => console.log(res)); // 123 -
await暂停执行,等待 Promise 完成await关键字只能在async函数内部使用,它的作用是:- 接收一个 Promise 对象(如果不是 Promise,则会先自动包装为
Promise.resolve(值))。 - 暂停当前
async函数的执行,将执行权交还给外部环境(避免阻塞主线程)。 - 当 Promise 状态变为
resolved时,恢复async函数的执行,并将 Promise 的结果作为await表达式的返回值。 - 如果 Promise 状态变为
rejected,则会抛出异常,需要用try/catch捕获。
- 接收一个 Promise 对象(如果不是 Promise,则会先自动包装为
-
基于 Generator 和自动执行器的模拟实现
async/await的行为类似 Generator 函数 + 自动执行器。Generator 函数(function*)通过yield暂停执行,而async/await可以看作是 Generator 的 “语法糖”,但区别在于:async函数会自动执行,不需要像 Generator 那样手动调用next()或使用co等库。await只能等待 Promise,而yield可以等待任意值。
用 Generator 模拟
async/await的示例:javascript
运行
// Generator 函数模拟 async 函数 function* asyncFunc() { const result1 = yield Promise.resolve(1); const result2 = yield Promise.resolve(2); return result1 + result2; } // 自动执行器(模拟 async/await 的底层执行逻辑) function run(generator) { const iterator = generator(); function handleNext(value) { const { done, value: promise } = iterator.next(value); if (done) { return Promise.resolve(value); // 最终返回结果 } // 递归执行下一步,直到 Generator 完成 return promise.then(res => handleNext(res)); } return handleNext(); } // 执行 run(asyncFunc).then(res => console.log(res)); // 3可以看到,
async/await的底层逻辑与上述自动执行器类似:通过递归处理 Promise 的状态变化,自动推进代码执行,直到异步操作全部完成。 -
事件循环中的执行顺序
await会将后续代码放入微任务队列,等待当前 Promise 处理完成后执行。例如:javascript
运行
async function test() { console.log(1); await Promise.resolve(); console.log(2); // 微任务:等待同步代码执行完后执行 } test(); console.log(3); // 输出顺序:1 → 3 → 2
总结
async/await是 Promise 的语法糖,完全基于 Promise 实现,不改变 JavaScript 单线程和异步的本质。- 其核心是通过 暂停 / 恢复执行 的机制,让异步代码的逻辑更清晰,避免了 Promise 链式调用(
.then())的嵌套问题。 - 底层可理解为:
async函数返回 Promise,await通过自动执行的 “Generator 逻辑” 等待 Promise 完成,并将结果返回给同步代码流。
9、如何做异步加载
在 JavaScript 中实现异步加载(非阻塞加载)的核心是避免资源加载阻塞页面解析或渲染,常用于优化大型脚本、非首屏资源或第三方依赖的加载。以下是几种常用方案:
一、脚本(JS)的异步加载
1. 利用 <script> 标签的 async 和 defer 属性
直接在 HTML 中通过标签属性控制加载行为,无需额外 JS 逻辑:
async:异步加载,加载完成后立即执行(不保证顺序)。defer:异步加载,等待 HTML 解析完成后按顺序执行(适合有依赖关系的脚本)。
html
预览
<!-- async:加载完立即执行,可能乱序 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
<!-- defer:HTML 解析完后按顺序执行(先执行 lib.js,再执行 main.js) -->
<script src="lib.js" defer></script>
<script src="main.js" defer></script>
2. 动态创建 <script> 标签
通过 JS 动态生成脚本标签并插入 DOM,实现按需加载,且默认不阻塞页面:
javascript
运行
function loadScript(url, onLoad, onError) {
const script = document.createElement('script');
script.src = url;
// 加载成功回调
script.onload = () => {
console.log(`脚本 ${url} 加载完成`);
onLoad && onLoad();
};
// 加载失败处理
script.onerror = () => {
console.error(`脚本 ${url} 加载失败`);
onError && onError();
};
// 插入到 DOM 中(开始加载)
document.body.appendChild(script);
}
// 使用示例:加载并执行一个工具库
loadScript(
'https://cdn.example.com/util.js',
() => { console.log('工具库可用'); },
() => { console.log('备用方案执行'); }
);
注意:若多个脚本有依赖关系(如 B 依赖 A),需通过链式调用保证顺序:
javascript
运行
loadScript('A.js', () => {
loadScript('B.js', () => { /* B 依赖 A,A 加载完再加载 B */ });
});
3. ES6 动态 import()(模块化异步加载)
现代浏览器支持的原生模块化加载方式,返回 Promise,适合按需加载模块(如路由懒加载):
javascript
运行
// 基础用法
import('./api.js')
.then((module) => {
// 模块加载成功后使用导出的方法
module.fetchData();
})
.catch((err) => {
console.error('模块加载失败', err);
});
// 结合 async/await 更简洁
async function init() {
try {
const utils = await import('./utils.js');
utils.formatData();
} catch (err) {
// 错误处理
}
}
init();
特点:原生支持模块化,适合现代前端工程化项目(Webpack、Vite 等均支持)。
二、其他资源的异步加载
1. 图片(Image)异步加载
常用于懒加载(滚动到可视区域再加载):
javascript
运行
function loadImage(url, container) {
const img = new Image(); // 创建图片对象
img.src = url; // 开始加载
img.onload = () => {
container.appendChild(img); // 加载完成后插入 DOM
};
img.onerror = () => {
container.innerHTML = '图片加载失败';
};
}
// 使用:点击按钮后加载图片
document.getElementById('loadBtn').addEventListener('click', () => {
loadImage('https://example.com/image.jpg', document.getElementById('imgContainer'));
});
2. CSS 样式表异步加载
通过动态创建 <link> 标签加载样式,不阻塞页面解析:
javascript
运行
function loadCSS(url) {
const link = document.createElement('link');
link.rel = 'stylesheet'; // 声明为样式表
link.href = url;
link.media = 'print'; // 临时设置为 print,避免阻塞渲染
document.head.appendChild(link);
// 加载完成后恢复媒体类型
link.onload = () => {
link.media = 'all';
};
}
// 加载非首屏样式
loadCSS('https://example.com/extra-style.css');
三、核心原则与最佳实践
- 优先非阻塞:对非首屏必要资源,优先使用
async/defer或动态加载,避免阻塞 HTML 解析。 - 按需加载:结合用户行为(如点击、滚动)触发加载,减少初始加载体积(例如:点击 “查看更多” 再加载详情脚本)。
- 错误处理:必须监听
onerror事件,避免加载失败导致功能异常。 - 顺序控制:有依赖关系的脚本用
defer(按标签顺序)或链式回调(动态加载)保证执行顺序。 - 利用浏览器缓存:通过合理的缓存策略(如
Cache-Control)减少重复加载。
10、高阶函数如何控制返回参数
JavaScript 高阶函数(接收或返回函数的函数)控制返回参数的核心是通过拦截、加工、转发参数或返回值的过程,实现对输出的精准控制。常见手段包括闭包固化参数、参数过滤、返回值包装、动态调整等,以下是具体实现方式:
一、通过闭包 “固化” 部分参数(部分应用)
高阶函数可以通过闭包捕获外部变量,将部分参数提前 “固定”,子函数只需传入剩余参数,从而控制最终返回的参数组合。
javascript
运行
// 高阶函数:固化第一个参数(前缀)
function setPrefix(prefix) {
// 子函数只接收第二个参数,返回拼接结果
return function (value) {
return `${prefix}-${value}`; // 控制返回格式为 "前缀-值"
};
}
// 生成特定前缀的函数
const withUser = setPrefix('user');
console.log(withUser('123')); // "user-123"
console.log(withUser('456')); // "user-456"
场景:固定通用配置(如 API 前缀、日志类型),简化后续调用。
二、参数透传与过滤(控制输入即控制输出)
高阶函数可以对传入的原始参数进行筛选、转换后再传递给内部函数,间接控制最终返回值(因返回值依赖输入参数)。
javascript
运行
// 高阶函数:过滤非数字参数,仅传递有效参数给原函数
function filterNumbers(fn) {
return function (...args) {
// 处理参数:保留数字,过滤其他类型
const validArgs = args.filter(arg => typeof arg === 'number');
// 调用原函数,返回处理后的结果
return fn(...validArgs);
};
}
// 原函数:计算总和
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
// 包装后:仅接收数字参数
const sumValid = filterNumbers(sum);
console.log(sumValid(1, 'a', 3, true, 5)); // 9(仅 1+3+5 有效)
场景:参数校验、格式统一(如去除空值、转换单位)。
三、直接包装 / 修改返回值(控制输出格式)
高阶函数可以拦截内部函数的返回值,加工后再返回,强制统一输出格式。
javascript
运行
// 高阶函数:将结果包装为 { code, data, msg } 结构
function formatResponse(fn) {
return function (...args) {
try {
const result = fn(...args);
// 成功时返回固定结构
return { code: 200, data: result, msg: 'success' };
} catch (err) {
// 失败时返回错误结构
return { code: 500, data: null, msg: err.message };
}
};
}
// 原函数:获取用户信息
function getUser(id) {
if (id < 0) throw new Error('无效 ID');
return { id, name: '张三' };
}
// 包装后:返回统一响应格式
const getFormattedUser = formatResponse(getUser);
console.log(getFormattedUser(1));
// { code: 200, data: { id: 1, name: '张三' }, msg: 'success' }
console.log(getFormattedUser(-1));
// { code: 500, data: null, msg: '无效 ID' }
场景:API 响应标准化、日志格式化、错误统一处理。
四、动态调整返回参数(基于条件)
根据不同条件,高阶函数可以动态决定返回参数的内容或结构。
javascript
运行
// 高阶函数:根据权限控制返回字段
function withPermission(hasAdminRight) {
return function (userInfo) {
const base = { id: userInfo.id, name: userInfo.name };
// 管理员权限:返回完整信息
if (hasAdminRight) {
return { ...base, role: userInfo.role, email: userInfo.email };
}
// 普通权限:仅返回基础信息
return base;
};
}
const user = { id: 1, name: '张三', role: 'admin', email: 'zhangsan@test.com' };
// 普通用户视角:仅基础信息
const getNormalUser = withPermission(false);
console.log(getNormalUser(user)); // { id: 1, name: '张三' }
// 管理员视角:完整信息
const getAdminUser = withPermission(true);
console.log(getAdminUser(user));
// { id: 1, name: '张三', role: 'admin', email: 'zhangsan@test.com' }
场景:权限控制、条件渲染、动态数据脱敏。
五、限制返回参数的数量或类型
通过高阶函数裁剪返回值的长度,或强制转换类型,控制输出范围。
javascript
运行
// 高阶函数:只返回数组的前 n 项
function take(n, fn) {
return function (...args) {
const result = fn(...args);
// 若返回值是数组,截取前 n 项;否则直接返回
return Array.isArray(result) ? result.slice(0, n) : result;
};
}
// 原函数:返回列表
function getList() {
return [10, 20, 30, 40, 50];
}
// 包装后:只返回前 3 项
const take3 = take(3, getList);
console.log(take3()); // [10, 20, 30]
核心逻辑总结
高阶函数控制返回参数的本质是:在 “接收原始参数 → 加工参数 → 执行内部逻辑 → 加工返回值” 的流程中,通过拦截任意环节实现对输出的控制。
关键技巧包括:
- 用闭包固化参数,减少后续调用的参数输入;
- 过滤 / 转换输入参数,确保内部函数接收有效数据;
- 包装返回值,统一格式或添加额外信息;
- 基于条件动态调整返回内容,实现灵活输出。
这种能力让高阶函数在函数式编程(如柯里化、中间件)和业务逻辑封装中非常实用。
11、TypeScript常用
TypeScript(TS)是 JavaScript 的超集,核心优势是类型系统,能在开发阶段捕获类型错误。以下是 TS 中常用的语法和场景举例:
一、基础类型
typescript
// 布尔值
const isDone: boolean = false;
// 数字(支持二进制、八进制、十六进制)
const decLiteral: number = 6;
const hexLiteral: number = 0xf00d;
const binaryLiteral: number = 0b1010;
// 字符串
const name: string = "bob";
const sentence: string = `Hello, my name is ${name}`;
// 数组(两种写法)
const list1: number[] = [1, 2, 3];
const list2: Array<number> = [1, 2, 3]; // 泛型写法
// 元组(固定长度和类型的数组)
const x: [string, number] = ["hello", 10]; // 正确
// const y: [string, number] = [10, "hello"]; // 错误(类型顺序不符)
// 枚举(为一组数值赋予友好名称)
enum Color {
Red,
Green,
Blue,
}
const c: Color = Color.Green; // 值为 1(默认从 0 开始)
console.log(Color[1]); // "Green"(反向映射)
// Any(任意类型,关闭类型检查)
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
// Void(无返回值)
function warnUser(): void {
console.log("This is a warning message");
}
// Null 和 Undefined
const u: undefined = undefined;
const n: null = null;
// Never(永不存在的类型,如抛出错误或无限循环)
function error(message: string): never {
throw new Error(message);
}
二、接口(Interface)
用于定义对象的类型结构(形状):
typescript
// 基础接口
interface Person {
name: string;
age: number;
gender?: string; // 可选属性(可存在或不存在)
readonly id: number; // 只读属性(初始化后不可修改)
}
const person: Person = {
name: "Alice",
age: 30,
id: 1001, // 只读,后续不可修改
// gender: "female" // 可选,可省略
};
// 函数类型接口
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = (src, sub) => {
return src.includes(sub);
};
// 索引类型接口(描述对象的索引方式)
interface StringArray {
[index: number]: string; // 用数字索引时返回 string
}
const arr: StringArray = ["a", "b"];
三、类(Class)
结合类型注解和访问修饰符:
typescript
class Animal {
// 访问修饰符:public(默认)、private、protected
private name: string; // 私有属性,仅类内部可访问
protected age: number; // 受保护属性,类内部和子类可访问
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public sayHi(): string {
return `Hello, I'm ${this.name}`;
}
}
// 继承
class Dog extends Animal {
constructor(name: string, age: number, private breed: string) {
super(name, age); // 调用父类构造函数
}
getBreed(): string {
return this.breed;
}
// 重写父类方法
sayHi(): string {
return `${super.sayHi()}, and I'm a ${this.breed}`;
}
}
const dog = new Dog("Buddy", 3, "Golden Retriever");
console.log(dog.sayHi()); // "Hello, I'm Buddy, and I'm a Golden Retriever"
四、函数(Function)
强化参数和返回值类型:
typescript
// 基础函数类型
function add(x: number, y: number): number {
return x + y;
}
// 可选参数(必须放在必选参数后)
function buildName(firstName: string, lastName?: string): string {
return lastName ? `${firstName} ${lastName}` : firstName;
}
// 默认参数
function greet(name: string = "Guest"): string {
return `Hello, ${name}`;
}
// 剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
// 函数表达式
const multiply: (a: number, b: number) => number = (a, b) => a * b;
五、泛型(Generic)
解决类型复用问题,让函数 / 类 / 接口支持多种类型:
typescript
// 泛型函数:交换数组的前两个元素
function swap<T>(arr: T[]): T[] {
if (arr.length >= 2) {
[arr[0], arr[1]] = [arr[1], arr[0]];
}
return arr;
}
swap([1, 2, 3]); // 正确,T 为 number
swap(["a", "b", "c"]); // 正确,T 为 string
// 泛型接口
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 10 };
const stringBox: Box<string> = { value: "hello" };
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
六、联合类型与类型别名
typescript
// 联合类型(取值可以是多种类型中的一种)
let unionType: string | number;
unionType = "hello"; // 正确
unionType = 123; // 正确
// unionType = true; // 错误
// 类型别名(给类型起别名)
type Data = string | number | boolean;
let data: Data = "test";
data = 100;
data = false;
// 结合联合类型和类型别名
type Status = "success" | "error" | "pending";
const currentStatus: Status = "success"; // 只能是指定的三个值
七、类型断言(Type Assertion)
手动指定变量的类型(告诉 TS “我比你更清楚类型”):
typescript
// 两种语法
const someValue: any = "this is a string";
// 方式1:<类型>值
const strLength1: number = (<string>someValue).length;
// 方式2:值 as 类型(推荐,JSX 中必须用这种)
const strLength2: number = (someValue as string).length;
// 场景:断言 DOM 元素类型
const el = document.getElementById("app") as HTMLDivElement;
el.innerHTML = "Hello"; // 正确(TS 知道 el 是 div 元素)
八、高级类型:交叉类型
合并多个类型为一个(同时拥有所有类型的属性):
typescript
interface A { a: number }
interface B { b: string }
type C = A & B; // 交叉类型:同时有 a 和 b 属性
const c: C = { a: 1, b: "hello" };
九、类型守卫(Type Guard)
缩小变量的类型范围(在条件分支中更精确地推断类型):
typescript
function isNumber(x: any): x is number {
return typeof x === "number";
}
function handleValue(x: string | number) {
if (isNumber(x)) {
// 此处 x 被推断为 number
console.log(x.toFixed(2));
} else {
// 此处 x 被推断为 string
console.log(x.length);
}
}
这些是 TS 最常用的特性,覆盖了类型定义、接口、类、函数、泛型等核心场景。实际开发中,结合这些特性可以大幅提升代码的可维护性和健壮性。
12、Es6常用
ES(ECMAScript)是 JavaScript 的标准规范,从 ES6(2015)开始引入了大量实用特性,极大提升了开发效率。以下是 ES 常用特性的举例:
一、ES6(2015)核心特性
1. 变量声明(let/const)
let:块级作用域,不可重复声明,无变量提升(或说提升后处于 “暂时性死区”)。const:声明常量,块级作用域,赋值后不可修改(引用类型的内部属性可改)。
javascript
运行
// let 示例
if (true) {
let x = 10;
console.log(x); // 10
}
// console.log(x); // 报错(x 仅在块内有效)
// const 示例
const PI = 3.14;
// PI = 3; // 报错(常量不可重新赋值)
const obj = { name: 'Tom' };
obj.age = 20; // 允许(引用地址未变)
2. 箭头函数(=>)
简化函数语法,无自己的 this(继承外层作用域的 this),不能用作构造函数。
javascript
运行
// 基础用法
const add = (a, b) => a + b;
// 单参数可省略括号
const double = x => x * 2;
// 多语句需用大括号和 return
const sum = (...nums) => {
let total = 0;
for (const n of nums) total += n;
return total;
};
// 绑定 this 示例
const obj = {
name: 'Alice',
sayHi: function() {
setTimeout(() => {
console.log(`Hi, ${this.name}`); // this 指向 obj(继承外层 sayHi 的 this)
}, 100);
}
};
obj.sayHi(); // "Hi, Alice"
3. 模板字符串(`)
支持多行字符串和变量插值(${})。
javascript
运行
const name = 'Bob';
const age = 25;
// 变量插值
const info = `Name: ${name}, Age: ${age}`;
console.log(info); // "Name: Bob, Age: 25"
// 多行字符串
const html = `
<div>
<p>${name}</p>
</div>
`;
4. 解构赋值
快速从数组或对象中提取值并赋值给变量。
javascript
运行
// 数组解构
const [a, b, ...rest] = [1, 2, 3, 4];
console.log(a); // 1,b: 2,rest: [3,4]
// 对象解构
const { name, age: userAge } = { name: 'Charlie', age: 30 };
console.log(name); // "Charlie",userAge: 30(重命名属性)
// 默认值
const { city = 'Beijing' } = { name: 'Dave' };
console.log(city); // "Beijing"
5. 扩展运算符(...)
展开数组 / 对象,或收集剩余参数。
javascript
运行
// 展开数组
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1,2,3,4]
// 展开对象
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 }; // { a:1, b:2 }
// 收集剩余参数(函数中)
function logArgs(...args) {
console.log(args); // 接收所有参数为数组
}
logArgs(1, 'a', true); // [1, 'a', true]
6. 类(class)
语法糖,简化原型继承的写法。
javascript
运行
class Person {
constructor(name) {
this.name = name; // 实例属性
}
sayHi() { // 实例方法
return `Hi, ${this.name}`;
}
static create(name) { // 静态方法(类本身的方法)
return new Person(name);
}
}
// 继承
class Student extends Person {
constructor(name, grade) {
super(name); // 调用父类构造函数
this.grade = grade;
}
getGrade() {
return this.grade;
}
}
const student = new Student('Eve', 5);
console.log(student.sayHi()); // "Hi, Eve"
console.log(Student.create('Frank')); // Person 实例
7. 模块(import/export)
实现模块化开发,拆分代码为多个文件。
javascript
运行
// math.js(导出)
export const pi = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; } // 默认导出
// app.js(导入)
import multiply, { pi, add } from './math.js';
console.log(pi); // 3.14
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6
8. Promise
解决异步回调地狱问题,统一异步代码风格。
javascript
运行
// 创建 Promise
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('数据加载成功'); // 成功时调用
} else {
reject(new Error('加载失败')); // 失败时调用
}
}, 1000);
});
};
// 使用 Promise
fetchData()
.then(data => console.log(data)) // 成功回调
.catch(err => console.error(err)) // 失败回调
.finally(() => console.log('无论成功失败都执行'));
二、ES7(2016)
1. 数组 includes ()
判断数组是否包含某个值(比 indexOf 更直观,支持 NaN)。
javascript
运行
const arr = [1, 2, 3, NaN];
console.log(arr.includes(2)); // true
console.log(arr.includes(NaN)); // true(indexOf 会返回 -1)
2. 指数运算符(**)
替代 Math.pow()。
javascript
运行
console.log(2** 3); // 8(等价于 Math.pow(2,3))
三、ES8(2017)
1. async/await
基于 Promise 的语法糖,让异步代码更像同步代码。
javascript
运行
// 结合 Promise 使用
async function getData() {
try {
const data = await fetchData(); // 等待 Promise 完成
console.log(data);
return data;
} catch (err) {
console.error(err);
}
}
getData();
2. 对象方法扩展
Object.values():获取对象所有值组成的数组。Object.entries():获取对象[key, value]数组。
javascript
运行
const obj = { a: 1, b: 2 };
console.log(Object.values(obj)); // [1, 2]
console.log(Object.entries(obj)); // [['a',1], ['b',2]]
四、ES9(2018)
1. 异步迭代(for-await-of)
遍历异步迭代器(如多个 Promise)。
javascript
运行
const promises = [Promise.resolve(1), Promise.resolve(2)];
async function process() {
for await (const p of promises) {
console.log(p); // 依次输出 1、2
}
}
process();
2. 对象扩展运算符改进
支持 rest 参数收集对象剩余属性。
javascript
运行
const { a, ...rest } = { a: 1, b: 2, c: 3 };
console.log(rest); // { b:2, c:3 }
五、ES10+ 常用特性
-
ES10(2019) :
Array.flat()(数组扁平化)、Array.flatMap()、String.trimStart()/trimEnd()。javascript
运行
[1, [2, [3]]].flat(2); // [1,2,3](深度为 2) -
ES11(2020) :
BigInt(大整数)、nullish coalescing(空值合并??)、optional chaining(可选链?.)。javascript
运行
const obj = { a: { b: 1 } }; console.log(obj?.a?.b); // 1(安全访问嵌套属性) console.log(obj?.c ?? '默认值'); // "默认值"(仅 null/undefined 时生效) -
ES12(2021) :
Promise.any()(等待第一个成功的 Promise)、数字分隔符(1_000_000)。
这些特性已被现代浏览器和 Node.js 广泛支持,是日常开发的基础。合理使用能显著简化代码,提升可读性和效率。