德冠

67 阅读32分钟

1、ts里面的泛型

TypeScript 中的泛型(Generics)是一种强大的类型工具,它允许你在定义函数、类、接口时不预先指定具体类型,而是在使用时动态传入类型,从而实现类型复用类型安全。简单来说,泛型就像 “类型层面的函数参数”,让代码更灵活且保持类型约束。

一、为什么需要泛型?

没有泛型时,若想实现 “适用于多种类型” 的功能,通常有两种方案,但都有缺陷:

  1. 使用 any 类型:丢失类型检查,无法保证输入输出类型一致。

    typescript

    function identity(arg: any): any {
      return arg;
    }
    // 调用时,输入 number 但输出可能被误当作 string,TS 无法提示错误
    const res: string = identity(123); // 不报错,但实际是 number
    
  2. 为每种类型写重复代码:冗余且维护成本高。

    typescript

    function identityNumber(arg: number): number { return arg; }
    function identityString(arg: string): string { return arg; }
    // ... 更多类型
    

泛型的解决思路:用一个 “类型变量” 表示未知类型,在使用时指定具体类型,既灵活又保留类型检查。

二、泛型的基本用法

1. 泛型函数

在函数名后用 <T> 声明一个类型变量(T 是约定俗成的名称,也可用 UV 等),表示 “传入的类型”,并在参数和返回值中使用该变量。

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 不在允许的类型范围内

六、核心作用总结

  1. 类型安全:避免 any 导致的类型丢失,确保输入输出类型一致。
  2. 代码复用:一套逻辑支持多种类型,减少重复代码。
  3. 灵活性:在使用时动态指定类型,适应不同场景。
  4. 可读性:通过泛型变量(如 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 的限制与注意事项

  1. 不能直接赋值给其他类型unknown 类型的变量不能直接赋值给非 unknown/any 类型的变量,除非先确认类型。

    typescript

    let unknownVal: unknown = "test";
    let str: string = unknownVal; // 报错!不能直接赋值
    
    if (typeof unknownVal === "string") {
      str = unknownVal; // 正确(已确认类型)
    }
    
  2. 不能直接调用方法 / 访问属性即使 unknown 变量实际是某个类型,也必须先通过类型检查或断言,否则 TS 会报错(这是与 any 的核心区别)。

  3. 与 any 的转换unknown 和 any 可以互相赋值(但应尽量避免,保持 unknown 的安全性):

    typescript

    let a: any = 123;
    let u: unknown = a; // 正确(any 可赋值给 unknown)
    
    u = "hello";
    a = u; // 正确(unknown 可赋值给 any)
    
  4. 类型断言的使用若确定 unknown 变量的类型,可通过 as 进行类型断言(但需谨慎,错误的断言会导致运行时问题):

    typescript

    const value: unknown = "hello";
    const str = value as string; // 断言为 string(需确保实际类型正确)
    str.toUpperCase(); // 正确
    

四、何时使用 unknown 而非 any

  • 当你需要表示 “类型不确定”,但希望 TypeScript 强制进行类型检查时,用 unknown
  • 当你确实需要 “关闭类型检查”(例如兼容非类型化的 JS 代码),且愿意承担类型风险时,才用 any

最佳实践:默认用 unknown 处理未知类型,仅在必要时使用 any,这能显著提升代码的类型安全性。

最佳实践总结

  1. 首选 unknown over any:当你不确定类型时,总是优先使用 unknown 而不是 any
  2. 强制类型安全unknown 强制你在使用值之前进行适当的类型检查,这能帮助你在编译时捕获潜在的错误。
  3. 合理使用类型断言:只有在你有绝对把握知道值的类型时才使用类型断言,并做好错误处理。
  4. 善用类型守卫:使用 typeofinstanceof 或自定义类型守卫来安全地缩小 unknown 类型的范围。

一句话总结unknown 保持了 TypeScript 的类型安全性,同时提供了处理动态内容的灵活性。它让你明确地表达"我不知道这个值的类型,所以在使用它之前我会先检查",这是编写健壮 TypeScript 代码的重要模式。

unknown 与 any 的对比

image.png

3、interface和type的区别

在 TypeScript 中,interface 和 type 都用于描述类型结构,但它们在功能、语法和使用场景上存在一些关键区别。理解这些区别有助于在不同场景下选择更合适的工具。

一、核心功能对比

image.png

二、具体区别详解

1. 定义范围不同
  • interface只能描述对象类型或类的结构(包括对象的属性、方法,类的实例类型等),无法用于基本类型(如 numberstring)、联合类型、元组等。

    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 当

    1. 描述对象、类的结构(尤其是需要被其他类实现时)。
    2. 需要扩展或合并类型(如多人协作扩展同一接口)。
    3. 定义公共 API 或库的类型契约(更符合开发者直觉)。
  • 优先用 type 当

    1. 描述基本类型、联合类型、交叉类型、元组、函数类型等。
    2. 需要为类型创建别名(提高可读性,如 type Age = number)。
    3. 不需要重复声明或合并类型时。

总结

interface 更适合描述 “对象 / 类的结构契约”,支持扩展和合并;type 更灵活,可描述任意类型,适合复杂类型组合。在实际开发中,两者并非互斥,可根据具体场景混合使用(例如用 type 定义联合类型,再用 interface 扩展对象类型)。

4、awaite是宏任务还是微任务

在 JavaScript 中,await 本身并不直接是宏任务或微任务,但它的执行逻辑与微任务队列密切相关。具体来说,await 会暂停当前异步函数的执行,等待右侧表达式(通常是一个 Promise)完成,而等待期间的后续代码会被放入微任务队列

详细执行逻辑:

  1. 当执行到 await promise 时:

    • 首先执行 promise 表达式(立即同步执行,直到返回一个 Promise 对象)。
    • 然后,await 会将当前异步函数中 await 之后的代码(称为 “后续代码”)包装成一个微任务,放入微任务队列。
    • 暂停当前异步函数的执行,跳出函数,继续执行主线程中后续的同步代码。
  2. 当 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');

执行顺序

  1. start(同步代码)
  2. asyncFn start(同步执行 asyncFn 中的同步部分)
  3. end(主线程同步代码继续执行)
  4. asyncFn end(微任务队列清空时执行)

可见,await 后的代码是作为微任务执行的。

结论:

await 本身是语法关键字,不是任务类型,但它会将后续代码注册为微任务。这也是为什么 await 之后的代码总是在主线程同步代码执行完毕、且微任务队列清空时才会执行。 image.png

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)。

