解密JS中的class:语法、继承与底层实现原理

232 阅读5分钟

理解JavaScript中的class:语法糖背后的本质

JavaScript作为一门灵活多变的语言,在ES6(ECMAScript 2015)中引入了class关键字,这为JavaScript的面向对象编程带来了更清晰、更接近传统面向对象语言的语法。本文将全面剖析JavaScript中的class,从基础语法到底层实现,帮助开发者深入理解这一重要特性。

一、class基础:从传统构造函数到class语法

1.1 传统的构造函数模式

在ES6之前,JavaScript使用构造函数和原型链来实现面向对象编程:

javascript

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const john = new Person('John', 30);
john.greet(); // Hello, my name is John

这种方式虽然有效,但对于来自其他面向对象语言的开发者来说显得不够直观。

1.2 class语法糖的引入

ES6的class提供了一种更简洁的语法:

javascript

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const john = new Person('John', 30);
john.greet(); // Hello, my name is John

关键点

  • class关键字声明一个类
  • constructor是类的构造函数,在实例化时调用
  • 方法直接定义在类体内,不需要function关键字
  • 方法之间不需要逗号分隔

二、class的详细语法解析

2.1 类的基本构成

一个完整的class可以包含以下部分:

javascript

class MyClass {
  // 构造函数
  constructor(...) {
    // ...
  }
  
  // 实例方法
  method1(...) {}
  
  // getter方法
  get something(...) {}
  
  // setter方法
  set something(...) {}
  
  // 静态方法
  static staticMethod(...) {}
  
  // 静态属性
  static staticProperty = ...;
  
  // 私有属性(ES2022)
  #privateField = ...;
  
  // 私有方法(ES2022)
  #privateMethod() {}
}

2.2 构造函数

constructor方法是类的默认方法,通过new命令生成对象实例时自动调用。一个类必须有constructor方法,如果没有显式定义,JavaScript引擎会默认添加一个空的constructor方法。

javascript

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

2.3 实例方法与静态方法

  • 实例方法:由类的实例调用
  • 静态方法:由类本身调用,使用static关键字定义

javascript

class MyClass {
  instanceMethod() {
    console.log('This is an instance method');
  }
  
  static staticMethod() {
    console.log('This is a static method');
  }
}

const obj = new MyClass();
obj.instanceMethod(); // This is an instance method
MyClass.staticMethod(); // This is a static method

2.4 Getter和Setter

可以使用getset关键字对某个属性设置存值函数和取值函数,拦截该属性的存取行为:

javascript

class User {
  constructor(name) {
    this._name = name;
  }
  
  get name() {
    return this._name.toUpperCase();
  }
  
  set name(value) {
    if (value.length < 2) {
      console.log("Name is too short");
      return;
    }
    this._name = value;
  }
}

const user = new User('John');
console.log(user.name); // JOHN
user.name = 'Alice';
console.log(user.name); // ALICE
user.name = 'A'; // Name is too short

2.5 静态属性与实例属性

ES2022正式将静态属性加入标准:

javascript

class MyClass {
  static staticProperty = 'static value';
  instanceProperty = 'instance value';
  
  constructor() {
    console.log(this.instanceProperty); // instance value
  }
}

console.log(MyClass.staticProperty); // static value

2.6 私有属性和方法(ES2022)

ES2022正式为class添加了私有属性和方法,在属性名之前使用#表示:

javascript

class Counter {
  #count = 0; // 私有属性
  
  #increment() { // 私有方法
    this.#count++;
  }
  
  get value() {
    return this.#count;
  }
  
  increment() {
    this.#increment();
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.value); // 1
console.log(counter.#count); // 报错:Private field '#count' must be declared in an enclosing class

三、class的继承

3.1 extends实现继承

class通过extends关键字实现继承:

javascript

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // 调用父类的constructor
  }
  
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const d = new Dog('Rex');
d.speak(); // Rex barks.

3.2 super关键字

