从JS到TS:类、构造函数与单例模式,一篇吃透

在这里插入图片描述

作为一名前端开发者,从熟悉的JavaScript切换到TypeScript项目时,遇到类、构造函数、单例这些概念经常会感到困惑这太正常了。

其实这些概念本质上是JavaScript已有特性的“强类型升级版”,只是TypeScript给它们加了更严谨的约束。这篇文章我会用最通俗的语言,结合大量JS/TS对比示例,帮你彻底搞懂这些核心概念。

一、先搞懂:TS中的类(Class)到底是什么?

1. 类的本质:JS的语法糖,TS的“强类型模板”

在ES6之前,JS没有真正的“类”,我们靠构造函数模拟面向对象;

ES6引入class关键字,本质是构造函数的语法糖;

而TS的class则是在ES6类的基础上,增加了类型约束,让代码更严谨。

简单说:类是创建对象的“模板”,就像做饼干的模具,用同一个模具能做出规格一致的饼干(对象),TS的类还会强制要求饼干的“配料”(属性/方法)必须符合预设类型。

2. TS类的基础语法(对比JS)

先看一个最基础的例子,帮你快速对比差异:

JS中的类
// JS类:无类型约束,属性可以随便加
class Person {
  // 构造函数:创建对象时初始化属性
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 实例方法
  sayHi() {
    console.log(`我是${this.name},今年${this.age}岁`);
  }
}

// 创建实例
const zhangsan = new Person('张三', 20);
zhangsan.sayHi(); // 我是张三,今年20岁
zhangsan.gender = '男'; // 随便加属性,JS不限制
TS中的类
// TS类:必须先声明属性类型,构造函数参数也要约束
class Person {
  // 第一步:声明实例属性 + 类型(TS特有)
  name: string;
  age: number;
  // 可选属性:加?表示可以不存在
  gender?: string;

  // 第二步:构造函数(参数也要加类型)
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 实例方法(返回值也可以加类型)
  sayHi(): void {
    console.log(`我是${this.name},今年${this.age}岁`);
  }

  // 带返回值的方法
  getAge(): number {
    return this.age;
  }
}

// 创建实例:参数类型不对会直接报错(TS的核心价值)
const zhangsan = new Person('张三', 20); // 正确
// const lisi = new Person(20, '李四'); // 报错:类型不匹配
zhangsan.sayHi(); // 我是张三,今年20岁
// zhangsan.gender = '男'; // 可选属性可以赋值
// zhangsan.height = 180; // 报错:未声明的属性不能加

3. TS类的核心特性(比JS多的约束)

(1)访问修饰符(TS特有)

用来控制类的属性/方法能被谁访问,这是TS类最常用的特性,也是你看项目代码时最常见的:

修饰符含义示例
public(默认)公开的,类内、子类、外部都能访问public name: string;
private私有的,只有当前类内部能访问private age: number;
protected受保护的,类内、子类能访问,外部不能protected gender: string;

示例:

class Person {
  public name: string; // 公开
  private age: number; // 私有
  protected gender: string; // 受保护

  constructor(name: string, age: number, gender: string) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

  private getAge(): number { // 私有方法
    return this.age;
  }

  public showInfo(): void {
    // 类内部可以访问所有属性/方法
    console.log(`姓名:${this.name},年龄:${this.getAge()},性别:${this.gender}`);
  }
}

const zhangsan = new Person('张三', 20, '男');
console.log(zhangsan.name); // 正常:public可以访问
// console.log(zhangsan.age); // 报错:private不能外部访问
// console.log(zhangsan.gender); // 报错:protected不能外部访问
zhangsan.showInfo(); // 正常:通过public方法间接访问私有/受保护属性

// 子类示例
class Student extends Person {
  constructor(name: string, age: number, gender: string) {
    super(name, age, gender);
  }

  showGender(): void {
    console.log(this.gender); // 正常:子类能访问protected
    // console.log(this.age); // 报错:子类不能访问private
  }
}
(2)只读属性(readonly)

TS特有,用readonly修饰的属性只能在构造函数中赋值,后续不能修改:

class Person {
  readonly id: number; // 只读属性
  name: string;

