Javascript 零基础教程

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 Alice

2. 构造函数 (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); // 输出: Tesla

2.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); // 输出: undefined

2.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 为什么使用继承?

  1. 代码复用(DRY 原则):你不需要在 Manager 中重新编写 namesalary 的初始化逻辑,直接利用 Employee 的逻辑即可。
  2. 方法重写(Method Overriding):子类可以根据自己的需求,覆盖父类同名的方法。上面的例子中,Manager 修改了 getDetails 的输出格式。
  3. 多态性:你可以编写处理 Employee 类型对象的代码,而这些代码同样适用于所有的子类(如 ManagerDeveloper 等),因为它们都具备父类的基本特征。

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.propobj.prop (调用方式一致)