三、开发流程规范:从需求到上线的标准化步骤

  1. 分支管理:采用 Git Flow 或简化版流程:

    • main/master:生产环境代码,禁止直接提交。
    • develop:开发分支,集成各功能分支。
    • feature/xxx:功能开发分支(从 develop 创建,完成后合并回 develop)。
    • hotfix/xxx:紧急修复分支(从 main 创建,修复后合并到 main 和 develop)。
  2. 提交规范:使用 Conventional Commits 规范,提交信息格式:

    plaintext

    <类型>[可选作用域]: <描述>
    

    类型包括:feat(新功能)、fix(修复)、docs(文档)、style(格式)、refactor(重构)等。工具:用 commitlint 配合 Husky 校验提交信息,commitizen 提供交互式提交工具。

  3. 代码审查(Code Review)

    • 提交 PR/MR 前必须自我检查(运行 lint、测试用例)。

    • 至少 1 名团队成员审核通过后才能合并,重点检查:

      • 是否符合代码规范。
      • 逻辑是否正确,是否有性能隐患。
      • 是否有冗余代码或重复逻辑。

四、性能与质量规范:避免线上问题

  1. 性能优化规则

    • 加载性能

      • 图片:优先用 WebP 格式,按需加载(lazyload),避免大图(单个图片不超过 200KB)。
      • 资源:JS/CSS 压缩(terser/css-minimizer),Tree-Shaking 移除无用代码,CDN 分发静态资源。
    • 运行性能

      • 避免长任务(单个同步任务不超过 50ms),复杂计算用 Web Worker。
      • 减少 DOM 操作,频繁操作用 documentFragment 或虚拟 DOM。
      • 合理使用缓存(localStorage 存储非敏感数据,避免存储过大值)。
  2. 可访问性(A11y)

    • 按钮、链接等可交互元素必须支持键盘访问(tab 聚焦)。
    • 图片添加 alt 属性,表单元素关联 label
    • 颜色对比度符合 WCAG 标准(文本与背景对比度至少 4.5:1)。
  3. 兼容性

    • 明确支持的浏览器版本(如 Chrome ≥ 80,iOS ≥ 14),用 browserslist 配置。
    • 新语法需通过 Babel 转译(如 ES6+ 转 ES5),CSS 新特性需加前缀(autoprefixer)。

