JavaScript ES6 类入门
在 ES6 引入 class 关键字之前,JavaScript 主要通过原型链(Prototype)实现面向对象编程。
ES6 的类语法本质上是 JavaScript 原型继承的语法糖,它提供了一种更清晰、更符合直觉的结构来定义对象模板。通过类,开发者可以更高效地封装数据、实现代码复用以及管理复杂的状态逻辑。
1. ES6 Class 基本语法
在 ES6 中,class 关键字为 JavaScript 提供了一种更标准、更易读的定义对象模板的方式。
尽管其底层仍然是基于原型链(Prototype)的继承机制,但这种语法糖让代码结构更接近于传统面向对象语言(如 Java 或 C#)。
1.1 类的声明与实例化
定义类使用 class 关键字,后跟类名。通过 new 关键字可以基于类创建一个实例。
class User {
// 构造函数:用于初始化属性
constructor(name) {
this.name = name;
}
// 实例方法:定义在类的原型上
sayHello() {
console.log(`Hello, I am ${this.name}`);
}
}
// 实例化
const user = new User('Alice');
user.sayHello(); // 输出: Hello, I am Alice2. 构造函数 (Constructor)
在 ES6 类中,constructor(构造函数)是创建和初始化对象的“发动机”。每当你使用 new 关键字调用类时,它都会被自动执行。
2.1 基础构造函数:初始化属性
构造函数最常见的用途是将传入的参数赋值给实例(this)。
class Car {
constructor(brand, color) {
this.brand = brand; // 将参数赋值给实例属性
this.color = color;
}
}
const myCar = new Car('Tesla', 'Red');
console.log(myCar.brand); // 输出: Tesla2.2 构造函数特性
2.2.1 默认构造函数
如果你在定义类时没有写 constructor,JavaScript 会在后台自动为你生成一个空的构造函数,类似于:
class EmptyClass {
// 引擎自动添加的:
// constructor() {}
}2.2.2 唯一性
一个类中只能存在一个 constructor 方法。如果你定义了两个,程序会抛出 SyntaxError。
2.2.3 返回行为的特殊性
构造函数默认返回 this(即新创建的实例)。但是,如果你手动在构造函数中 return 一个对象,那么 new 的结果将是那个被返回的对象,而不是原来的实例:
class Box {
constructor() {
this.type = 'box';
// 手动返回一个完全不同的对象
return { name: 'I am a custom object' };
}
}
const item = new Box();
console.log(item.name); // 输出: I am a custom object
console.log(item.type); // 输出: undefined2.3 在继承中的使用 (super)
当使用 extends 继承时,子类的构造函数必须调用 super(),它负责调用父类的构造函数并初始化父类属性。如果不调用,程序会报错。
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须先调用父类构造函数
this.breed = breed;
}
}3. 实例方法 (Instance Methods)
实例方法定义在类的原型(Prototype)上,所有通过 new 创建的实例都可以调用这些方法。它们能够通过 this 访问当前实例的属性。
在类中定义的函数(如上例中的 sayHello)会自动成为实例方法。
- 共享性:这些方法被挂载在类的原型对象(
Prototype)上,这意味着所有实例共享同一份方法定义,节省内存。 this指向:在实例方法内部,this指向当前被创建的实例对象。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}4. 静态方法 (Static Methods)
使用 static 关键字定义的方法不属于实例,而是属于类本身。
class Calculator {
static sum(a, b) {
return a + b;
}
}
// 通过类名直接调用,无需实例化
console.log(Calculator.sum(1, 2)); // 3用途:通常用于编写工具函数,或者是不需要依赖实例属性(即不需要 this.xxx)的逻辑。
5. 继承(Extends)
在 JavaScript 中,继承(Inheritance) 是面向对象编程的核心。它允许你创建一个类(子类),该类继承另一个类(父类)的属性和方法。这不仅能减少代码冗余,还能通过层级结构组织代码,让程序更具扩展性。
5.1 继承的基本语法
我们使用 extends 关键字来建立继承关系,并使用 super 来调用父类的功能。
核心要点:
extends:表示“子类继承父类”。super():在子类的构造函数(constructor)中,必须在this被使用之前调用super()。它负责调用父类的构造函数,初始化父类的部分。
5.2 代码示例:从“员工”到“经理”
假设我们有一个通用的 Employee 类,以及一个更具体的 Manager 类:
// 父类
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
getDetails() {
return `${this.name} 的薪资是 ${this.salary}`;
}
}
// 子类:继承 Employee
class Manager extends Employee {
constructor(name, salary, department) {
// 1. 调用父类构造函数,初始化 name 和 salary
super(name, salary);
// 2. 初始化子类特有的属性
this.department = department;
}
// 子类可以重写(Override)父类的方法
getDetails() {
// 调用父类原本的方法,并追加新的逻辑
return `${super.getDetails()},所属部门:${this.department}`;
}
// 子类可以拥有父类没有的新方法
conductMeeting() {
console.log(`${this.name} 正在主持 ${this.department} 的会议。`);
}
}
const mgr = new Manager('张三', 20000, '技术部');
console.log(mgr.getDetails());
// 输出: 张三 的薪资是 20000,所属部门:技术部
mgr.conductMeeting();5.3 为什么使用继承?
- 代码复用(DRY 原则):你不需要在
Manager中重新编写name和salary的初始化逻辑,直接利用Employee的逻辑即可。 - 方法重写(Method Overriding):子类可以根据自己的需求,覆盖父类同名的方法。上面的例子中,
Manager修改了getDetails的输出格式。 - 多态性:你可以编写处理
Employee类型对象的代码,而这些代码同样适用于所有的子类(如Manager、Developer等),因为它们都具备父类的基本特征。
5.4 关键易错点
super的双重角色:- 作为函数调用:
super(),只能在子类constructor中使用,用于初始化父类。 - 作为对象使用:
super.methodName(),用于在子类方法中调用父类原型链上的同名方法。 - 必须调用
super():如果你在子类中定义了constructor,就不能省略super()。如果你不写constructor,JavaScript 会自动生成一个带有super(...args)的默认构造函数。 - 单继承限制:JavaScript 的
class仅支持单继承(一个类只能有一个父类)。
6. 存取器 (Getters & Setters)
在 JavaScript 类中,存取器(Getters & Setters) 是一种特殊的语法,允许你拦截对对象属性的访问和赋值操作。
你可以把它们看作是属性的“守门人”。通过它们,你可以在获取或修改数据时插入额外的逻辑(例如验证数据、记录日志或进行格式化),而外部调用者依然像使用普通属性一样使用它们。
6.1 为什么需要 Getters 和 Setters?
直接访问属性(如 user.name = 'alice')通常无法控制数据的质量。通过存取器,你可以:
- 数据封装:隐藏内部存储的变量。
- 输入校验:确保赋值的内容符合业务规则。
- 计算属性:在读取属性时动态计算值。
6.2 代码示例
假设我们有一个 User 类,我们要确保年龄必须大于 0:
class User {
constructor(name, age) {
this.name = name;
this._age = age; // 使用下划线表示这是一个“私有”属性,不建议外部直接修改
}
// Getter:读取 age 时触发
get age() {
return this._age;
}
// Setter:设置 age 时触发
set age(value) {
if (value < 0) {
console.error("年龄不能为负数!");
return;
}
this._age = value;
}
}
const user = new User('Bob', 25);
// 像操作普通属性一样使用
console.log(user.age); // 25 (触发 get age())
user.age = 30; // 触发 set age(30)
user.age = -5; // 触发 set age(-5),输出错误信息,赋值失败6.3 普通属性 VS. 存取器
| 特性 | 普通属性 (this.name = 'x') | 存取器 (get/set) |
|---|---|---|
| 执行逻辑 | 直接读写内存 | 执行一段自定义函数 |
| 数据校验 | 无法做到 | 可以在赋值时进行逻辑校验 |
| 调用方式 | obj.prop | obj.prop (调用方式一致) |