码字,杂谈

真丶深入理解 JavaScript 原型和原型链(四):ES6中的class

今天最后总结一下 class 与 原型的关系。

ES6 的语法糖 – 类(class)

ES6 有了更加清晰明确的面向对象的关键字,但其实它们只不过是经过修饰的语法糖。

类的基础概念和语法

我们之前在原型链中创建一个对象,需要使用函数的形式,然后在其原型中添加方法/属性,最后通过 new 关键字来创建实例。

function User(name) {
  this.name = name;
}
User.prototype.show = function () {
  console.log("Hi, " + this.name);
};

let user = new User("jeremyjone");
user.show(); // Hi, jeremyjone

那么在 ES6 之后,我们可以使用类的方式:

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

  show() {
    console.log("Hi, " + this.name);
  }
}

let user2 = new User2("jeremyjone");
user2.show(); // Hi, jeremyjone

看上去确实清晰了很多。需要明确几点:

  • 1、constructor 是一个构造函数,创建对象时会自动调用。即使你不写,它也默认存在。
  • 2、所有写在 constructor 中的属性都是实例属性,是定义在实例中的。那么相对的,在 constructor 之外的属性,都是定义在类中的,也就是原型属性。
  • 3、this 指向的是调用的实例对象,静态方法指向类本身。
  • 4、子类使用构造器时,必须使用 super 关键字来扩展构造器,并且需要先调用 super
  • 5、子类会覆盖父类同名属性/方法,这与原型优先级一致。如果需要使用父类属性/方法,使用 super 关键字。
  • 6、使用 static 关键字标明类属性/方法,它们无法在实例中使用,而是通过类直接调用的。

类与原型的关系

为了深入理解,首先来看一下它们的原型结构:

《真丶深入理解 JavaScript 原型和原型链(四):ES6中的class》

看上去差不多,只是一个标记为函数,一个标记为类。

测试一下发现:

// 接上例
user2.__proto__ === User2.prototype; // true
User2.prototype.constructor === User2; // true

这也符合我们之前说过的原型方式,所以 class 本质上还是一个函数,只不过是一个语法糖,一个原型的另一种写法而已。

在此基础上,我们甚至可以通过原型的方式来修改/新增方法:

// 接上例
User2.prototype.print = function () {
  console.log("hello, " + this.name);
};

user2.print(); // hello, jeremyjone

实例属性和原型属性的分别

上面提到,constructor 属性内的是实例属性,之外的是原型属性,可以使用之前提到的检测方法来实践:

// 接上例
// 检测自身属性
console.log(user2.hasOwnProperty("name")); // true
console.log(user2.hasOwnProperty("print")); // false

// 检测原型属性
console.log("name" in user2); // true
console.log("print" in user2); // true

可以看到实例中自身只有 name 属性,而 print 方法确实在其原型链中可以被找到。

类的静态方法/属性

通过关键字 static 可以声明一个静态方法/属性。和其他语言一样,静态方法/属性只会挂载到类中,而不会通过类创建的实例调用。

class User {
  static type = "JZ";

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

  show() {
    console.log("show: " + this.name);
  }

  static print() {
    console.log("static print by: " + this.type); // 静态方法里的 this 指向类本身
  }
}

let user = new User("jeremyjone");

// 实例调用类方法
user.print(); // 报错。找不到对象方法

// 使用类方法
User.print(); // static print by: JZ

类的继承

ES6 中通过 extends 关键字来实现类之间的继承。

// 接上例
class Child extends User {} // 最基本的继承

let child = new Child("child jz");
child.show(); // show: child jz

同时,静态属性/方法是会被继承的。

// 接上例
Child.print(); // static print by: JZ

super 关键字

在继承过程中,经常会看到 super 关键字,它有两个作用:

  • 1、子类调用构造函数 constructor 时,必须在构造函数内部先调用 super 关键字,然后才可以使用 this 对象。
  • 2、子类同名方法会覆盖父类方法,这时使用 super 关键字可以调用父类方法。

构造函数中使用 super

// 接上例
// 错误示例
class Child2 extends User {
  constructor() {} // 空
}

// 当子类调用了构造函数,却没有在内部使用 super,新建实例会报错
let child2 = new Child2("c2"); // 报错

《真丶深入理解 JavaScript 原型和原型链(四):ES6中的class》

所以需要在使用到 this 地方之前,调用一下 super

// 接上例
// 正确示例
class Child2 extends User {
  constructor(name) {
    super(name);
  }
}

let child2 = new Child2("c2"); // 正确

调用父级属性/方法

作为对父类的扩展,有时候需要覆写父类,但是又需要用到父类的功能,这时可以在子类中使用 super 调用父类功能作为子类方法的一部分。

// 接上例
class Child3 extends User {
  show() {
    console.log("Blessings from child3");
    super.show();
  }
}

let child3 = new Child3("c3");
child3.show();
// Blessings from child3
// show: c3

super 指向哪里

ES6 给我们提供的 super 会指向父级的原型。所以我们可以通过 super 找到其原型链中的所有属性/方法,但是无法找到 static 方法/属性。

举一个例子,我们可以将上面的例子转换为:

// 修改上例
class Child3 extends User {
  show() {
    console.log("Blessings from child3");
    // super.show();
    // 转换为如下方式:
    User.prototype.show.call(this, this.name);
    // 或者:
    this.__proto__.show.call(this, this.name);
  }
}

let child3 = new Child3("c3");
child3.show();
// Blessings from child3
// show: c3

从上面可以看到,其实 super 就是指向了原型,同时给我们提供了 this 的指向。

总结

到此为止,基于JS的原型和原型链的内容基本就总结完毕了,学习 JS 一定要搞明白原型的内容。JS 的灵活之处就在于原型和原型链,其继承的方式也基于此,之后的类的概念也是在此基础上的。

总之,这段内容还是要多多练习领悟,才能通透。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注