JavaScript内核与高级编程之:`JavaScript` 的 `class` 语法与 `new.target`:其在继承中的底层作用。

大家好,我是你们今天的 JavaScript 语法课代表。今天咱们来聊聊 JavaScript 的 class 语法,以及藏在它背后的 new.target,尤其是它们在继承中扮演的那些“不可告人”的角色。放心,今天咱们不掉书袋,争取把这些概念讲得像唠嗑一样轻松愉快。

第一幕:class 语法——披着 OO 外衣的语法糖

首先,我们要明确一点:JavaScript 的 class 语法,本质上是语法糖。它只是让 JavaScript 的原型继承看起来更像传统的面向对象编程(OOP)语言(比如 Java、C++)的 class 声明。但它并没有改变 JavaScript 原型继承的本质。

咱们先来看一个简单的例子:

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

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

let animal = new Animal("Generic Animal");
animal.speak(); // 输出 "Generic Animal makes a sound."

这段代码,看起来是不是很像 Java 或者 C++?但请记住,它仍然是 JavaScript。class 只是一个更友好的方式来创建基于原型的对象。

constructor 方法,是类的构造函数。当我们使用 new 关键字创建一个类的实例时,constructor 方法会被调用。

speak 方法,就是一个原型方法。它会被添加到 Animal.prototype 上,所有 Animal 的实例都可以访问到它。

第二幕:继承——extends 关键字的妙用

接下来,我们来看看 extends 关键字,它是实现继承的关键。

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

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

  wagTail() {
    console.log(`${this.name} wags its tail.`);
  }
}

let dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // 输出 "Buddy barks."
dog.wagTail(); // 输出 "Buddy wags its tail."

在这个例子中,Dog 类继承了 Animal 类。

  • extends Animal:这表明 Dog 类继承自 Animal 类。它会设置 Dog.prototype.__proto__Animal.prototype,实现了原型链的连接。
  • super(name):这是在子类的 constructor 中调用父类的 constructor 的关键。它必须在 this 关键字之前调用,否则会报错。super() 本质上是 Animal.call(this, name) 的一种更简洁的写法。
  • speak()Dog 类重写了 Animal 类的 speak() 方法。这就是方法覆盖(method overriding)。当调用 dog.speak() 时,会先在 Dog 类的原型链上查找,找到 Dog 类的 speak() 方法,并执行它。
  • wagTail()Dog 类新增了一个 wagTail() 方法。这是子类特有的方法。

第三幕:new.target——幕后英雄的登场

现在,我们来聊聊今天的主角之一:new.targetnew.target 是一个元属性(meta-property),它只有在构造函数(包括 classconstructor 和普通函数构造函数)中才能使用。它的值是:

  • 如果构造函数是通过 new 关键字调用的,那么 new.target 的值就是被 new 调用的构造函数本身。
  • 如果构造函数不是通过 new 关键字调用的(比如直接调用),那么 new.target 的值就是 undefined

咱们来看一个例子:

function MyConstructor() {
  if (!new.target) {
    throw new Error("MyConstructor must be called with new");
  }
  console.log("new.target:", new.target);
}

let obj = new MyConstructor(); // 输出 "new.target: function MyConstructor() { ... }"
//MyConstructor(); // 抛出错误 "Error: MyConstructor must be called with new"

在这个例子中,我们使用 new.target 来确保 MyConstructor 只能通过 new 关键字调用。如果直接调用 MyConstructor(),就会抛出一个错误。

new.target 在继承中的作用

new.target 在继承中扮演着非常重要的角色,尤其是在抽象类和多态的实现中。

1. 抽象类的实现

JavaScript 本身并没有提供抽象类的概念,但我们可以使用 new.target 来模拟抽象类的行为。抽象类不能被实例化,只能被继承。

class AbstractAnimal {
  constructor() {
    if (new.target === AbstractAnimal) {
      throw new Error("AbstractAnimal cannot be instantiated directly");
    }
  }

  speak() {
    throw new Error("Method 'speak()' must be implemented.");
  }
}

class ConcreteAnimal extends AbstractAnimal {
  speak() {
    console.log("Concrete animal speaks.");
  }
}

//let abstractAnimal = new AbstractAnimal(); // 抛出错误 "Error: AbstractAnimal cannot be instantiated directly"
let concreteAnimal = new ConcreteAnimal();
concreteAnimal.speak(); // 输出 "Concrete animal speaks."

在这个例子中,AbstractAnimal 是一个抽象类。在它的 constructor 中,我们检查 new.target 是否等于 AbstractAnimal。如果是,就抛出一个错误,阻止直接实例化 AbstractAnimal

ConcreteAnimal 继承了 AbstractAnimal,并实现了 speak() 方法。它可以被正常实例化。

2. 决定构造函数的行为

