今天最后总结一下 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
关键字标明类属性/方法,它们无法在实例中使用,而是通过类直接调用的。
类与原型的关系
为了深入理解,首先来看一下它们的原型结构:
看上去差不多,只是一个标记为函数,一个标记为类。
测试一下发现:
// 接上例
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"); // 报错
所以需要在使用到 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 的灵活之处就在于原型和原型链,其继承的方式也基于此,之后的类的概念也是在此基础上的。
总之,这段内容还是要多多练习领悟,才能通透。
文章评论