系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)



前言

ES2015 引入的 classextends,为构造函数与原型继承提供了更规整的写法;在语义上,它们仍是基于原型的模型:实例的原型链、instanceof 与构造器对象之间的关系,可在上一篇《原型与原型链》中找到对应图示。本篇从声明规则super this 的约束静态成员私有字段 # 等角度说明用法与易错点;文末「动物体系 + 单例」综合示例在知识点顺序上与素材文档「类与继承」一节一致,便于对照复习。


一、class 的本质与声明规则

下列几条可视为使用 class 时的「底座结论」,与构造函数、原型链的描述可以逐条互译。

  1. 语法糖建立在基于原型的构造函数之上;class C {} 在概念上对应「具名的构造函数 + prototype 对象上挂方法」这一套机制,并不等于某些静态语言里「类即类型」在编译期单独存在的模型。
  2. 不存在声明提升class 声明不会提升,且处于块级作用域,须先定义后使用;在定义之前引用会触发 ReferenceError(存在暂时性死区式的约束,可类比 let)。
  3. 继承与静态方法:子类通过 extends 建立与父类的链接,在子类 constructor 中通过 super() 调用父类构造函数;static 成员挂在构造器上而非实例上,子类可继承或重写。
  4. 默认严格模式class 体内为严格模式代码,有利于提早暴露常见的 this 误用。
{
  const c = new C(); // ReferenceError:尚未初始化
  class C {}
}

二、常见用途(简要)

在日常工程里,class 常用于以下几类场景(与是否使用框架无关,可按需取舍)。

  1. 组件抽象:在 React、Vue 等框架的历史写法中,类组件曾长期承担「本地状态 + 生命周期」的载体;理解 class 有助于阅读遗留代码与部分底层封装。
  2. 工具类封装:把一批相关方法收拢到同一构造器命名空间下,静态方法做纯函数入口、实例方法带内聚状态,边界清晰。
  3. 设计模式落地:例如用工厂式静态方法创建实例、用单例约束全局唯一连接或总线对象、或配合策略对象组织可替换行为。
  4. 类型约束:与 TypeScript 的接口、泛型、可见性修饰符并用时,「类 + 继承」比在普通对象字面量上标注类型更顺手续写面向对象风格的库与领域模型。

三、实例属性、类字段与「原型方法」

  1. constructor:在 new 时执行;若省略,引擎会补全一个将参数原样交给父类的构造器(在存在 extends 时才有意义,见下文)。
  2. constructor 里为 this.xxx 赋值:得到的是实例自有属性
  3. 类体中的非静态方法(如 m() {}):默认定义在 C.prototype 上,由实例共享同一函数引用,节省内存;方法内的 this 取决于调用方式(参见本系列第 5 篇)。
  4. 公有实例字段(类字段语法,如 count = 0):一般等价于在构造过程中在实例上定义属性;在派生类中,字段初始化的实际执行时机在 super() 成功返回之后
  5. 箭头函数形式的字段(如 fn = () => this):会在每个实例上挂一份函数,词法捕获外层的 this,适合作为回调传出;代价是内存占用相对原型方法更高。
class Counter {
  #n = 0;
  label = "count"; // 实例字段
  inc = () => ++this.#n; // 每实例一份箭头函数
  get value() {
    return this.#n;
  } // 访问器在 prototype
}

const a = new Counter();
const b = new Counter();
a.inc !== b.inc; // true

四、extendssuper

  1. extends:建立原型链上的对接关系——派生类的 prototype[[Prototype]] 指向基类的 prototype;派生类(构造器函数)自身的 [[Prototype]] 则指向基类构造器,以便继承静态成员
  2. 派生类 constructor 中必须先调用 super(...),然后才能使用 this;否则在访问 this 时会报错。原因在于:基类部分需要先完成实例分配与初始化,子类才能在其上追加状态。
  3. 若派生类不显式写 constructor:等价于使用默认构造,它会调用 super(...arguments) 并转发实参。
  4. super 作为函数调用super(...) 仅在派生类构造器内出现,语义为「执行父类构造器逻辑」。
  5. super 作为属性访问:在方法中写 super.m(),会从基类的 prototype解析 m,并以当前 this 调用(需由引擎做 HomeObject 绑定;箭头函数方法没有自己的 super 绑定,一般不要在字段初始化里依赖 super)。
  6. 面试常考点:在异步回调、Promise.then 里若需调用依赖 super 的方法,注意 this 是否仍是期望的实例,必要时先缓存实例引用或使用箭头字段。
class Base {
  constructor(x) {
    this.x = x;
  }
  show() {
    return this.x;
  }
}

class Sub extends Base {
  constructor(x, y) {
    super(x);
    this.y = y;
  }
  show() {
    return `${super.show()}|${this.y}`;
  }
}

new Sub(1, 2).show(); // "1|2"

五、静态方法、静态字段与静态初始化块

  1. static m():挂在构造器函数自身C.m),不出现在实例上;子类可继承或遮蔽,内部可用 super 指涉父类静态侧。
  2. 静态字段:同属构造器对象;适合放与类型相关不随实例变化的配置。
  3. static {} 静态块(较新特性):在类求值阶段执行一次,便于在不写函数外层语句的情况下完成静态注册;具体可用性取决于运行环境与构建链的目标版本。
class Util {
  static version = "1.0.0";
  static sum(a, b) {
    return a + b;
  }
}
Util.sum(1, 2); // 3

