JS `class` 语法:更清晰的面向对象编程结构

各位观众老爷们,大家好!今天咱们不聊风花雪月,也不谈人生理想,就来聊聊JavaScript里class这个“假正经”的东西。为什么说它假正经呢?因为JavaScript本质上还是基于原型的,class只不过是语法糖,让你写起来更像传统的面向对象语言,但骨子里还是那套原型链的玩法。

好了,废话不多说,咱们这就开讲!

一、class 登场:不再是原型链的“祖传秘方”

class出现之前,JavaScript里实现面向对象,那可是个体力活。你得手动构建原型链,搞清楚构造函数、prototype__proto__这些弯弯绕绕,一不小心就掉进坑里。

// 传统方式:构造函数 + 原型链
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}, and I'm ${this.age} years old.`);
};

const john = new Person("John", 30);
john.sayHello(); // 输出: Hello, my name is John, and I'm 30 years old.

看到没?是不是觉得有点繁琐?特别是当你要实现继承的时候,那代码更是绕得像毛线团。

现在,class来了!它可以让你用更简洁、更易读的方式定义类,就像你在Java、C++里做的那样。

// class 语法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}, and I'm ${this.age} years old.`);
  }
}

const jane = new Person("Jane", 25);
jane.sayHello(); // 输出: Hello, my name is Jane, and I'm 25 years old.

是不是清爽多了?这就是class的魅力所在。它把复杂的原型链操作隐藏起来,让你专注于类的定义和使用。

二、class 的基本结构:构造函数、方法和属性

一个class通常包含以下几个部分:

  • constructor (构造函数): 这是创建对象时调用的函数,用于初始化对象的属性。如果没有显式定义constructor,JavaScript会自动创建一个默认的构造函数。
  • 方法 (Methods): 类中定义的函数,用于实现对象的行为。
  • 属性 (Properties): 对象的状态,存储在对象的属性中。
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }

  getPerimeter() {
    return 2 * (this.width + this.height);
  }
}

const rect = new Rectangle(10, 5);
console.log(`Area: ${rect.getArea()}`); // 输出: Area: 50
console.log(`Perimeter: ${rect.getPerimeter()}`); // 输出: Perimeter: 30

在这个例子中,Rectangle类有widthheight两个属性,以及getAreagetPerimeter两个方法。

三、class 的继承:子类继承父类的衣钵

继承是面向对象编程的一个重要概念。它允许你创建一个新的类(子类),继承已有类(父类)的属性和方法,并在此基础上进行扩展。

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

  speak() {
    console.log("Generic animal sound");
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类的构造函数
    super(name);
    this.breed = breed;
  }

  speak() {
    console.log("Woof!");
  }

  fetch() {
    console.log("Fetching the ball!");
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
console.log(`Dog's name: ${dog.name}`); // 输出: Dog's name: Buddy
dog.speak(); // 输出: Woof!
dog.fetch(); // 输出: Fetching the ball!

const animal = new Animal("Generic Animal");
animal.speak(); // 输出: Generic animal sound

这里,Dog类继承了Animal类,并扩展了自己的属性breed和方法fetchDog类还重写了speak方法,实现了自己的行为。

注意super()的用法。super()用于调用父类的构造函数,确保父类的属性也能被正确初始化。如果没有调用 super(),在子类的构造函数中 this 关键字不能被使用,会报错。

四、static 静态方法和属性:类级别的“共享资源”

静态方法和属性是属于类本身的,而不是属于类的实例。它们可以通过类名直接访问,而不需要创建类的实例。

class MathUtils {
  static PI = 3.14159;

  static calculateCircleArea(radius) {
    return MathUtils.PI * radius * radius;
  }
}

console.log(`PI: ${MathUtils.PI}`); // 输出: PI: 3.14159
console.log(`Area of circle with radius 5: ${MathUtils.calculateCircleArea(5)}`); // 输出: Area of circle with radius 5: 78.53975

在这个例子中,PI是静态属性,calculateCircleArea是静态方法。它们都属于MathUtils类,可以直接通过MathUtils.PIMathUtils.calculateCircleArea()访问。

使用场景:

  • 常量: 静态属性可以用来存储常量,例如上面的PI
  • 工具函数: 静态方法可以用来实现工具函数,例如上面的calculateCircleArea
  • 单例模式: 静态属性可以用来实现单例模式,确保类只有一个实例。

五、gettersetter:属性访问的“守门员”

gettersetter允许你控制属性的访问和修改。它们可以让你在访问或修改属性时执行一些额外的操作,例如验证数据、计算值等。

class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  get fahrenheit() {
    return this._celsius * 1.8 + 32;
  }

  set fahrenheit(value) {
    this._celsius = (value - 32) / 1.8;
  }

  get celsius() {
    return this._celsius;
  }

  set celsius(value) {
      if (value < -273.15) {
          throw new Error("Temperature cannot be below absolute zero.");
      }
      this._celsius = value;
  }
}

