假设你正在开发一个学习平台:每名学员都有姓名和分数,也都能生成自己的成绩说明。只创建一名学员时,写一个对象就够了;当人数增加到几十名,反复手写相同的属性名和方法,不但麻烦,还很容易漏字段、拼错名称。
class 提供了一种集中描述对象结构与行为的写法。你先定义“学员应该拥有什么、能够做什么”,再用 new 创建数据不同但规则一致的实例。初始化、数据校验、共享方法和继承关系也因此有了清楚的归属。
不过,JavaScript 的类没有取代对象与原型。类方法依然存放在 prototype 上,继承依然通过原型链完成。理解这层关系后,constructor、static、extends 和 super 就不会变成一组孤立的关键字。
本文所有控制台结果均使用 Node.js v25.2.1 验证。示例只使用 JavaScript 语言能力,在现代浏览器控制台中也能得到相同的核心结果。
完成本章后,你应该能够:
class 定义对象模板,并用 new 创建彼此独立的实例;constructor、默认参数和公共字段初始化实例;prototype,还是类本身;static 定义不依赖某个具体实例的能力;extends、super 复用和改写父类行为;instanceof;new、丢失 this、误用继承等常见问题。对象字面量适合描述一个具体对象。下面两个对象结构相同,却要重复书写属性名和方法:
const studentA = {
name: "小林",
score: 92,
sayHi() {
return `${this.name}:我这次得了 ${this.score} 分`;
},
};
const studentB = {
name: "小周",
score: 88,
sayHi() {
return `${this.name}:我这次得了 ${this.score} 分`;
},
};当一批对象遵循相同规则时,可以把公共结构提取成类。类名通常使用大驼峰命名,例如 Student、OrderItem:
class Student {
constructor(name, score = 0) {
this.name = name;
this.score = score;
}
sayHi() {
return `${this.name}:我这次得了 ${this.score} 分`;
}
}
const a = new Student(
小林:我这次得了 92 分
小周:我这次得了 88 分
false
trueclass Student { ... } 是类声明,描述一类对象共同的初始化规则和行为。new Student(...) 每执行一次,就创建一个新的实例,因此 a 和 b 并不是同一个对象。instanceof 在这里用来检查 Student.prototype 是否出现在实例的原型链上。
类不会强迫所有实例共享同一份数据。a.name 与 b.name 分别存放在两个实例上,修改其中一个不会影响另一个:
a.score = 95;
console.log(a.score);
console.log(b.score);95
88
实例保存各自的数据,类体中的普通方法由实例通过原型共享。
constructor 是类中的特殊方法。执行 new Order(...) 时,JavaScript 会完成一组初始化动作:
Order.prototype 为原型的新对象;constructor 中的 this 指向这个新对象;
类中最多只能声明一个 constructor。需要处理多种初始化方式时,应使用默认参数、参数对象或静态工厂方法,而不是再写一个同名构造器。
class Order {
status = "draft";
constructor(id, total = 0) {
this.id = id;
this.total = total;
}
summary() {
return `${this.id} / ${this.status} / ${this.total}`;
A001 / draft / 68
A002 / draft / 0
status,id,totalstatus = "draft" 是公共实例字段。创建基础类实例时,实例字段会在 constructor 的代码体运行前初始化。this.id 和 this.total 也会写到当前实例上,所以三个名称都能被 Object.keys(one) 找到。
第二个实例没有传入 total,默认参数让它得到 0。如果参数没有默认值又没有对应实参,JavaScript 通常不会仅因为少传参数而报错,参数值会是 undefined:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
const person = new Person("小满");
console.log(person.firstName, person.lastName);小满 undefined如果类不需要自定义初始化,可以省略 constructor。基础类会使用一个空的默认构造器;派生类则会得到一个会把参数继续交给父类的默认构造器。
this 不是类本身,也不是永久指向某个对象的变量。在构造器和以 instance.method() 形式调用的实例方法中,它指向当前实例;调用方式改变时,this 也可能改变。
类把数据和处理这些数据的行为放在一起。类体中直接定义普通方法时,不需要写 function 关键字:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
greet(prefix = "你好") {
return `${
你好,我是 林 晓
欢迎,我是 周 远
true
false
true输出揭示了成员所在的位置:
greet() 内部通过 this 读取当前调用者,因此 p1.greet() 和 p2.greet() 使用不同姓名。方法还可以接收参数、调用同一个实例的其他方法,并用 return 把结果交给调用者。
方法之所以能共享,是因为读取 p1.greet 时,JavaScript 先检查 p1 自身;没有找到后,再沿原型链找到 Person.prototype.greet。
类体默认在严格模式下运行。把一个依赖 this 的方法取出来再直接调用,它不会自动记住原实例:
const greetLater = p1.greet;
try {
greetLater();
} catch (error) {
console.log(error.name);
}
const boundGreet = p1.greet.bind(p1);
console.log(boundGreet());TypeError
你好,我是 林 晓bind(p1) 创建了一个把 this 固定为 p1 的新函数。将实例方法交给定时器、事件系统或其他回调接收者时,要确认对方最终会怎样调用它。
如果外部代码可以随意写入余额、年龄或进度,对象很快就可能进入无效状态。类可以把真实数据放进私有字段,再通过 getter 和 setter 提供受控入口。
私有字段以 # 开头,而且必须先在类体中声明:
class Wallet {
#balance = 0;
constructor(owner, openingBalance = 0) {
this.owner = owner;
this.balance = openingBalance;
}
get balance() {
return this.#balance;
}
set balance(value) {
if (!
小夏 120
false
RangeError: 余额必须是非负数字wallet.balance 看起来像普通属性,读取时却会执行 get balance();赋值时会执行 set balance(value)。访问器按属性使用,所以不要写成 wallet.balance()。
构造器通过 this.balance = openingBalance 复用了 setter 的校验逻辑,创建和后续修改因此遵守同一条规则。只有 getter 而没有 setter 时,可以对外提供只读入口。
#balance 不只是命名约定。类外直接写 wallet.#balance 会在解析阶段产生语法错误;写 wallet["#balance"] 访问的则是一个名字恰好为 #balance 的普通属性,与真正的私有字段没有关系。

外部代码只接触访问器,私有字段与校验规则留在类内部。
有些操作不依赖某个具体实例。例如“根据已完成数量创建进度对象”只需要输入参数,不需要先拥有一个进度实例。这样的成员可以写成 static:
class LessonProgress {
static unit = "%";
static fromDone(done, total) {
return new LessonProgress(Math.round((done / total) * 100));
}
constructor(value) {
this.value = value;
}
label() {
return `${
75%
undefined
%三个位置现在可以明确区分:
progress.value 是实例自身的数据;progress.label() 是实例通过 LessonProgress.prototype 找到的方法;LessonProgress.fromDone() 和 LessonProgress.unit 属于类本身。静态工厂方法、工具方法、全局计数器和类级配置都适合放在类上。判断依据不是“这个方法是否重要”,而是“它是否需要某个具体实例的状态”。静态方法内部若使用 this,this 指向调用该方法的类,而不是某个实例。

当两个概念满足“子类是一种父类”的关系,并且确实共享稳定行为时,可以用 extends 建立继承。例如,计时任务是一种任务,同时多出预计时长:
class Task {
constructor(title) {
this.title = title;
}
describe() {
return `任务:${this.title}`;
}
static kind() {
return "普通任务";
}
}
class TimedTask extends Task {
constructor(title,
任务:复习 class,预计 25 分钟
true true
true
true
计时版 / 普通任务这段代码同时展示了几种继承行为:
extends Task 让 TimedTask 的实例能够找到父类实例方法;super(title) 调用父类构造器,先初始化 title;describe() 会覆盖继承来的版本;super.describe() 调用父类原型上的实现;super.kind() 调用父类的静态实现;task 的原型链同时包含两个类的 prototype,所以两次 instanceof 都为 true。
派生类自己声明 constructor 后,父类构造器负责建立实例中的父类部分。在它完成前,子类不能正常使用 this:
class ChildTask extends Task {
constructor(title) {
// this.ready = true; // ReferenceError:此时还不能访问 this
super(title);
this.ready = true;
}
}实用规则是:派生构造器先调用 super(...),再访问 this。如果子类完全不写 constructor,JavaScript 会提供类似下面的默认行为:
constructor(...args) {
super(...args);
}父类私有字段仍可由父类构造器放进子类实例,但子类代码不能用同一个私有名称直接访问它。需要让子类使用这部分状态时,应由父类提供方法或受保护的公共接口,而不是突破私有边界。
继承也不是复用代码的唯一办法。“购物车里有商品”是包含关系,通常让 Cart 保存 Product 对象更自然;让 Cart extends Product 会制造错误的“是一种”关系。
class 提供了清楚的语法,但实例方法共享和继承仍由原型完成。以 TimedTask 实例为例,读取一个属性时会按下面的方向查找:
task
→ TimedTask.prototype
→ Task.prototype
→ Object.prototype
→ nullJavaScript 在某一层找到名称后就停止继续向上查找。因此,在实例自身写入一个与原型同名的属性,只会遮蔽原型值,不会改动原型本身。
你也可以在类声明后直接修改它的 prototype。这种写法常见于旧代码、兼容层或运行时扩展:
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `你好,我是 ${this.name}`;
}
}
Person.prototype.role = "学员";
Person.prototype.introduce = function () {
return `${
学员:阿宁
助教:小越
false
truep1 没有自己的 role,所以读取到 Person.prototype.role。给 p2.role 赋值时,会在 p2 自身创建同名属性;原型上的默认值仍是“学员”。
如果你能维护类声明,通常应把永久方法直接写进类体。更不要随意修改 Object.prototype、Array.prototype 等内置原型:新增名称会影响整个运行环境中的对象,还可能与未来标准或第三方代码冲突。
类与构造函数都可以建立“实例自身数据 + 原型共享方法”的结构:
class Box {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
function OldBox(value) {
this.value = value;
}
OldBox.prototype.getValue = function () {
return this
function
42 42
true
true
TypeError: Class constructor Box cannot be invoked without 'new'类本身的 typeof 是 function,实例原型指向 Box.prototype,而 Box.prototype.constructor 又指回 Box。但类并不等于普通函数的简单文本替换:类必须用 new 调用,类体默认使用严格模式,而且类声明在初始化前不能访问。
instanceof 的核心问题是“右侧构造器的 prototype 是否出现在左侧对象的原型链中”。它检查的是原型关系,不是对象当前拥有哪些同名属性。
类相关错误大多不是随机发生的。先判断成员位于哪里,再观察代码以什么方式调用它,通常就能找到原因。
还有两个不容易从报错文字中看出的陷阱。
第一,不要为了“省略 function”而用箭头函数扩展原型。箭头函数没有自己的动态 this:
Person.prototype.wrongIntroduce = () => {
return this.name;
};这里的 this 不会因为 p1.wrongIntroduce() 而自动变成 p1。直接在类体中写普通方法,或给 prototype 赋普通函数更合适。
第二,类声明存在暂时性死区。下面的调用发生在声明初始化之前,会抛出 ReferenceError:
// const early = new Ticket();
class Ticket {}
const ticket = new Ticket();把类声明放在首次使用之前,代码的依赖顺序会更直观。
现在把本章能力组合成一个小型任务模型。看板有普通任务和计时任务;每项任务有自动编号、负责人和受控进度,计时任务还要在说明中显示预计分钟数。
WorkItem 类,用静态字段 nextId 为实例生成递增编号。title 与 owner,初始进度为 0。progress getter/setter。0 到 100 之间的有限数字,否则抛出 RangeError。describe(),返回任务编号、标题、负责人和进度。TimedWorkItem extends WorkItem,构造器额外接收 minutes。describe(),同时用 super.describe() 复用父类结果。可以先从这个骨架开始:
class WorkItem {
static nextId = 1;
#progress = 0;
constructor(title, owner) {
// 生成 id,并保存公共数据
}
get progress() {
// 返回私有进度
}
set progress(value) {
// 校验并更新私有进度
}
describe() {
// 返回任务说明
}
完成后,下面的测试代码应该能够运行:
const reading = new WorkItem("阅读类语法", "小林");
const coding = new TimedWorkItem("完成类练习", "小周", 45);
reading.progress = 100;
coding.progress = 60;
console.log(reading.describe());
console.log(coding.describe());
console.log
一种符合要求的输出是:
#1 阅读类语法 / 小林 / 100%
#2 完成类练习 / 小周 / 60% / 预计 45 分钟
true true
RangeErrordone,当进度等于 100 时返回 true;WorkItem.from(data),根据普通对象创建实例;UrgentWorkItem,覆盖 describe() 并显示优先级;class 集中描述一批对象的初始化规则与共享行为,new 每次创建一个独立实例。constructor 负责初始化;通过 this 写入的字段和公共类字段通常位于实例自身。prototype,由实例沿原型链共享。#privateField 提供语言级私有边界,getter/setter 可以在公开访问时统一校验规则。static 成员属于类本身,适合工厂方法、工具能力、类级配置和计数器。extends 建立“是一种”关系;派生构造器先用 super(...) 完成父类初始化,再使用 this。super.method() 复用父类实现;父类私有名称不能被子类直接访问。Object.prototype → null”,instanceof 也基于这条原型关系。