  constructor(id: number, name: string) {
    this.id = id; // 构造函数中可以赋值
    this.name = name;
  }
}

const zhangsan = new Person(1, '张三');
zhangsan.name = '张三三'; // 正常:普通属性可修改
// zhangsan.id = 2; // 报错:readonly属性不能修改

二、构造函数(constructor):类的“初始化函数”

1. 构造函数的核心作用

构造函数是类中特殊的方法,只有两个核心功能:

  • 当用new创建类的实例时,自动执行;
  • 给实例的属性初始化赋值。

你可以把构造函数理解为:“用模具做饼干时,往模具里放配料的步骤”,每次做饼干(创建实例)都会执行这一步,保证饼干有基础配料。

2. TS构造函数的简化写法(项目中常用)

TS支持在构造函数参数上直接加修饰符/类型,省略“声明属性+赋值”的步骤,这是项目代码中最常见的写法,一定要掌握:

简化前
class Person {
  private name: string;
  public age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
简化后(项目中常用)
class Person {
  // 直接在参数上加修饰符,自动声明为实例属性并赋值
  constructor(
    private name: string,
    public age: number
  ) {}
}

const zhangsan = new Person('张三', 20);
console.log(zhangsan.age); // 正常:public
// console.log(zhangsan.name); // 报错:private

3. 构造函数的继承(super关键字)

当子类继承父类时,子类的构造函数必须先调用super()(父类的构造函数),否则TS会报错:

class Person {
  constructor(public name: string) {}
}

class Student extends Person {
  constructor(name: string, public score: number) {
    super(name); // 必须先调用父类构造函数,传递参数
    this.score = score;
  }
}

const student = new Student('李四', 90);
console.log(student.name, student.score); // 李四 90

三、单例模式:让类“只能创建一个实例”

1. 单例模式的核心场景

为什么项目中会用单例?比如:

  • 全局的弹窗管理器:整个项目只能有一个,避免重复创建;
  • 全局的状态管理器:保证数据唯一;
  • 工具类:比如日期工具、请求工具,只需要一个实例即可。

简单说:单例模式就是强制一个类只能创建出一个实例对象,后续无论怎么调用,都返回同一个实例。

2. TS实现单例模式的核心思路

实现单例的关键有3步:

  1. 把构造函数设为private:外部不能用new创建实例;
  2. 在类内部创建一个静态的“实例缓存”;
  3. 提供一个静态方法(比如getInstance()):检查缓存,有则返回,无则创建。

3. 基础版单例(项目中最常用)

class ModalManager {
  // 1. 静态属性:缓存唯一实例(static表示属于类,而非实例)
  private static instance: ModalManager;

  // 2. 私有构造函数:外部不能new
  private constructor() {
    console.log('创建弹窗管理器实例');
  }

  // 3. 静态方法:获取唯一实例
  public static getInstance(): ModalManager {
    // 检查缓存:没有则创建,有则直接返回
    if (!ModalManager.instance) {
      ModalManager.instance = new ModalManager();
    }
    return ModalManager.instance;
  }

  // 实例方法:比如打开弹窗
  openModal(content: string): void {
    console.log(`打开弹窗:${content}`);
  }
}

// 测试:无论调用多少次,只创建一次实例
const modal1 = ModalManager.getInstance();
const modal2 = ModalManager.getInstance();

console.log(modal1 === modal2); // true:是同一个实例
modal1.openModal('提示信息'); // 打开弹窗:提示信息
modal2.openModal('错误信息'); // 打开弹窗:错误信息

// const modal3 = new ModalManager(); // 报错:构造函数是private,不能外部调用

4. 带初始化参数的单例(进阶版)

如果单例需要初始化参数(比如请求工具需要baseURL),可以这样写:

class RequestUtil {
  private static instance: RequestUtil;
  private baseURL: string;

  // 私有构造函数,接收初始化参数
  private constructor(baseURL: string) {
    this.baseURL = baseURL;
  }

  // 静态方法,支持传参初始化
  public static getInstance(baseURL: string = 'https://api.default.com'): RequestUtil {
    if (!RequestUtil.instance) {
      RequestUtil.instance = new RequestUtil(baseURL);
    }
    return RequestUtil.instance;
  }