const temp = new Temperature(25);
console.log(`Celsius: ${temp.celsius}`); // 输出: Celsius: 25
console.log(`Fahrenheit: ${temp.fahrenheit}`); // 输出: Fahrenheit: 77

temp.fahrenheit = 68;
console.log(`Celsius: ${temp.celsius}`); // 输出: Celsius: 20

try {
    temp.celsius = -300;
} catch (e) {
    console.error(e.message);  // 输出: Temperature cannot be below absolute zero.
}

在这个例子中,fahrenheit是一个只读属性,因为它只有getter没有settercelsius属性具有getter和setter,并且setter对输入值进行了验证。

注意,这里使用了_celsius来存储实际的摄氏度值。这是一种常见的约定,表示这个属性是私有的,不应该直接访问。当然,JavaScript并没有真正的私有属性(直到ES2022引入了私有字段),这只是一种建议。

六、extends 的多重继承?想多了!

JavaScript的class只支持单继承,也就是说一个类只能继承一个父类。如果你想实现多重继承的效果,可以考虑使用组合(Composition)的方式,将多个类的功能组合到一个类中。

七、class 的本质:语法糖,糖衣炮弹?

前面已经说过了,class本质上是语法糖,它只是让你的代码看起来更像面向对象,但底层仍然是基于原型链的。

class MyClass {
  constructor(value) {
    this.value = value;
  }

  getValue() {
    return this.value;
  }
}

console.log(typeof MyClass); // 输出: function
console.log(MyClass.prototype.constructor === MyClass); // 输出: true

可以看到,MyClass的类型是function,也就是说class实际上就是一个函数。而MyClass.prototype.constructor指向的也是MyClass本身。

八、关于私有属性,ES2022的救星

在ES2022之前,JavaScript并没有真正的私有属性。通常的做法是使用下划线_来命名私有属性,但这只是一种约定,并不能阻止外部访问。

ES2022引入了私有字段,使用#符号来声明。私有字段只能在类的内部访问,外部无法访问。

class Counter {
  #count = 0;

  increment() {
    this.#count++;
  }

  decrement() {
    this.#count--;
  }

  getCount() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出: 2

// console.log(counter.#count); // 报错: Private field '#count' must be declared in an enclosing class

九、总结:class 的优缺点

特性 优点 缺点
语法 更简洁、易读,更接近传统的面向对象语言。 本质上是语法糖,对原型链的理解仍然是必要的。
继承 提供了extends关键字,方便实现继承关系。 只支持单继承,多重继承需要使用组合的方式实现。
静态方法/属性 方便定义类级别的共享资源。
getter/setter 可以控制属性的访问和修改,实现数据验证和计算。
私有属性 ES2022引入了私有字段,可以真正实现属性的私有化。 在ES2022之前,只能使用约定来模拟私有属性。

总的来说,class是JavaScript面向对象编程的一个重要改进。它让代码更易读、易维护,也更符合传统的面向对象编程习惯。虽然它只是语法糖,但它降低了学习和使用的门槛,让更多人可以轻松地编写面向对象的JavaScript代码。

十、最后的彩蛋:class 的一些“坑”

  • this 指向:class的方法中,this的指向可能会让你感到困惑。你需要小心处理this的绑定,特别是当你在回调函数中使用方法时。可以使用bindcallapply或者箭头函数来解决this的指向问题。
  • new 关键字: 使用class创建对象必须使用new关键字。如果你忘记了new,会抛出一个错误。
  • class 声明不会提升: 与函数声明不同,class声明不会被提升到代码的顶部。你必须在使用class之前先声明它。

好了,今天的讲座就到这里。希望大家通过今天的学习,能够更好地理解和使用JavaScript的class语法,写出更优雅、更健壮的代码!记住,class只是工具,关键在于你如何运用它,设计出优秀的面向对象程序。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注