阅读视图

发现新文章,点击刷新页面。

重排、重绘、合成:浏览器渲染的“三兄弟”,你惹不起也躲不过

你给一个元素悄悄改了宽度,结果整个页面都抖了一下?你加了个动画,电脑风扇开始狂转?今天我们来认识浏览器渲染里的“三兄弟”——重排、重绘、合成。弄懂它们,你就能写出流畅60帧的页面,告别卡顿。

前言

想象一下,你家客厅要重新装修。你只是换了个抱枕(重绘),很轻松。但如果你要把墙拆了(重排),那得搬家具、砸墙、重新粉刷,累得半死。如果只是把电视画面换个图层(合成),连工人都不要,遥控器一按就行。

浏览器的渲染也是这个道理。理解这三种操作的成本,就能写出性能飞起的页面。

一、先复习:渲染流水线

之前我们讲过,浏览器把HTML/CSS变成屏幕上的像素,要经过:DOM树 + CSSOM树 → 渲染树 → 布局(计算位置大小)→ 绘制(填充像素)→ 合成(合并图层)。

其中:

  • 重排(Reflow):重新计算布局(位置、大小)。成本最高。
  • 重绘(Repaint):重新绘制像素(颜色、背景、阴影等)。成本中等。
  • 合成(Composite):重新合并图层。成本极低(走GPU)。

二、重排:动到筋骨,全员遭殃

什么操作会触发重排?

  • 改变元素的几何属性widthheightmarginpaddingbordertopleft……
  • 改变DOM结构:增删元素、改变内容(文字变了导致高度变化)。
  • 读取某些布局属性offsetTopscrollTopclientWidthgetComputedStyle()。因为浏览器需要返回最新值,不得不强制重排。
  • 改变窗口大小(resize事件)。
  • 激活伪类(如:hover导致样式变化影响布局)。

重排的代价:浏览器要重新计算整个或部分渲染树,然后重新布局、绘制、合成。就像你拆了一面墙,整个房子都得重新量尺寸。

三、重绘:只换皮肤,不动骨架

什么操作会触发重绘但不触发重排?

  • 改变颜色colorbackground-colorborder-colorbox-shadow等。
  • 改变可见性visibility(但display: none会触发重排)。
  • 改变背景图outline等。

重绘的代价:不需要重新布局,但还是要重新绘制像素,比重排轻,但也不是免费。

四、合成:GPU加速的“超车道”

合成是成本最低的环节,因为它不涉及布局和绘制,只把已有的图层合并。能触发合成的属性有:

  • transform(平移、旋转、缩放)
  • opacity
  • filter

当你用transform: translateZ(0)will-change: transform时,浏览器会把这个元素提升到单独的合成层,后续动画只由GPU处理,完全不触发重排和重绘。这就是为什么动画推荐用transform而不是left

/* 差:触发重排 */
.box {
  transition: left 0.3s;
  left: 0;
}
.box:hover {
  left: 100px;
}

/* 好:只触发合成 */
.box {
  transition: transform 0.3s;
  transform: translateX(0);
}
.box:hover {
  transform: translateX(100px);
}

五、如何减少重排和重绘?

1. 批量修改样式

不要挨个改属性,用class一次改完:

// 差
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 好
element.classList.add('new-size');

2. 让元素脱离文档流再操作

比如要插入多个li,可以先隐藏(display: none),改完再显示,只触发两次重排。

const ul = document.getElementById('list');
ul.style.display = 'none';
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  ul.appendChild(li);
}
ul.style.display = 'block';

3. 使用文档片段(DocumentFragment)

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li);
}
ul.appendChild(fragment); // 只触发一次重排

4. 读写分离

不要交替读取和修改布局属性,否则会触发多次重排。

// 差
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = boxes[i].offsetWidth + 'px'; // 读后立即写
}

// 好:先读后写
const widths = [];
for (let i = 0; i < boxes.length; i++) {
  widths.push(boxes[i].offsetWidth);
}
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = widths[i] + 'px';
}

5. 使用transformopacity做动画

永远不要用lefttopwidthmargin做动画,改用transform

6. 固定元素位置