  // 实例方法:发送请求
  get(path: string): string {
    return `${this.baseURL}${path}`;
  }
}

// 第一次调用传参,初始化baseURL
const request1 = RequestUtil.getInstance('https://api.xxx.com');
console.log(request1.get('/user')); // https://api.xxx.com/user

// 后续调用不传参,复用已有实例
const request2 = RequestUtil.getInstance();
console.log(request2.get('/order')); // https://api.xxx.com/order
console.log(request1 === request2); // true

四、实战:TS类+单例的完整示例(项目级)

结合上面的知识点,写一个项目中常见的“本地存储工具类(单例)”,帮你串联所有知识点:

/**
 * 本地存储工具类(单例模式)
 * 功能:封装localStorage,增加类型约束和异常处理
 */
class StorageUtil {
  // 单例缓存
  private static instance: StorageUtil;
  // 前缀:区分不同项目的存储key
  private prefix: string;

  // 私有构造函数:初始化前缀
  private constructor(prefix: string = 'app_') {
    this.prefix = prefix;
    // 检查localStorage是否可用(比如浏览器隐私模式)
    this.checkStorageAvailable();
  }

  // 获取单例实例
  public static getInstance(prefix?: string): StorageUtil {
    if (!StorageUtil.instance) {
      StorageUtil.instance = new StorageUtil(prefix);
    }
    return StorageUtil.instance;
  }

  // 私有方法:检查localStorage可用性
  private checkStorageAvailable(): void {
    try {
      const testKey = `${this.prefix}test`;
      localStorage.setItem(testKey, 'test');
      localStorage.removeItem(testKey);
    } catch (e) {
      throw new Error('本地存储不可用,请关闭浏览器隐私模式!');
    }
  }

  // 公开方法:设置存储(支持任意类型,自动JSON.stringify)
  public set<T>(key: string, value: T): void {
    const fullKey = `${this.prefix}${key}`;
    const strValue = JSON.stringify(value);
    localStorage.setItem(fullKey, strValue);
  }

  // 公开方法:获取存储(支持泛型,自动JSON.parse)
  public get<T>(key: string): T | null {
    const fullKey = `${this.prefix}${key}`;
    const strValue = localStorage.getItem(fullKey);
    if (!strValue) return null;
    try {
      return JSON.parse(strValue) as T;
    } catch (e) {
      console.error('解析存储数据失败', e);
      return null;
    }
  }

  // 公开方法:删除存储
  public remove(key: string): void {
    const fullKey = `${this.prefix}${key}`;
    localStorage.removeItem(fullKey);
  }

  // 公开方法:清空当前项目的所有存储
  public clear(): void {
    const keys = Object.keys(localStorage);
    keys.forEach(key => {
      if (key.startsWith(this.prefix)) {
        localStorage.removeItem(key);
      }
    });
  }
}

// 使用示例
const storage = StorageUtil.getInstance('my_project_');
// 存储数据(TS会校验类型)
storage.set<string>('username', '张三');
storage.set<number>('age', 20);
storage.set<{ id: number; name: string }>('user', { id: 1, name: '张三' });

// 获取数据(指定泛型,返回对应类型)
const username = storage.get<string>('username'); // string | null
const user = storage.get<{ id: number; name: string }>('user'); // {id: number; name: string} | null

console.log(username); // 张三
console.log(user?.id); // 1

// 复用实例
const storage2 = StorageUtil.getInstance();
console.log(storage === storage2); // true

总结

  1. TS类:是JS类的“强类型版”,核心增加了属性/方法的类型约束、访问修饰符(public/private/protected)、只读属性,本质还是创建对象的模板;
  2. 构造函数:类的初始化方法,new实例时自动执行,TS支持参数简化写法,子类必须通过super()调用父类构造函数;
  3. 单例模式:核心是private构造函数 + 静态缓存实例 + 静态获取方法,保证类只有一个实例,常用于全局工具类、管理器。

记住:TS的类和单例都是在JS基础上增加了类型约束,核心逻辑和JS是一致的,先理解“为什么用”,再记“怎么写”,很快就能上手项目中的TS代码了。

更多推荐