大家好,我是你们今天的 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.target
。new.target
是一个元属性(meta-property),它只有在构造函数(包括 class
的 constructor
和普通函数构造函数)中才能使用。它的值是:
- 如果构造函数是通过
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"
在这个例子中,cat
的 speak
函数中的 super.speak()
并不是直接调用 animal
的 speak
,而是从 cat.__proto__
开始查找,也就是 dog
,然后 dog
内部的 super.speak()
才会调用 animal
的 speak
。
表格总结
为了更好地总结今天的内容,我们用一个表格来梳理一下:
概念 | 描述 | 作用 |
---|---|---|
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 的面向对象编程。
好了,今天的课程就到这里。希望大家有所收获,下次再见!