类与继承(Class & Inheritance)
系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承(本文)
文章目录
前言
ES2015 引入的 class 与 extends,为构造函数与原型继承提供了更规整的写法;在语义上,它们仍是基于原型的模型:实例的原型链、instanceof 与构造器对象之间的关系,可在上一篇《原型与原型链》中找到对应图示。本篇从声明规则、super 与 this 的约束、静态成员、私有字段 # 等角度说明用法与易错点;文末「动物体系 + 单例」综合示例在知识点顺序上与素材文档「类与继承」一节一致,便于对照复习。
一、class 的本质与声明规则
下列几条可视为使用 class 时的「底座结论」,与构造函数、原型链的描述可以逐条互译。
- 语法糖:类建立在基于原型的构造函数之上;
class C {}在概念上对应「具名的构造函数 +prototype对象上挂方法」这一套机制,并不等于某些静态语言里「类即类型」在编译期单独存在的模型。 - 不存在声明提升:
class声明不会提升,且处于块级作用域,须先定义后使用;在定义之前引用会触发 ReferenceError(存在暂时性死区式的约束,可类比let)。 - 继承与静态方法:子类通过
extends建立与父类的链接,在子类constructor中通过super()调用父类构造函数;static成员挂在构造器上而非实例上,子类可继承或重写。 - 默认严格模式:
class体内为严格模式代码,有利于提早暴露常见的this误用。
{
const c = new C(); // ReferenceError:尚未初始化
class C {}
}
二、常见用途(简要)
在日常工程里,class 常用于以下几类场景(与是否使用框架无关,可按需取舍)。
- 组件抽象:在 React、Vue 等框架的历史写法中,类组件曾长期承担「本地状态 + 生命周期」的载体;理解
class有助于阅读遗留代码与部分底层封装。 - 工具类封装:把一批相关方法收拢到同一构造器命名空间下,静态方法做纯函数入口、实例方法带内聚状态,边界清晰。
- 设计模式落地:例如用工厂式静态方法创建实例、用单例约束全局唯一连接或总线对象、或配合策略对象组织可替换行为。
- 类型约束:与 TypeScript 的接口、泛型、可见性修饰符并用时,「类 + 继承」比在普通对象字面量上标注类型更顺手续写面向对象风格的库与领域模型。
三、实例属性、类字段与「原型方法」
constructor:在new时执行;若省略,引擎会补全一个将参数原样交给父类的构造器(在存在extends时才有意义,见下文)。- 在
constructor里为this.xxx赋值:得到的是实例自有属性。 - 类体中的非静态方法(如
m() {}):默认定义在C.prototype上,由实例共享同一函数引用,节省内存;方法内的this取决于调用方式(参见本系列第 5 篇)。 - 公有实例字段(类字段语法,如
count = 0):一般等价于在构造过程中在实例上定义属性;在派生类中,字段初始化的实际执行时机在super()成功返回之后。 - 箭头函数形式的字段(如
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
四、extends 与 super
extends:建立原型链上的对接关系——派生类的prototype的[[Prototype]]指向基类的prototype;派生类(构造器函数)自身的[[Prototype]]则指向基类构造器,以便继承静态成员。- 派生类
constructor中必须先调用super(...),然后才能使用this;否则在访问this时会报错。原因在于:基类部分需要先完成实例分配与初始化,子类才能在其上追加状态。 - 若派生类不显式写
constructor:等价于使用默认构造,它会调用super(...arguments)并转发实参。 super作为函数调用:super(...)仅在派生类构造器内出现,语义为「执行父类构造器逻辑」。super作为属性访问:在方法中写super.m(),会从基类的prototype上解析m,并以当前this调用(需由引擎做HomeObject绑定;箭头函数方法没有自己的super绑定,一般不要在字段初始化里依赖super)。- 面试常考点:在异步回调、
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"
五、静态方法、静态字段与静态初始化块
static m():挂在构造器函数自身(C.m),不出现在实例上;子类可继承或遮蔽,内部可用super指涉父类静态侧。- 静态字段:同属构造器对象;适合放与类型相关、不随实例变化的配置。
static {}静态块(较新特性):在类求值阶段执行一次,便于在不写函数外层语句的情况下完成静态注册;具体可用性取决于运行环境与构建链的目标版本。
class Util {
static version = "1.0.0";
static sum(a, b) {
return a + b;
}
}
Util.sum(1, 2); // 3
六、私有字段与私有方法 #
#name语法:成员名以#开头,表示词法私有——仅在类体大括号内可见,外部无法用点号、Reflect或Object.getOwnPropertyNames枚举到同名键(与_prefix命名约定不同,_只是团队纪律,#由语言强制执行)。- 子类与基类各自拥有一套私有名空间:子类不能访问基类实例的
#成员,除非基类提供受控的公开接口。 - 与 TS
private:private仅在编译期约束;运行时 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(函数对象侧)」。- 实例
sub:Object.getPrototypeOf(sub) === Sub.prototype,继续沿链可到Base.prototype、Object.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 篇的工厂函数互补。
九、易混淆点归纳
- 「写在类里」不等于都在
prototype上:类字段、箭头字段多在实例上;普通m() {}方法默认在prototype,实例间共享,利于省内存。 constructor里的this.xx = yy:才是典型的实例自有属性;勿与原型上的方法混为一谈。- 派生类构造器里
super()须先于对this的读写:习惯上写在构造器靠前位置;未执行super()就访问this会报错。 super在构造「调用」与方法中「属性访问」形态不同:前者super(...),后者super.method();勿混用语法。instanceof判断的是原型链:表达的是「Constructor.prototype是否出现在对象的原型链上」,不是把值「贴上类标签」;跨 iframe 时若存在多个全局 Object/构造器环境,结果可能不符合直觉,需单独讨论。- 私有字段无法在外部绕过:
#由语言强制执行;与仅约定_前缀不同。 class仍是用函数作构造器:typeof MyClass === "function";MyClass.prototype仍存在。
十、思考与练习
1. 为什么派生类构造器里不能在 super() 之前读取 this?
解析:实例的「基类部分」尚未完成绑定前,尚未形成完整对象;语言强制 super() 先建立父类姿态的实例,再交由子类扩展。
2. 把方法写成 m = () => {} 与写成 m() {} 的主要差异是什么?
解析:前者每实例一份箭头函数并锁住词法 this;后者在 prototype 上共享一份,this 动态绑定。
3. static 方法中的 this 指向什么?
解析:指向构造器对象本身(子类方法里则为子类构造器),不是实例。
总结
class/extends/super是在原型继承之上的结构化写法;不提升、默认严格模式。- 实例字段、箭头字段、原型方法三者所在位置与
this语义不同,选型影响内存与回调行为。 - 派生类须
super()先于this;super.m()从父原型取方法并以当前实例执行。 #提供语言级私有;static承载类型级能力。- 工程上可组合 类 + 私有静态 实现单例等模式。
下一篇计划整理 JavaScript 执行机制与异步队列(执行上下文、宏任务与微任务、async/await 与事件循环的关系)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)