五、文档规范:降低协作成本

  1. 项目文档

    • 根目录必备 README.md:包含项目介绍、启动命令、目录说明、环境配置。
    • 接口文档:用 Swagger、Apifox 或 Markdown 维护,包含请求参数、响应格式、错误码。
  2. 代码注释

    • 公共组件 / 函数:必须写 JSDoc 注释(描述功能、参数、返回值):

      typescript

      /**
       * 格式化日期
       * @param {Date} date - 日期对象
       * @param {string} format - 格式字符串(如 'YYYY-MM-DD')
       * @returns {string} 格式化后的日期
       */
      function formatDate(date: Date, format: string): string { ... }
      
    • 复杂逻辑:在代码块前添加注释说明设计思路(避免 “解释代码”,而是 “解释为什么这么做”)。

六、规范落地与迭代

  1. 新人培训:将规范文档作为新人入职资料,确保全员理解。
  2. 定期复盘:每季度回顾规范执行情况,根据团队反馈调整(如新增常用组件规范、修改不合理的目录结构)。
  3. 工具链自动化:尽可能用工具替代人工检查(如 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 会引入 BannerProductListFooter 等组件,并处理页面级逻辑(如请求首页数据、控制弹窗显示)。

三、常见误区:避免过度拆分或过度耦合

1. 误区 1:过度拆分(颗粒度太细)
  • 问题:将组件拆解得过细,导致组件数量激增,且组件间依赖复杂(如一个页面需要引入 10 + 小组件),反而增加维护成本。
  • 反例:把 UserCard 拆分为 UserAvatar(头像)、UserName(姓名)、UserLevel(等级)3 个独立组件,而这 3 个组件从未在其他地方复用 —— 完全可以合并为 UserCard 的内部元素,通过 props 控制显示。
  • 判断方法:若一个组件的 “复用次数为 1”,且只被父组件唯一引用,可考虑与父组件合并(除非父组件本身已过大)。
2. 误区 2:过度耦合(颗粒度太粗)
  • 问题:将多个不相关的功能塞进一个组件,导致组件 “臃肿”,无法复用且修改风险高。
  • 反例:一个 Dashboard 组件内,既包含 “数据统计图表”,又包含 “用户列表”,还包含 “系统通知弹窗”—— 这 3 个模块无直接关联,应拆分为 StatChartUserListNoticeModal 3 个组件,由 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 的工作机制可以拆解为以下几点:

  1. 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
    
  2. await 暂停执行,等待 Promise 完成await 关键字只能在 async 函数内部使用,它的作用是:

    • 接收一个 Promise 对象(如果不是 Promise,则会先自动包装为 Promise.resolve(值))。
    • 暂停当前 async 函数的执行,将执行权交还给外部环境(避免阻塞主线程)。
    • 当 Promise 状态变为 resolved 时,恢复 async 函数的执行,并将 Promise 的结果作为 await 表达式的返回值。
    • 如果 Promise 状态变为 rejected,则会抛出异常,需要用 try/catch 捕获。
  3. 基于 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 的状态变化,自动推进代码执行,直到异步操作全部完成。

  4. 事件循环中的执行顺序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');

三、核心原则与最佳实践

  1. 优先非阻塞:对非首屏必要资源,优先使用 async/defer 或动态加载,避免阻塞 HTML 解析。
  2. 按需加载:结合用户行为(如点击、滚动)触发加载,减少初始加载体积(例如:点击 “查看更多” 再加载详情脚本)。
  3. 错误处理:必须监听 onerror 事件,避免加载失败导致功能异常。
  4. 顺序控制:有依赖关系的脚本用 defer(按标签顺序)或链式回调(动态加载)保证执行顺序。
  5. 利用浏览器缓存:通过合理的缓存策略(如 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; // 交叉类型:同时有 ab 属性
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 广泛支持,是日常开发的基础。合理使用能显著简化代码,提升可读性和效率。