new.target 还可以用来决定构造函数的行为,根据实际创建的对象类型来执行不同的初始化逻辑。

class Parent {
  constructor() {
    if (new.target === Parent) {
      console.log("Parent constructor called directly.");
      this.type = "Parent";
    } else {
      console.log("Parent constructor called from a subclass.");
      this.type = "Subclass";
    }
  }
}

class Child extends Parent {
  constructor() {
    super();
  }
}

let parent = new Parent(); // 输出 "Parent constructor called directly."  this.type = "Parent"
let child = new Child(); // 输出 "Parent constructor called from a subclass." this.type = "Subclass"

在这个例子中,Parent 的构造函数根据 new.target 的值,来判断是被直接调用还是被子类调用,从而执行不同的初始化逻辑。

3. 实现工厂模式

new.target 还可以用于实现工厂模式,根据不同的参数创建不同的对象。

class Shape {
  constructor(type) {
    if (new.target === Shape) {
      throw new Error("Shape is an abstract class.");
    }
  }

  static create(type) {
    switch (type) {
      case "circle":
        return new Circle();
      case "square":
        return new Square();
      default:
        throw new Error("Invalid shape type.");
    }
  }
}

class Circle extends Shape {
  constructor() {
    super();
    console.log("Creating a circle.");
  }
}

class Square extends Shape {
  constructor() {
    super();
    console.log("Creating a square.");
  }
}

let circle = Shape.create("circle"); // 输出 "Creating a circle."
let square = Shape.create("square"); // 输出 "Creating a square."

在这个例子中,Shape 是一个抽象类,它有一个静态方法 create(),用于根据不同的类型创建不同的形状对象。

第四幕:super 关键字的更多秘密

super 关键字除了可以在 constructor 中调用父类的构造函数之外,还可以在普通方法中调用父类的方法。

class Animal {
  speak() {
    console.log("Animal makes a sound.");
  }
}

class Dog extends Animal {
  speak() {
    super.speak(); // 调用父类的 speak() 方法
    console.log("Dog barks.");
  }
}

let dog = new Dog();
dog.speak();
// 输出:
// "Animal makes a sound."
// "Dog barks."

在这个例子中,Dog 类的 speak() 方法通过 super.speak() 调用了 Animal 类的 speak() 方法。

super 的查找机制

super 的查找机制有点特殊。它不是简单地沿着原型链向上查找,而是根据 super 所在的方法所属的对象来确定查找的起点。

具体来说,super 的查找起点是 super 所在的方法所属对象的原型。

举个例子:

const animal = {
  speak() {
    console.log("Animal speaks");
  }
};

const dog = {
  speak() {
    super.speak();
    console.log("Dog barks");
  },
  __proto__: animal
};

dog.speak(); // 输出 "Animal speaks" 和 "Dog barks"

const cat = {
  speak() {
    super.speak();
    console.log("Cat meows");
  }
};

Object.setPrototypeOf(cat, dog); // 将 cat 的原型设置为 dog

cat.speak(); // 输出 "Animal speaks", "Dog barks", 和 "Cat meows"

在这个例子中,catspeak 函数中的 super.speak() 并不是直接调用 animalspeak,而是从 cat.__proto__ 开始查找,也就是 dog,然后 dog 内部的 super.speak() 才会调用 animalspeak

表格总结

为了更好地总结今天的内容,我们用一个表格来梳理一下:

概念 描述 作用
class JavaScript 中声明类的语法糖,本质上是基于原型的继承。 提供更友好的面向对象编程的语法,简化基于原型的对象创建和继承。
extends 用于实现继承的关键字,设置子类的原型为父类的实例。 实现原型链的连接,使得子类可以继承父类的属性和方法。
constructor 类的构造函数,用于初始化类的实例。 在使用 new 关键字创建类的实例时,constructor 方法会被调用,用于初始化实例的属性。
super() 在子类的 constructor 中调用父类的 constructor 确保父类的构造函数被正确调用,完成父类的初始化。
new.target 元属性,只有在构造函数中才能使用,值为被 new 调用的构造函数本身,如果不是通过 new 调用,则为 undefined 可以用于实现抽象类、决定构造函数的行为、实现工厂模式等。
super.method() 在子类的方法中调用父类的方法。 实现方法覆盖,在子类的方法中可以调用父类的方法,并在此基础上添加新的逻辑。

总结

今天我们深入探讨了 JavaScript 的 class 语法以及 new.target 在继承中的作用。希望通过今天的讲解,大家能够更深入地理解 JavaScript 的原型继承机制,并能够灵活运用 class 语法和 new.target 来编写更健壮、更灵活的代码。

记住,class 只是语法糖,原型继承才是 JavaScript 的本质。掌握原型继承,才能真正掌握 JavaScript 的面向对象编程。

好了,今天的课程就到这里。希望大家有所收获,下次再见!

发表回复

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