JS 构造函数 (`constructor`) 与 `super()`:继承中的初始化流程

各位观众,各位朋友,大家好!我是今天的主讲人,很高兴能和大家一起聊聊JavaScript构造函数(constructor)和super()这对好基友,以及它们在继承中扮演的关键角色。这俩哥们,一个负责创建对象,一个负责“继承遗产”,可谓是强强联合,缺一不可。

咱们今天的内容,将深入浅出,保证大家听完之后,不仅能理解,还能熟练运用,从此告别继承中的各种“坑”。

一、构造函数:对象的“出生证明”

首先,咱们得搞清楚啥是构造函数。说白了,构造函数就是一个用来创建对象的函数。在JavaScript中,任何函数都可以当做构造函数来使用。但是,为了区分普通函数和构造函数,我们通常会约定俗成地使用大写字母开头来命名构造函数。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    console.log(`你好,我是${this.name},今年${this.age}岁。`);
  };
}

// 使用new关键字调用构造函数,创建对象
const person1 = new Person("张三", 30);
const person2 = new Person("李四", 25);

person1.greet(); // 输出:你好,我是张三,今年30岁。
person2.greet(); // 输出:你好,我是李四,今年25岁。

上面这段代码,Person就是一个构造函数。当我们使用new关键字调用它时,会发生以下几件事:

  1. 创建一个新的空对象。 就好像医院里的新生儿,刚出生时啥也没有。
  2. this指向这个新对象。 这个新对象就是this的老大了,this说啥就是啥。
  3. 执行构造函数中的代码,给新对象添加属性和方法。 给新生儿穿衣服、喂奶,让它逐渐成长。
  4. 如果构造函数没有显式返回对象,则返回新对象。 孩子养大了,就交给父母了。

如果构造函数显式返回一个对象,那么new操作符会忽略构造函数内部的this指向和属性赋值,直接返回构造函数返回的对象。这在一些特殊情况下会用到,但一般情况下我们不推荐这么做,容易造成混乱。

二、super():继承的“接力棒”

继承是面向对象编程的重要特性,它允许我们创建一个新的类(子类),继承已有类(父类)的属性和方法,从而实现代码的复用。在JavaScript中,我们可以使用extends关键字来实现继承。

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

  speak() {
    console.log("动物叫!");
  }
}

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

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

  fetch() {
    console.log("狗狗捡球!");
  }
}

const dog1 = new Dog("旺财", "金毛");
dog1.speak(); // 输出:汪汪汪!
dog1.fetch(); // 输出:狗狗捡球!
console.log(dog1.name); // 输出:旺财

在这个例子中,Dog类继承了Animal类。Dog类可以使用Animal类的name属性和speak方法,并且还可以定义自己的breed属性和fetch方法。

重点来了,super()是啥?super()函数用于调用父类的构造函数。在子类的构造函数中,必须先调用super(),才能使用this关键字。 这就像接力赛跑,你得先接过上一棒,才能继续跑。

super()的作用:

  1. 调用父类的构造函数,初始化父类的属性。 让子类拥有父类的“基因”。
  2. 在子类的构造函数中使用this之前,必须先调用super() 这是JavaScript的强制规定,否则会报错。

三、super()的用法详解

super()不仅仅是用来调用父类的构造函数,它还有其他用法:

  • super() 调用父类的构造函数,并传递参数。
  • super.methodName() 调用父类的methodName方法。
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log("动物叫!");
  }
}

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

  speak() {
    // 调用父类的speak方法
    super.speak();
    console.log("汪汪汪!");
  }
}

const dog1 = new Dog("旺财", "金毛");
dog1.speak(); // 输出:动物叫!n汪汪汪!

在这个例子中,Dog类的speak方法不仅输出了“汪汪汪!”,还调用了父类Animalspeak方法,输出了“动物叫!”。这体现了继承的特性:子类可以扩展父类的功能。