position: fixedabsolute的元素,其重排影响范围较小(只在自己层内)。

7. 避免使用table布局

一个小改动可能触发整个table的重排。

六、实战:一个性能优化的例子

假设你要做一个跟随鼠标移动的小光点(类似鼠标特效)。错误做法:每帧改变top/left,触发重排。正确做法:用transform

// 差:每移动1px就重排一次
dot.style.left = x + 'px';
dot.style.top = y + 'px';

// 好:只触发合成
dot.style.transform = `translate(${x}px, ${y}px)`;

七、怎么分析页面重排/重绘?

Chrome DevTools → Performance 面板,录制一段操作,查看“Layout Shift”、“Paint”等标记。红色紫色区域越少越好。

八、总结:三兄弟的“饭量”

  • 重排:吃满汉全席,最贵。动几何、DOM结构。
  • 重绘:吃快餐,中等。动颜色、背景。
  • 合成:喝矿泉水,几乎免费。动transform、opacity。

优化口诀:能用transform别用left,能用class别改style,读写分离,批量操作。

如果你觉得今天的“三兄弟”够形象,点个赞让更多人看到。明天我们将聊聊JavaScript引擎与内存管理,看看V8是怎么给代码“打扫卫生”的。我们明天见!

装饰器:那个在代码里“贴标签”的黑魔法,到底有什么用?

你有没有在Angular或NestJS里见过@Component@Injectable这种稀奇古怪的“@符号”?它们就像给代码贴的“便利贴”,背后却能自动帮你做一堆事情。今天我们就来揭开TypeScript装饰器的神秘面纱,看看这个“贴标签”魔法到底怎么用,以及为什么它能让你少写几千行重复代码。

前言

想象你去餐厅吃饭,你在菜单上贴了个标签“@少油”,厨房看到后自动给你少放油。你又贴个“@加辣”,厨房又自动加辣。你只需要贴标签,厨房负责执行。

这就是装饰器。它是一种特殊的声明,可以附加在类、方法、属性、参数上,用来修改或增强它们的行为。你不用手动调用什么函数,只要贴上“标签”,背后的逻辑就会自动生效。

一、装饰器长啥样?先看个例子

在TypeScript里,装饰器以@expression的形式出现,expression是一个函数,会在运行时被调用。

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('调用了方法:', propertyKey);
}

class Person {
  @log
  sayHello() {
    console.log('Hello');
  }
}

const p = new Person();
p.sayHello();
// 输出:
// 调用了方法: sayHello
// Hello

你什么都没改,只是加了个@log,每次调用sayHello就会自动打印日志。这就是装饰器的魅力。

二、启用装饰器:别急,先开个开关

TypeScript的装饰器目前是实验性特性,需要在tsconfig.json里开启:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // 可选,用于反射元数据
  }
}

三、装饰器的四种类型

装饰器可以贴在四个地方:类、方法、访问器/属性、参数。每种都有不同的参数签名。

1. 类装饰器

作用在类上,通常用来修改或替换类的定义。

function addTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    timestamp = new Date();
  };
}

@addTimestamp
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const user = new User('张三');
console.log(user); // User { name: '张三', timestamp: 2025-04-10... }

类装饰器接收一个参数:类的构造函数。你可以返回一个新类替换它,或者直接修改原型。

2. 方法装饰器

最常用,可以拦截、修改、替换方法。

function measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} 耗时 ${end - start}ms`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @measure
  add(a: number, b: number) {
    return a + b;
  }
}

参数:

  • target:类的原型(静态方法则是构造函数)
  • propertyKey:方法名
  • descriptor:属性描述符(可以修改value、writable等)

3. 属性装饰器

作用在属性上,通常用于配合元数据做依赖注入或验证。

function format(formatStr: string) {
  return function(target: any, propertyKey: string) {
    let value: string;
    const getter = function() {
      return value;
    };
    const setter = function(newVal: string) {
      value = formatStr.replace('%s', newVal);
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Greeting {
  @format('Hello, %s')
  name: string;
}

属性装饰器只能拿到目标类和属性名,不能直接修改属性值,但可以通过Object.defineProperty替换getter/setter。

4. 参数装饰器

作用在函数参数上,常用于依赖注入框架(比如Angular)。

function paramLogger(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`参数位置 ${parameterIndex} 被装饰了`);
}

class UserService {
  getUser(@paramLogger id: number) {
    return { id };
  }
}

参数装饰器很少单独用,通常配合类装饰器或方法装饰器收集元数据。

四、装饰器工厂:给装饰器传参数

你看到@log@measure这些是不带参数的。如果想让装饰器接受配置,需要再包一层函数:

function log(prefix: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log(`${prefix} 调用 ${propertyKey}`);
      return original.apply(this, args);
    };
  };
}

class Test {
  @log('DEBUG')
  doSomething() {
    console.log('执行');
  }
}

这就是装饰器工厂:外层函数接收参数,内层函数是真正的装饰器。

五、多个装饰器:从下往上,从右往左

当你在同一个目标上使用多个装饰器时,它们的执行顺序是:先执行靠近目标的(从下往上),再执行外层的

@classDecoratorA
@classDecoratorB
class MyClass {}

执行顺序:classDecoratorB 先执行,然后 classDecoratorA

方法上的装饰器类似:先执行参数装饰器,再执行方法装饰器,最后是类装饰器(但方法装饰器本身的调用顺序是从下往上)。

六、实战:用装饰器实现权限校验

假设你要写一个类,某些方法只有管理员能调用。你可以用装饰器优雅地实现:

function adminOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    if (!this.isAdmin) {
      throw new Error('无权限,需要管理员角色');
    }
    return original.apply(this, args);
  };
}

class UserController {
  isAdmin = false;

  @adminOnly
  deleteUser(id: number) {
    console.log(`删除用户 ${id}`);
  }
}

const ctrl = new UserController();
ctrl.deleteUser(1); // 报错:无权限
ctrl.isAdmin = true;
ctrl.deleteUser(1); // 成功

看,你只需要在需要权限的方法上贴个@adminOnly,逻辑自动注入。

七、装饰器的实际应用场景

  • 日志记录:自动打印方法入参、返回值、耗时。
  • 权限校验:检查当前用户角色。
  • 数据验证:验证方法参数格式。
  • 依赖注入:Angular、NestJS 里大量使用。
  • 性能监控:自动记录方法执行时间。
  • 重试机制:方法失败后自动重试。

八、注意事项与坑点

  1. 装饰器目前是实验特性,虽然Angular、NestJS等框架广泛使用,但未来ECMAScript标准可能会有所变化。
  2. 不能用在普通JS文件,必须在TS或Babel中启用。
  3. 属性装饰器不能直接修改属性值,需要通过Object.defineProperty替换getter/setter。
  4. 装饰器在类定义时执行,而不是实例化时。这意味着你不能依赖实例属性(比如this.isAdmin)来做静态分析,但可以在返回的函数中延迟读取。

九、总结:装饰器就像“代码贴纸”

  • 装饰器是给类、方法、属性、参数贴的“标签”。
  • 标签背后的函数会在运行时自动执行,修改目标的行为。
  • 装饰器工厂可以传参,实现定制化。
  • 多个装饰器从下往上执行。
  • 常见用途:日志、权限、验证、注入。

学会装饰器,你就能写出更声明式、更优雅的代码。很多框架的魔法背后,其实就是这些小小的“@”符号。

如果你觉得今天的“便利贴”魔法够神奇,点个赞让更多人看到。明天我们将开启浏览器渲染原理之旅,从输入URL到页面显示,中间到底发生了什么?我们明天见!

泛型:像“填空”一样写类型,让你的代码从“复制粘贴”中解放

你是不是遇到过这种场景:写了一个函数,处理数字的版本写一遍,处理字符串的版本再写一遍,处理数组的又写一遍……最后代码里全是长得差不多的“双胞胎”。今天我们来学TypeScript的泛型——一个能让你写一次、处处用的“类型模板”。从此告别复制粘贴,做个体面的程序员。

前言

想象一下,你开了一家“万能快递公司”。客户要寄书,你准备书盒;要寄衣服,你准备衣服盒;要寄手机,你准备手机盒……每种物品都要单独设计盒子,累不累?

更好的做法:设计一种可调节大小的盒子,客户说寄什么,你就把盒子调成对应大小。这个“可调节的盒子”,就是泛型

TypeScript的泛型让你在定义函数、类、接口时,先“留个空”,等用的时候再往里填具体类型。这样既保证了类型安全,又避免了重复代码。

一、泛型长啥样?一个简单的例子

先看一个没有泛型的“悲惨世界”:

// 只能处理数字
function identityNumber(arg: number): number {
  return arg;
}
// 只能处理字符串
function identityString(arg: string): string {
  return arg;
}
// 要处理布尔值?再写一个……

用泛型,只需要一个:

function identity<T>(arg: T): T {
  return arg;
}

这里的<T>就像个“占位符”,你调用时可以指定具体类型:

let output1 = identity<string>('hello'); // 类型是 string
let output2 = identity<number>(123);     // 类型是 number

但TS很聪明,能自动推断,所以通常可以省略:

let output = identity('hello'); // TS推断出T为string

二、泛型不只是“传进去又返回来”

你可以约束参数的类型关系。比如,你想让函数接收一个数组,并返回数组的第一个元素:

function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const firstNumber = getFirst([1, 2, 3]);   // 类型 number
const firstString = getFirst(['a', 'b']); // 类型 string

T帮我们保持了“数组元素类型”和“返回值类型”的一致性。

三、泛型约束:给“占位符”画个圈

有时候,你不能让T为所欲为。比如你想写一个函数,打印参数的length属性:

function logLength<T>(arg: T): T {
  console.log(arg.length); // ❌ 报错:T可能没有length
  return arg;
}

因为T可能是numberboolean,它们没有.length。这时候需要约束——告诉TS:T必须是有length属性的类型。

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length); // ✅ 安全
  return arg;
}

logLength('hello');    // 字符串有length
logLength([1, 2, 3]);  // 数组有length
logLength(123);        // ❌ 数字没有length,报错

extends关键字在这里不是继承,而是“约束为某个类型的子集”。

四、泛型接口:把接口变成“模具”

接口也可以泛型化,比如定义一个通用的响应结构:

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 使用
type User = { name: string; age: number };
const response: ApiResponse<User> = {
  code: 200,
  message: 'success',
  data: { name: '张三', age: 18 }
};

这样,你就能用一个接口描述所有API返回格式,只需替换T

五、泛型类:像造模具一样造类

类同样可以泛型:

class Queue<T> {
  private data: T[] = [];
  push(item: T) {
    this.data.push(item);
  }
  pop(): T | undefined {
    return this.data.shift();
  }
}

const numberQueue = new Queue<number>();
numberQueue.push(123);
numberQueue.push('456'); // ❌ 报错,只能放数字

六、泛型工具类型:TS内置的“变形金刚”

TS提供了一些内置的泛型工具,能帮你快速转换类型。

1. Partial<T>:把属性都变成可选

interface User {
  name: string;
  age: number;
}
type PartialUser = Partial<User>; // { name?: string; age?: number; }

2. Readonly<T>:把所有属性变成只读

type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age: number; }

3. Pick<T, K>:从T中挑选部分属性

type UserName = Pick<User, 'name'>; // { name: string; }

4. Omit<T, K>:从T中排除部分属性

type UserWithoutAge = Omit<User, 'age'>; // { name: string; }

还有Record<K, T>ExcludeExtract等,遇到具体场景时再查文档即可。

七、联合类型与交叉类型:不是泛型,但常一起用

联合类型(|):这个或那个

let value: string | number;
value = 'hello'; // OK
value = 123;     // OK
value = true;    // ❌

联合类型适合“不确定具体是哪个,但知道是有限的几种”。

交叉类型(&):既要又要

interface Name { name: string; }
interface Age { age: number; }
type Person = Name & Age; // 同时有name和age属性

const p: Person = { name: '张三', age: 18 };

交叉类型常用来合并多个类型。

八、类型保护:让TS相信你

当你使用联合类型时,TS会限制你只能调用所有类型共有的方法。要调用特定类型的方法,需要类型保护

function printLength(value: string | number) {
  // console.log(value.length); // ❌ 报错,number没有length
  if (typeof value === 'string') {
    console.log(value.length); // ✅ 这里TS知道value是string
  } else {
    console.log(value.toFixed(2));
  }
}

除了typeof,还有instanceofin关键字、自定义类型守卫。

九、实战:用泛型写一个“万能”的缓存函数

interface Cache<T> {
  get(key: string): T | undefined;
  set(key: string, value: T): void;
}

function createCache<T>(): Cache<T> {
  const store: Record<string, T> = {};
  return {
    get(key) { return store[key]; },
    set(key, value) { store[key] = value; }
  };
}

const stringCache = createCache<string>();
stringCache.set('name', '张三');
const name = stringCache.get('name'); // 类型是 string | undefined

const numberCache = createCache<number>();
numberCache.set('age', 18);

看,一套代码同时服务了字符串缓存和数字缓存,类型还完全安全。

十、总结:泛型是“类型编程”的起点

  • 泛型就是“类型的参数”,让组件(函数、类、接口)能适应多种类型,同时保留类型关系。
  • 约束extends限定泛型的范围。
  • 泛型接口/类让数据结构通用。
  • 内置工具类型(Partial、Pick等)简化常见类型转换。
  • 联合类型表示“或”,交叉类型表示“且”,类型保护用来区分联合中的具体类型。

掌握泛型,你就能写出更抽象、更复用、更安全的代码。明天我们将继续TypeScript的高级主题——装饰器,看看这个类似Java注解的特性,如何在TS里玩出花样。

如果你觉得今天的“万能模具”讲得通透,点个赞让更多人看到。明天我们聊聊装饰器——那个在Angular和NestJS里无处不在的黑魔法。我们明天见!

你的JS代码总在半夜崩溃?TypeScript来“上保险”了

你有没有经历过:凌晨三点,线上报“Cannot read property 'name' of undefined”,你爬起来一看,原来是后端返回的数据少了一层。如果JS有“类型检查”,这种悲剧根本不会发生。今天我们就来认识TypeScript——给JavaScript买了一份“意外险”。

前言

JavaScript就像个自由散漫的天才:你给它一个字符串,它当数字用;你忘记传参数,它给你个undefined;你访问对象不存在的属性,它笑眯眯地说“没事,我给你undefined”。这种灵活在小型项目里很爽,但项目一大,就成了噩梦。

TypeScript(简称TS)就是来解决这个问题的。它给JS加上了类型系统,在代码运行之前就帮你检查类型错误。就像给代码装了安检门,不规范的写法根本过不去。

一、TypeScript是啥?JS的“严格模式”Pro Max

TypeScript是微软开发的开源语言,它是JavaScript的超集。意思是:所有合法的JS代码,在TS里也合法。TS只是给JS加了类型注解和一些新特性,然后编译成干净的JS。

// JS写法
function greet(name) {
  return 'Hello, ' + name;
}

// TS写法(加了类型)
function greet(name: string): string {
  return 'Hello, ' + name;
}

greet(123); // ❌ 报错:参数不能是数字

你看,TS在编译阶段就抓住了错误,不用等到运行时。

二、为什么要用TS?三个字:稳、爽、香

  • :类型错误在写代码时就暴露,而不是在用户手里炸。
  • :编辑器智能提示飞起,不用记方法名、参数顺序。
  • :代码即文档,看函数签名就知道怎么用。

据统计,使用TS的项目,早期Bug能减少15%~25%。对于中大型项目,TS几乎是标配。

三、基础类型:TS的“基本词汇”

TS支持JS的所有类型,还加了一些新的。

1. 原始类型

let name: string = '张三';
let age: number = 18;
let isStudent: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let big: bigint = 100n;
let sym: symbol = Symbol('id');

2. 数组

let list1: number[] = [1, 2, 3];
let list2: Array<string> = ['a', 'b'];  // 泛型写法

3. 元组(固定长度和类型的数组)

let person: [string, number] = ['张三', 18];
person[0] = '李四';  // OK
person[1] = '20';   // ❌ 报错,第二个元素必须是数字

4. 枚举(给一组数字起名字)

enum Color { Red, Green, Blue }
let c: Color = Color.Red;
console.log(c); // 0(默认从0开始)

// 自定义值
enum Status { Success = 200, NotFound = 404 }

5. Any(万能类型,慎用)

let notSure: any = 4;
notSure = '字符串';  // OK
notSure = true;      // OK

any会关闭类型检查,相当于回到JS。尽量少用,除非你确定这个值无法预知类型。

6. Unknown(安全的Any)

let value: unknown = 'hello';
value = 123;  // OK
// console.log(value.toUpperCase()); // ❌ 报错,unknown不能直接调用方法
if (typeof value === 'string') {
  console.log(value.toUpperCase()); // 类型收窄后可用
}

unknownany安全,因为使用前必须先判断类型。

7. Void(没有返回值)

function warnUser(): void {
  console.log('警告');
}
// 变量声明为void类型只能赋值为null或undefined(strict模式下只能undefined)

8. Never(永远不会发生的类型)

function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

四、类型注解:给变量贴标签

TS的核心就是类型注解:在变量、函数参数、返回值后面加上: 类型

let myName: string = '张三';
function add(a: number, b: number): number {
  return a + b;
}

但TS很智能,很多时候可以类型推断,不用显式写:

let age = 18; // TS自动推断为number
age = '18';   // ❌ 报错

五、接口(Interface):定义对象的形状

接口是TS里最常用的功能,用来描述对象的结构。

interface Person {
  name: string;
  age: number;
  email?: string;  // 可选属性
  readonly id: number; // 只读属性
}

const zhangsan: Person = {
  name: '张三',
  age: 18,
  id: 1
};
zhangsan.id = 2; // ❌ 报错,只读属性不能改

接口还可以描述函数类型:

interface AddFunc {
  (a: number, b: number): number;
}
const add: AddFunc = (x, y) => x + y;

六、类型别名(Type):给类型起外号

类型别名和接口很像,但能表示联合类型、元组等更复杂的类型。

type ID = string | number;  // 联合类型
type Point = [number, number]; // 元组
type Callback = (data: string) => void;

let userId: ID = 123;
userId = 'abc';

接口 vs 类型别名

  • 接口可以扩展(extends),类型别名用交叉(&)。
  • 接口可以重复定义自动合并,类型别名不能重复。
  • 推荐优先用接口描述对象,用类型别名描述联合、元组等。

七、实战:用TS写一个简单的函数

// 需求:格式化用户信息
interface User {
  name: string;
  age: number;
  address?: string;
}

function formatUser(user: User, withAddress: boolean = false): string {
  let base = `${user.name}, ${user.age}岁`;
  if (withAddress && user.address) {
    base += `, 地址:${user.address}`;
  }
  return base;
}

const u: User = { name: '李四', age: 20, address: '北京' };
console.log(formatUser(u, true)); // "李四, 20岁, 地址:北京"

如果你在编辑器里打formatUser(,它会提示参数类型和返回值类型,爽不爽?

八、常见坑点与建议

  1. 不要滥用any:any越多,TS的价值越低。实在不知道类型,先写unknown
  2. 严格模式:开启strict: true(tsconfig.json),让TS更严格地检查。
  3. 第三方库:大多数库都有@types/xxx类型定义,安装后就能获得智能提示。
  4. 编译后的JS:TS只负责编译时检查,运行时还是JS,类型信息会被擦除。

九、总结:TS不是敌人,是保镖

  • 给JS加上类型,提前发现错误。
  • 基础类型、接口、类型别名是核心工具。
  • 用好类型推断,少写冗余注解。
  • 逐步迁移老项目,从.js改成.ts,开启allowJs: true

学TS并不难,你只需要把“写JS时的心理预期”明确写出来。明天我们继续深入TypeScript,聊聊高级类型——泛型、联合类型、交叉类型、类型保护,让你写出更灵活更安全的代码。

如果你觉得今天的“保险课”够实在,点个赞让更多人看到。我们明天见!

❌