在JavaScript中,Class是ES6引入的一种语法糖,它基于原型继承机制,提供了更接近传统面向对象语言的写法。下面我们将深入探讨JavaScript中的Class,包括其定义、继承、静态方法、私有字段等。

JavaScript是一门基于原型(Prototype)的编程语言,而非传统的基于类(Class)的语言。在ES6引入Class语法之前,JavaScript完全通过原型链实现对象间的继承关系。

ES6引入的Class语法并不是 JavaScript 中新增的继承模型,而是基于原型继承的语法糖(Syntactic Sugar),使代码更易读、更接近传统面向对象语言的写法。

Class的基本定义

使用class关键字可以定义一个类,类名通常首字母大写。类中可以包含构造函数constructor、实例方法、静态方法等。

1. 基本定义
// 定义一个基本的类
class Person {
  // 构造函数
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // 实例方法
  sayHello() {
    return `Hello, my name is ${this.name}, and I'm ${this.age} years old.`;
  }
}

// 实例化
const person = new Person('青癸', 20);
console.log(person.sayHello()); // 输出: Hello, my name is 青癸, and I'm 20 years old.

Class的本质:

  • Class语法仍然基于原型继承实现。

  • constructor方法对应ES5中的构造函数。

  • Class内部定义的方法会被添加到类的prototype上。

  • static关键字定义的方法直接附加在类本身上。

  • Class的实例化必须使用new关键字。

2. 继承

使用 extends 关键字实现继承,子类可以继承父类的属性和方法。子类的构造函数中必须调用 super() 来初始化父类的构造函数。

class Student extends Person {
  // 构造函数
  constructor(name, age, grade) {
    super(name, age); // 调用父类的constructor
    this.grade = grade;
  }

  // 重写父类方法
  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm in grade ${this.grade}`);
  }

  // 子类特有方法
  study() {
    console.log(`${this.name} is studying`);
  }
}

const student = new Student('黑龙', 21, 10);
student.sayHello(); // 输出: Hello, my name is 黑龙 and I'm in grade 12
student.study(); // 输出: 黑龙 is studying

class Parent {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    return `Hello from ${this.name}`;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 1. 作为函数调用父类构造函数
    this.age = age;
  }
  
  sayHello() {
    // 2. 作为对象调用父类方法
    return `${super.sayHello()}, I'm ${this.age} years old`;
  }
}

Class继承的本质

  • extends关键字建立了子类与父类之间的原型链关系。

  • super关键字用于调用父类的构造函数和方法。

  • 子类的prototype.__proto__指向父类的prototype。

  • 子类本身的__proto__指向父类。

  • 方法重写通过在子类原型上定义同名方法实现。

3. 静态方法

静态方法属于类本身,而不是类的实例。它们通常用于实现与类相关的功能,但不依赖于实例的数据。静态方法可以被子类继承,也可以通过子类调用。

class MyClass {
  static staticMethod() {
    console.log('Static method called');
  }
}

MyClass.staticMethod(); // 输出: Static method called

class ChildClass extends MyClass {}

ChildClass.staticMethod(); // 输出: Static method called

class MathUtils { 
  static PI = 3.14159;
  
  // 静态方法
  static max(...args) {
    return Math.max(...args);
  }
  
  // 静态块 (ES2022)
  static {
    console.log('MathUtils类已加载');
  }
}

// MathUtils类已加载
console.log(MathUtils.max(1, 5, 3)); // 5
console.log(MathUtils.PI); // 3.14159
4. 私有方法和私用属性

ES2022引入了真正的私有方法和私有属性,使用#前缀表示。私有属性和方法只能在类内部访问,外部无法直接访问。

class BankAccount {
  // 私有字段
  #balance = 0;
  // 私有静态字段
  static #bankCode = 'BANK001';
  
  constructor(initialBalance) {
    this.#balance = initialBalance;
  }
  
  // 私有方法
  #validateAmount(amount) {
    if (amount <= 0) throw new Error('Amount must be positive');
    return true;
  }
  
  deposit(amount) {
    this.#validateAmount(amount);
    this.#balance += amount;
  }
  
  getBalance() {
    return this.#balance;
  }
  
  // 静态方法访问私有静态字段
  static getBankCode() {
    return this.#bankCode;
  }
}

const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
// console.log(account.#balance); // 报错:私有字段无法访问
// 需要放置script标签中,浏览器控制台不会报错,具体原因不明
5. Getter和Setter

可以使用 get 和 set 关键字来定义获取和设置属性值的方法,从而允许对属性进行更精细的控制。

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // 使用getter定义计算属性area
  get area() {
    return this.width * this.height;
  }

  // 使用setter对宽度进行验证
  set width(newWidth) {
    if (newWidth > 0) {
      this._width = newWidth;
    } else {
      console.error('Width must be positive');
    }
  }

  get width() {
    return this._width;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.area); // 输出: 200
rect.width = -5; // 输出: Width must be positive
6. 类表达式

类也可以使用表达式的方式定义,类似于函数表达式。

// 匿名类表达式
const Person = class {
  constructor(name) {
    this.name = name;
  }
};