四、构造函数和super()的“爱恨情仇”:初始化流程

现在,让我们来梳理一下继承中的初始化流程:

  1. 创建子类对象。 就像盖房子,先打地基。
  2. 调用子类的构造函数。 开始装修房子。
  3. 在子类的构造函数中,必须先调用super() 接过父类的“遗产”,初始化父类的属性。
  4. 执行子类构造函数中的代码,初始化子类的属性。 继续装修,添加子类特有的东西。
  5. 返回子类对象。 房子装修好了,可以入住了。

如果子类没有定义构造函数,那么会自动创建一个默认的构造函数,并调用super()

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

  speak() {
    console.log("动物叫!");
  }
}

class Dog extends Animal {
  // 没有定义构造函数,会自动创建
  // constructor(...args) {
  //   super(...args);
  // }

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

const dog1 = new Dog("旺财");
console.log(dog1.name); // 输出:旺财

五、常见错误及避坑指南

在使用构造函数和super()时,有一些常见的错误需要避免:

  • 忘记调用super() 这是最常见的错误,会导致this无法使用,程序崩溃。
  • 在调用super()之前使用this JavaScript会报错,告诉你“必须先调用super()”。
  • super()的参数传递错误。 需要根据父类构造函数的参数列表,正确传递参数。
  • 过度使用继承。 继承虽然强大,但也要适度使用,避免过度耦合,造成代码难以维护。

为了帮助大家更好地理解,我们用一张表格来总结一下:

概念 说明 示例
构造函数 用于创建对象的函数,约定俗成使用大写字母开头命名。 function Person(name, age) { this.name = name; this.age = age; }
new 关键字 用于调用构造函数,创建对象。 const person1 = new Person("张三", 30);
extends 关键字 用于实现继承。 class Dog extends Animal { ... }
super() 调用父类的构造函数。必须在子类构造函数中使用this之前调用。 constructor(name, breed) { super(name); this.breed = breed; }
super.methodName() 调用父类的methodName方法。 speak() { super.speak(); console.log("汪汪汪!"); }
初始化流程 1. 创建子类对象。 2. 调用子类构造函数。 3. 调用super()。 4. 初始化子类属性。 5. 返回子类对象。 详见上文描述
常见错误 1. 忘记调用super()。 2. 在调用super()之前使用this。 3. super()的参数传递错误。 4. 过度使用继承。 详见上文描述

六、实战演练:一个稍微复杂点的例子

咱们来个稍微复杂点的例子,加深一下理解。

class Shape {
  constructor(color) {
    this.color = color;
  }

  getArea() {
    return 0; // 默认面积为0
  }

  toString() {
    return `这是一个${this.color}色的形状,面积为${this.getArea()}。`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

  getArea() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

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

const circle1 = new Circle("红色", 5);
const rectangle1 = new Rectangle("蓝色", 4, 6);

console.log(circle1.toString()); // 输出:这是一个红色的形状,面积为78.53981633974483。
console.log(rectangle1.toString()); // 输出:这是一个蓝色的形状,面积为24。

在这个例子中,Shape是父类,CircleRectangle是子类。CircleRectangle都继承了Shapecolor属性和toString方法,并重写了getArea方法,计算各自的面积。

七、总结与展望

今天,我们深入探讨了JavaScript构造函数和super()在继承中的作用。希望通过今天的讲解,大家能够更加熟练地运用这两个关键概念,写出更加优雅、健壮的JavaScript代码。

继承是面向对象编程的重要组成部分,掌握继承的原理和技巧,对于提高代码的复用性和可维护性至关重要。

当然,JavaScript的继承机制还有一些其他的细节,例如原型链继承、组合继承等等,这些内容我们可以在以后的机会再进行深入探讨。

最后,希望大家在学习和工作中,多多实践,不断总结经验,成为真正的JavaScript高手!感谢大家的聆听!

有什么问题,欢迎大家提问!

发表回复

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