深入理解 JavaScript 中 class 的基本语法与继承机制
文章目录
一、class 基本语法
1.1 类的定义与实例化
在JavaScript中,我们可以通过 class 关键字来定义类。例如,下面定义了一个简单的 Person 类:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
在上述代码中,Person 就是我们定义的类,类内部有一个 constructor 构造函数(下一小节会详细介绍它),用于初始化类的实例属性。
而使用 new 运算符可以创建类的实例,就像这样:
const person1 = new Person('张三', 20);
const person2 = new Person('李四', 25);
这里,person1 和 person2 就是 Person 类的两个不同实例,它们各自拥有独立的属性值。new 关键字在创建实例时,主要进行了以下几个操作:
1.2 constructor 构造函数
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,它会自动被调用。例如前面定义的 Person 类中的 constructor:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
当我们使用 new Person(‘张三’, 20) 创建实例时,constructor 里的代码就会执行,将传入的参数 ‘张三’ 和 20 分别赋值给实例的 name 和 age 属性。
如果我们没有显式地定义 constructor 方法,JavaScript 会自动为类添加一个默认的空的 constructor 方法,就像这样:
class SomeClass {
constructor() {}
}
默认情况下,constructor 方法返回的就是实例对象 this ,不过也可以指定它返回其他对象。但需要注意的是,如果返回了一个非原始类型的值,它将成为 new 表达式的值,而原本的 this 值将会被丢弃。例如:
class AnotherClass {
constructor() {
this.prop = '初始属性值';
return { newProp: '新的属性值' };
}
}
const instance = new AnotherClass();
console.log(instance.newProp); // 输出 "新的属性值",原本的this里的属性就不存在了
1.3 类方法与属性
在类中,我们可以定义实例属性和方法,以及静态属性和方法。
实例属性:是定义在实例对象(this )上的属性,每个实例都有自己独立的一份副本。比如在 Person 类中定义实例属性:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.greeting = '你好呀'; // 这就是一个实例属性
}
sayHello() { // 这是一个实例方法
console.log(`${this.greeting},我叫${this.name},今年${this.age}岁。`);
}
}
const person1 = new Person('王五', 30);
const person2 = new Person('赵六', 35);
person1.sayHello(); // 输出 "你好呀,我叫王五,今年30岁。"
person2.sayHello(); // 输出 "你好呀,我叫赵六,今年35岁。"
这里的 greeting 、name 和 age 都是实例属性,每个 Person 类的实例都有自己独立的这些属性值。
原型属性(方法):实例方法实际上是定义在类的原型(prototype )上的,多个实例可以共享这些方法,节省内存空间。从本质上来说,class 定义的类的方法都是通过原型来实现共享的。我们可以通过查看原型来理解:
console.log(Person.prototype);
// 可以看到上面定义的sayHello方法就在原型对象上
静态属性和方法:是属于类本身的属性和方法,而不是实例的。通过 static 关键字来定义,直接通过类名来调用,实例对象无法访问。例如:
class MathUtils {
static PI = 3.1415926;
static add(a, b) {
return a + b;
}
}
console.log(MathUtils.PI); // 输出 3.1415926
console.log(MathUtils.add(2, 3)); // 输出 5
const utilInstance = new MathUtils();
console.log(utilInstance.PI); // 会报错,实例不能访问静态属性
utilInstance.add(1, 2); // 会报错,实例不能调用静态方法
1.4 this 指向问题
在类的方法中,this 的默认指向是调用该方法的实例对象。但在一些复杂的场景下,容易出现 this 指向不符合预期的情况。
比如在下面的代码中:
class Counter {
constructor() {
this.count = 0;
document.addEventListener('click', this.increment);
}
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
当点击页面时,会发现报错或者 count 的值没有按预期增加,这是因为在事件回调函数里,this 的指向发生了改变,不再指向 Counter 的实例对象了,而是指向了触发事件的元素(在浏览器环境下通常是 document )。
为了解决这类 this 指向问题,我们可以使用一些方法来改变 this 的指向,常见的有 call 、apply 和 bind 。
function showName() {
console.log(this.name);
}
const person = { name: '小明' };
showName.call(person); // 输出 "小明"
function sum(num1, num2) {
return this.val + num1 + num2;
}
const obj = { val: 10 };
const result = sum.apply(obj, [5, 3]);
console.log(result); // 输出 18
class Counter {
constructor() {
this.count = 0;
const boundIncrement = this.increment.bind(this);
document.addEventListener('click', boundIncrement);
}
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
通过 bind 把 increment 方法里的 this 绑定为 Counter 的实例,这样在点击事件触发时,就能正确地操作实例的 count 属性了。
不同的 this 绑定方式在使用场景和传参形式等方面有所区别,需要根据实际情况来合理选择使用哪种方式来处理 this 指向问题,确保类中的方法能正确地操作实例属性等内容。
二、类的继承
2.1 继承的基本概念与实现
在JavaScript中,类继承是实现代码复用和扩展的重要概念,通过继承,我们可以基于现有类创建新类,并继承父类的属性和方法。使用 extends 关键字可以实现基本的类继承,例如我们有一个基础的 Animal 类:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`The ${this.name} makes a sound.`);
}
}
现在我们要创建一个 Dog 类,让它继承自 Animal 类,可以这样写:
class Dog extends Animal {
bark() {
console.log(`${this.name} barks loudly.`);
}
}
let dog = new Dog('Fido');
dog.speak(); // 输出: The Fido makes a sound.
dog.bark(); // 输出: Fido barks loudly.
在上述代码中,Dog 类就是 Animal 类的子类,通过 extends 关键字建立了继承关系,Dog 类自动继承了 Animal 类中定义的 name 属性以及 speak 方法,同时它还可以定义自己独有的 bark 方法,这就体现了继承带来的代码复用与扩展的优势。
2.2 super 关键字的用途
super 关键字在JavaScript的类继承中有非常重要的作用,主要体现在两个方面:一是在子类构造函数中调用父类构造函数,二是用于访问父类的方法。
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
}
在 Child 类的构造函数中,通过 super(name) 调用了父类 Parent 的构造函数,并传入了 name 参数,这样,子类 Child 就拥有了父类 Parent 的 name 属性,同时还能定义自己的 age 属性。
class Parent {
sayHello() {
return 'Hello from Parent';
}
}
class Child extends Parent {
sayHello() {
return `${super.sayHello()} and Hello from Child`;
}
}
const child = new Child();
console.log(child.sayHello()); // 输出: Hello from Parent and Hello from Child
在子类 Child 的 sayHello 方法中,通过 super.sayHello() 调用了父类 Parent 的 sayHello 方法,然后在此基础上添加了自己的内容进行返回,实现了在子类中复用父类方法逻辑并进行扩展的功能。
2.3 方法重写与扩展
当子类继承父类后,子类可以通过定义与父类同名的方法来重写父类的方法,从而改变其行为。以下是一个简单示例:
class Animal {
sound() {
console.log('Animal makes a sound');
}
}
class Dog extends Animal {
sound() {
console.log('Dog barks');
}
}
const dog = new Dog();
dog.sound(); // 输出:'Dog barks'
在上述示例中,Animal 类有一个 sound 方法,它打印出 Animal makes a sound。然后,Dog 类继承自 Animal 类,并重写了 sound 方法,改为打印出 Dog barks。当我们创建一个 Dog 类的实例并调用 sound 方法时,就会执行子类中重写后的方法。
而且,子类在重写父类方法时,还可以调用父类的方法,使用 super 关键字,以保留和扩展父类的功能。例如:
class Dog extends Animal {
sound() {
super.sound(); // 调用父类的sound方法
console.log('Dog barks');
}
}
const dog = new Dog();
dog.sound();
// 输出:
// 'Animal makes a sound'
// 'Dog barks'
这里子类 Dog 的 sound 方法首先调用了父类 Animal 的 sound 方法,然后再在此基础上添加了额外的行为,实现了在继承基础上对方法的合理扩展。
2.4 继承中的静态方法与属性
静态方法和属性是属于类本身的,而不是类的实例的,通过 static 关键字来定义,直接通过类名来调用,实例对象无法访问它们。在类继承关系中,静态方法和属性也有着特定的继承规则。
例如,我们有一个 Animal 类定义了静态属性和方法:
class Animal {
static planet = 'Earth';
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
console.log(`${this.name} runs with speed ${this.speed}.`);
}
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}
}
然后创建一个 Rabbit 类继承自 Animal 类:
class Rabbit extends Animal {
hide() {
console.log(`${this.name} hides!`);
}
}
此时,子类 Rabbit 可以访问父类 Animal 的静态属性 planet,像这样:
let rabbits = [new Rabbit('White Rabbit', 10), new Rabbit('Black Rabbit', 5)];
rabbits.sort(Rabbit.compare);
rabbits[0].run(); // Black Rabbit runs with speed 5.
console.log(Rabbit.planet); // Earth
从这个例子可以看出,对于静态方法,子类可以直接继承使用,而对于静态属性,子类同样能够继承并访问,就如同它们是子类本身所定义的静态成员一样,这在一些需要基于类层面进行操作和共享数据的场景中非常有用,比如在多个子类实例间进行比较、按照统一规则排序等操作时,静态方法就能发挥很好的复用性优势。
三、实战应用与最佳实践
3.1 实际案例分析
假设我们正在开发一个简单的图形绘制应用程序,其中有一个基类 Shape,它具有一些通用的属性和方法,如颜色、位置等,以及一个绘制方法 draw。然后我们有具体的子类 Circle 和 Rectangle,它们继承自 Shape 类,并分别实现自己独特的绘制逻辑。
以下是代码示例:
// 基类Shape
class Shape {
constructor(color, x, y) {
this.color = color;
this.x = x;
this.y = y;
}
setColor(color) {
this.color = color;
}
draw() {
console.log(`Drawing a shape at (${this.x}, ${this.y}) with color ${this.color}`);
}
}
// 子类Circle
class Circle extends Shape {
constructor(color, x, y, radius) {
super(color, x, y);
this.radius = radius;
}
draw() {
console.log(`Drawing a circle at (${this.x}, ${this.y}) with radius ${this.radius} and color ${this.color}`);
}
}
// 子类Rectangle
class Rectangle extends Shape {
constructor(color, x, y, width, height) {
super(color, x, y);
this.width = width;
this.height = height;
}
draw() {
console.log(`Drawing a rectangle at (${this.x}, ${this.y}) with width ${this.width}, height ${this.height} and color ${this.color}`);
}
}
// 使用示例
const circle = new Circle('red', 10, 20, 5);
circle.draw();
const rectangle = new Rectangle('blue', 30, 40, 10, 20);
rectangle.draw();
在这个案例中,Shape 类作为基类,提供了通用的属性和方法。Circle 和 Rectangle 子类继承自 Shape 类,通过 super 关键字调用父类的构造函数来初始化继承的属性,并各自实现了独特的 draw 方法来绘制特定的图形。这样的设计使得代码结构清晰,易于扩展和维护。如果后续需要添加新的图形类型,只需创建一个新的子类并继承自 Shape 类,然后实现相应的绘制逻辑即可。
3.2 代码优化与注意事项
忘记使用 new 关键字实例化类:这是一个常见的错误,会导致函数调用而不是类实例化。例如:const person = Person(‘John’, 30); 应该改为 const person = new Person(‘John’, 30);。
在类的方法中使用箭头函数导致 this 指向错误:箭头函数会继承定义时的 this 上下文,而不是类实例的 this。如果在类方法中需要正确的 this 指向,应使用普通函数定义。例如:
class MyClass {
constructor() {
this.value = 1;
// 错误示例
this.clickHandler = () => {
console.log(this.value);
};
// 正确示例
this.clickHandler = function() {
console.log(this.value);
};
}
}
方法重写时错误地覆盖了父类的重要逻辑:当子类重写父类方法时,有时可能会不小心完全覆盖父类的关键逻辑,而不是在其基础上进行扩展。为了避免这种情况,可以在子类方法中先调用 super 方法来执行父类的原有逻辑,然后再添加子类特有的逻辑。例如:
class Parent {
doSomething() {
console.log('Parent doing something');
}
}
class Child extends Parent {
doSomething() {
super.doSomething(); // 先执行父类的逻辑
console.log('Child doing something more');
}
}
最后
推荐学习网站:ES6教程
作者:前端没钱