ES6类(Classes):语法糖下的原型与继承

ES6类(Classes):语法糖下的原型与继承

引言

嘿,大家好!今天我们要聊一聊ES6中的类(Classes)。如果你觉得类是个高深莫测的概念,那你就大错特错了!其实,ES6的类只是JavaScript原型链和构造函数的一层“语法糖”,它让代码看起来更简洁、更易读。接下来,我会用轻松诙谐的语言,带你深入了解ES6类背后的原型与继承机制。

什么是ES6类?

在ES6之前,JavaScript并没有真正的“类”概念。我们通常使用构造函数和原型链来模拟面向对象编程。ES6引入了class关键字,虽然它看起来像是其他语言中的类,但实际上它只是对现有机制的封装,简化了代码的编写。

传统方式 vs. ES6类

让我们先看看传统的构造函数和原型链的写法:

// 传统方式
function Person(name) {
  this.name = name;
}

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

const person1 = new Person('Alice');
person1.sayHello(); // Hello, my name is Alice

现在,用ES6类来实现同样的功能:

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

  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const person1 = new Person('Alice');
person1.sayHello(); // Hello, my name is Alice

是不是看起来更简洁了?class关键字让我们不需要显式地使用functionprototype,但底层的机制并没有改变。

类的本质:构造函数 + 原型链

ES6类本质上仍然是基于构造函数和原型链的。你可以通过以下代码验证这一点:

console.log(typeof Person); // "function"
console.log(Person === Person.prototype.constructor); // true

这说明Person类实际上是一个构造函数,而Person.prototype是它的原型对象。当我们创建一个实例时,new Person()会调用构造函数,并将实例的__proto__指向Person.prototype

静态方法与静态属性

除了实例方法,ES6类还支持静态方法和静态属性。静态方法不会被实例继承,而是直接属于类本身。静态属性也是类似的概念,但它们是类的属性而不是方法。

class MathUtils {
  static add(a, b) {
    return a + b;
  }

  static PI = 3.14; // 静态属性
}

console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.PI); // 3.14

getter 和 setter

ES6类还支持getset方法,用于定义属性的访问器。这在处理私有属性或需要对属性进行验证时非常有用。

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

  get area() {
    return this._width * this._height;
  }

  set width(value) {
    if (value > 0) {
      this._width = value;
    } else {
      throw new Error('Width must be positive');
    }
  }
}

const rect = new Rectangle(4, 5);
console.log(rect.area); // 20
rect.width = 6;
console.log(rect.area); // 30

继承:子类与父类的关系

ES6类的一个重要特性是继承。通过extends关键字,我们可以轻松地创建子类,并从父类继承属性和方法。子类可以重写父类的方法,也可以调用父类的方法。

基本继承

下面是一个简单的继承示例:

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

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

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

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog('Buddy', 'Golden Retriever');
dog.speak(); // Buddy barks.

在这个例子中,Dog类继承了Animal类的name属性和speak方法。super()用于调用父类的构造函数,确保子类能够正确初始化父类的属性。

方法重写与super

子类可以重写父类的方法,但有时我们仍然希望调用父类的原始实现。这时可以使用super关键字来调用父类的方法。

class Cat extends Animal {
  constructor(name, color) {
    super(name);
    this.color = color;
  }

  speak() {
    super.speak(); // 调用父类的speak方法
    console.log(`${this.name} meows.`);
  }
}

const cat = new Cat('Whiskers', 'gray');
cat.speak();
// Whiskers makes a noise.
// Whiskers meows.

多层继承

ES6类支持多层继承,即子类可以继承自另一个子类。这种结构在复杂的项目中非常常见。

class Bird extends Animal {
  constructor(name, wingspan) {
    super(name);
    this.wingspan = wingspan;
  }

  fly() {
    console.log(`${this.name} flies with a wingspan of ${this.wingspan} meters.`);
  }
}

class Penguin extends Bird {
  constructor(name, wingspan, habitat) {
    super(name, wingspan);
    this.habitat = habitat;
  }

  fly() {
    console.log(`${this.name} cannot fly, but it swims in the ${this.habitat}.`);
  }
}

const penguin = new Penguin('Pingu', 0.7, 'Antarctica');
penguin.fly(); // Pingu cannot fly, but it swims in the Antarctica.

类的内部工作原理

虽然ES6类看起来像是面向对象编程的“真类”,但它们实际上是基于原型链的。为了更好地理解这一点,我们可以查看类的内部结构。

构造函数与原型

每个类都有一个隐式的构造函数,它可以通过constructor方法来定义。如果没有显式定义构造函数,JavaScript会提供一个默认的构造函数。

class Vehicle {
  // 默认构造函数
}

class Car extends Vehicle {
  // 子类的构造函数
  constructor(make, model) {
    super(); // 调用父类的构造函数
    this.make = make;
    this.model = model;
  }
}

原型链

当你创建一个类的实例时,JavaScript会在实例的__proto__属性中存储该类的原型对象。这个原型对象又会指向其父类的原型对象,依次类推,直到到达Object.prototype

class A {}
class B extends A {}
class C extends B {}

const c = new C();
console.log(C.__proto__ === B); // false
console.log(C.__proto__ === B.prototype); // false
console.log(C.__proto__ === Function.prototype); // true
console.log(c.__proto__ === C.prototype); // true
console.log(c.__proto__.__proto__ === B.prototype); // true
console.log(c.__proto__.__proto__.__proto__ === A.prototype); // true
console.log(c.__proto__.__proto__.__proto__.__proto__ === Object.prototype); // true

instanceof 操作符

instanceof操作符用于检查一个对象是否是某个类的实例。它会沿着原型链向上查找,直到找到匹配的构造函数。

const a = new A();
const b = new B();
const c = new C();

console.log(c instanceof C); // true
console.log(c instanceof B); // true
console.log(c instanceof A); // true
console.log(c instanceof Object); // true

总结

好了,今天的讲座就到这里!我们讨论了ES6类的基本语法、继承机制以及它们与原型链的关系。虽然ES6类看起来像是面向对象编程的“真类”,但它们实际上是基于JavaScript的构造函数和原型链的语法糖。通过理解这些底层机制,你可以更好地掌握类的工作原理,并写出更加优雅的代码。

最后,引用MDN文档中的一句话:“Class bodies are always executed in strict mode。”这意味着在类中使用的所有代码都会自动启用严格模式,因此你不需要手动添加'use strict';

希望这篇文章对你有所帮助,如果你有任何问题,欢迎随时提问!

发表回复

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