super关键字有两种用法:

  1. 作为函数调用:super()在子类构造函数中调用父类构造函数
  2. 作为对象引用:super.method()调用父类方法

javascript

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

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

const c = new Child('Alice', 10);
c.sayHello();
// Hello from Alice
// I'm 10 years old

3.3 继承内置类

class可以继承JavaScript内置的引用类型:

javascript

class MyArray extends Array {
  get first() {
    return this[0];
  }
  
  get last() {
    return this[this.length - 1];
  }
}

const arr = new MyArray(1, 2, 3);
console.log(arr.first); // 1
console.log(arr.last); // 3

四、class的底层实现原理

4.1 class是语法糖

JavaScript的class本质上仍然是基于原型继承的语法糖。上面的class声明等价于:

javascript

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

4.2 class与普通函数的区别

虽然class本质上是函数,但与普通函数有一些重要区别:

  1. class必须使用new调用,普通函数可以不使用new
  2. class内部定义的方法不可枚举(non-enumerable)
  3. class默认使用严格模式
  4. class不存在变量提升
  5. class内部不能重写类名

4.3 原型链关系

使用class建立的继承关系与原型链继承完全一致:

javascript

class A {}
class B extends A {}

console.log(B.__proto__ === A); // true
console.log(B.prototype.__proto__ === A.prototype); // true

五、class的高级特性

5.1 类表达式

与函数一样,class也可以使用表达式形式定义:

javascript

const MyClass = class {
  // ...
};

const Person = class NamedPerson {
  constructor() {}
  whoAmI() {
    console.log(NamedPerson.name); // NamedPerson仍然可用
  }
};

const p = new Person();
p.whoAmI(); // NamedPerson
console.log(Person.name); // NamedPerson

5.2 new.target属性

在构造函数中,new.target返回new命令作用于的那个构造函数:

javascript

class Parent {
  constructor() {
    console.log(new.target.name);
  }
}

class Child extends Parent {}

const p = new Parent(); // Parent
const c = new Child(); // Child

5.3 类的混入(Mixins)

JavaScript不支持多重继承,但可以通过混入模式实现类似功能:

javascript

const Serializable = Base => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
};

const Loggable = Base => class extends Base {
  log() {
    console.log(this);
  }
};

class Person {
  constructor(name) {
    this.name = name;
  }
}

const LoggableSerializablePerson = Serializable(Loggable(Person));

const p = new LoggableSerializablePerson('John');
p.log(); // {name: "John"}
console.log(p.serialize()); // {"name":"John"}

六、class的最佳实践

6.1 何时使用class

适合使用class的场景:

  • 需要创建多个相似对象
  • 需要继承和复用代码
  • 需要组织和管理复杂的代码结构

6.2 常见陷阱与避免方法

  1. 忘记使用new:class构造函数必须使用new调用

    javascript

    class Person {}
    const p = Person(); // 报错
    
  2. 方法绑定问题:类方法中的this取决于调用方式

    javascript

    class Button {
      constructor() {
        this.click = this.click.bind(this);
      }
      
      click() {
        console.log(this);
      }
    }
    
  3. 过度使用继承:优先考虑组合而非继承

6.3 性能考量

class语法不会带来额外的性能开销,因为它在底层仍然是基于原型的实现。现代JavaScript引擎对class和原型继承都进行了高度优化。

七、总结

JavaScript的class语法提供了一种更清晰、更结构化的方式来实现面向对象编程,但它本质上仍然是基于原型继承的语法糖。理解class的底层实现原理对于编写高效、可维护的JavaScript代码至关重要。随着ES2022私有属性和方法的加入,JavaScript的class系统变得更加完善,能够更好地满足复杂应用开发的需求。

掌握class不仅意味着掌握了一种语法,更是理解了JavaScript面向对象编程的核心思想。在实际开发中,应根据具体需求合理使用class特性,结合原型继承的优势,构建灵活、高效的代码结构。