六、私有字段与私有方法 #

  1. #name 语法:成员名以 # 开头,表示词法私有——仅在类体大括号内可见,外部无法用点号、ReflectObject.getOwnPropertyNames 枚举到同名键(与 _prefix 命名约定不同,_ 只是团队纪律,# 由语言强制执行)。
  2. 子类与基类各自拥有一套私有名空间:子类不能访问基类实例的 # 成员,除非基类提供受控的公开接口。
  3. 与 TS privateprivate 仅在编译期约束;运行时 JS 的 # 仍有效(在支持该语法的引擎中)。
class Wallet {
  #balance = 0;
  deposit(v) {
    this.#balance += v;
  }
  get balance() {
    return this.#balance;
  }
}
const w = new Wallet();
w.deposit(10);
w.balance; // 10
// w.#balance // SyntaxError(类外书写即非法)

七、与原型链的简要对应

可对照上一篇自行画图验证:

  • class Sub extends Base:大致对应「Sub.prototype 的原型指向 Base.prototype,且 Sub[[Prototype]] 指向 Base(函数对象侧)」。
  • 实例 subObject.getPrototypeOf(sub) === Sub.prototype,继续沿链可到 Base.prototypeObject.prototype
  • instanceof Sub / instanceof Base:只要原型链上出现过对应 prototype,即为 true

因而:class 并没有发明一套新的继承数学,而是把既有机制用更不易写错的方式封装起来。


八、综合示例:继承体系与单例

下面两段代码覆盖 # 私有字段getter实例方法extends / super()静态工厂方法静态私有单例,可与笔记中的「动物体系 + DB」对照阅读。

8.1 基础继承(动物体系)

class Animal {
  #hp;

  constructor(name, hp) {
    this.name = name;
    this.#hp = hp;
  }

  get hp() {
    return this.#hp;
  }

  hit(dmg) {
    this.#hp = Math.max(0, this.#hp - dmg);
  }

  static of(name, hp) {
    return new Animal(name, hp);
  }
}

class Dog extends Animal {
  constructor(name, hp, breed) {
    super(name, hp);
    this.breed = breed;
  }

  bark() {
    return `${this.name}: Woof! (HP: ${this.hp})`;
  }
}

const d = new Dog("Max", 100, "Husky");
d.hit(30);
d.bark(); // "Max: Woof! (HP: 70)"

子类 constructor 中须先 super(name, hp),再为 this.breed 赋值;this.hp 经父类 getter 读取私有 #hp,外部无法直接改写血量字段,只能通过 hit 等行为修改。

8.2 单例(DB

利用 静态私有字段 保存唯一实例,适合全局配置、连接占位等;若业务需要多实例,应改工厂或依赖注入,而不是强套单例。

class DB {
  static #inst = null;

  constructor() {
    if (DB.#inst) return DB.#inst;
    this.conn = "connected";
    DB.#inst = this;
  }

  static get() {
    return DB.#inst ?? new DB();
  }
}

DB.get() === DB.get(); // true

业务里「请求客户端基类」「表单域模型」等,也可让静态方法充当命名空间,实例方法承载有状态逻辑,与第 4 篇的工厂函数互补。


九、易混淆点归纳

  1. 「写在类里」不等于都在 prototype:类字段、箭头字段多在实例上;普通 m() {} 方法默认在 prototype,实例间共享,利于省内存。
  2. constructor 里的 this.xx = yy:才是典型的实例自有属性;勿与原型上的方法混为一谈。
  3. 派生类构造器里 super() 须先于对 this 的读写:习惯上写在构造器靠前位置;未执行 super() 就访问 this 会报错。
  4. super 在构造「调用」与方法中「属性访问」形态不同:前者 super(...),后者 super.method();勿混用语法。
  5. instanceof 判断的是原型链:表达的是「Constructor.prototype 是否出现在对象的原型链上」,不是把值「贴上类标签」;跨 iframe 时若存在多个全局 Object/构造器环境,结果可能不符合直觉,需单独讨论。
  6. 私有字段无法在外部绕过# 由语言强制执行;与仅约定 _ 前缀不同。
  7. class 仍是用函数作构造器typeof MyClass === "function"MyClass.prototype 仍存在。

十、思考与练习

1. 为什么派生类构造器里不能在 super() 之前读取 this

解析:实例的「基类部分」尚未完成绑定前,尚未形成完整对象;语言强制 super() 先建立父类姿态的实例,再交由子类扩展。

2. 把方法写成 m = () => {} 与写成 m() {} 的主要差异是什么?

解析:前者每实例一份箭头函数并锁住词法 this;后者在 prototype共享一份this 动态绑定

3. static 方法中的 this 指向什么?

解析:指向构造器对象本身(子类方法里则为子类构造器),不是实例。


总结

  • class / extends / super 是在原型继承之上的结构化写法;不提升、默认严格模式
  • 实例字段箭头字段原型方法三者所在位置与 this 语义不同,选型影响内存回调行为
  • 派生类super() 先于 thissuper.m() 从父原型取方法并以当前实例执行。
  • # 提供语言级私有static 承载类型级能力。
  • 工程上可组合 类 + 私有静态 实现单例等模式。

下一篇计划整理 JavaScript 执行机制与异步队列(执行上下文、宏任务与微任务、async/await 与事件循环的关系)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