// 命名类表达式
const Employee = class EmployeeClass {
  constructor(name) {
    this.name = name;
  }
  
  getName() {
    // EmployeeClass只在类内部可见
    return EmployeeClass.name;
  }
};
7. 注意事项
  • 类声明和类表达式都不会被提升(hoisting),因此必须先定义后使用。

  • 类中的所有方法默认都是不可枚举的(non-enumerable)。

  • 类中的代码默认以严格模式执行。

  • 类内部默认严格模式。

  • 没有私有方法(ES2022之前),使用约定或WeakMap模拟。

  • 避免在构造函数中返回对象。

8. 与构造函数的对比

在ES5中,我们通常使用构造函数和原型来模拟类。ES6的Class语法更简洁,更易理解。

特性

Class声明

传统构造函数

语法清晰度

高(像Java)

低(原型链操作)

严格模式

自动严格模式

需要手动开启

提升(Hoisting)

不完全提升(有暂时性死区)

存在变量提升

原型修改

更安全,不易被外部篡改

容易被意外修改

// ES5
function PersonES5(name) {
  this.name = name;
}

PersonES5.prototype.sayHello = function() {
  console.log('Hello, ' + this.name);
};

// ES6
class PersonES6 {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}
9. 继承内置类

Class语法还可以用来继承JavaScript的内置类,比如Array、Error等。

class MyArray extends Array {
  // 可以添加自定义方法
  first() {
    return this[0];
  }

  last() {
    return this[this.length - 1];
  }
}

const arr = new MyArray(1, 2, 3);
console.log(arr.first()); // 输出: 1
console.log(arr.last()); // 输出: 3
10. 类与原型链的关系

JavaScript的class是ES6引入的语法糖,本质上仍然是基于原型的继承,但提供了更接近传统面向对象语言的语法。

class Animal {}
class Rabbit extends Animal {}

console.log(Rabbit.prototype.__proto__ === Animal.prototype); // true
console.log(Rabbit.__proto__ === Animal); // true

// instanceof 操作符
const rabbit = new Rabbit();
console.log(rabbit instanceof Rabbit); // true
console.log(rabbit instanceof Animal); // true
console.log(rabbit instanceof Object); // true

关键点

  • 每个函数都有一个prototype属性,指向一个对象。

  • 通过new关键字创建的实例,其__proto__属性指向构造函数的prototype。

  • 实例可以访问原型链上的所有属性和方法。

  • instanceof运算符通过检查原型链来判断对象类型。

11. Mixin模式

JavaScript中的Mixin模式是一种代码复用机制,允许将一个或多个对象的属性和方法"混入"(复制)到另一个对象中,从而实现功能复用,避免了传统继承的复杂性(如多重继承的问题)。

核心思想:

通过"复制"而非"继承"的方式,将多个独立的功能模块组合到目标对象上,使对象可以灵活地拥有多种特性。

在JavaScript中,Mixin主要通过以下几种方式实现:

1. 使用 Object.assign()(ES6+)

最常用的现代实现方式,直接将源对象的属性复制到目标对象:

// 定义Mixin对象(功能模块)
const canEat = {
  eat() {
    console.log('正在吃东西');
  }
};

const canSleep = {
  sleep() {
    console.log('正在睡觉');
  }
};

// 创建目标对象
const person = {
  name: '孙雪'
};

// 将Mixin混入目标对象
Object.assign(person, canEat, canSleep);

// 使用混入的方法
person.eat();   // 正在吃东西
person.sleep(); // 正在睡觉
2. 原型链Mixin

将Mixin添加到构造函数的原型上,使所有实例都能共享这些方法:

// 定义构造函数
function Animal(name) {
  this.name = name;
}

// 定义Mixin
const canRun = {
  run() {
    console.log(`${this.name}正在跑`);
  }
};

// 将Mixin混入原型链
Object.assign(Animal.prototype, canRun);

// 创建实例
const dog = new Animal('小柴');
dog.run(); // 小柴正在跑
3. 自定义Mixin函数(传统方式)

更灵活地控制混入过程,比如过滤某些属性或处理冲突:

function mixin(target, ...sources) {
  sources.forEach(source => {
    // 只复制自身属性(不包括原型链)
    Object.keys(source).forEach(key => {
      // 可以添加冲突处理逻辑
      if (!(key in target)) {
        target[key] = source[key];
      }
    });
  });
  return target;
}

// 使用自定义Mixin
const person = mixin({}, canEat, canSleep, {
  work() {
    console.log('正在工作');
  }
});

优缺点

  • 优点

    • 灵活的代码复用,避免继承层级过深。

    • 实现类似"多重继承"的效果。

    • 功能模块独立,易于维护。

  • 缺点

    • 可能导致方法名冲突(需额外处理)。

    • 无法直接追踪属性来源,调试较复杂。

    • 引用类型的属性会被共享(注意深拷贝问题)

总结

JavaScript的class语法虽然看起来像传统面向对象编程,但底层仍然是基于原型的。理解这一本质对于掌握JavaScript至关重要。Class提供了更清晰的语法结构,使得代码更易读、更易维护,是现代JavaScript开发中的重要工具。

彩蛋

完整示例代码,请移步Cnb: 《深入理解JavaScript:Class的本质与原型继承