预备知识:原型链
虽然
ECMAScript6类表面上看起来可以支持面向对象编程,但实际上它背后使用仍然是原型和构造函数的概念
-----------------------------------------------------------------------《javascript高级程序设计》 javascript中的class关键字更加像是一种语法糖,但是其背后本质依旧是js的老办法,因此在学习class之前,最好将原型链的知识理一理。这里我给出我觉得必要的模型。
在该图例中,使用了Person构造函数创建了空对象实例new Person,它继承了Person的原型对象,同时Person的原型对象作为Object的一个实例,继续继承了Object的原型对象。
当我们在创建类,我们在创建什么?
看下列代码:
class Person {
constructor() {
// 定义在实例上的属性和方法
this.locate = () => { console.log('instance') }
this.name = 'wtk';
}
// 定义在原型上的方法
locate() {
console.log('prototype');
}
['new' + 'locate']() {
console.log('newlocate')
}
// 定义getter和setter
get name_() {
return this.name;
}
set name_(val) {
this.name = val;
}
// 定义在类上的方法
static getName() {
return this.name;
}
// 实例工厂 创建出实例
static create() {
return new Person();
}
}
Person.hobby = '跳绳';
Person.prototype.sex = 'male';
console.dir(new Person())
我们分别在类中的构造器、类中、类中加static关键字之后定义函数,然后打印观察
Person 实例
Person原型对象
Person class
综合看来,可以得出下列的关系图
在图中,我们清晰地发现开头的js的class的背后依然是原型的结论被验证了,虽然使用了class,但我们还是得到了原型链,只不过构造函数被换成了一个class!
class关键字是一个语法糖,它只是对class中不同地方定义的方法和属性进行了再分配:
constructor{} ——————>实例中的方法属性。在构造函数中定义的属性和方法,会被分配到实例中去。class{}内定义的函数 —————>原型对象中的方法。在class中不带static关键字中定义的方法,会被分配到原型对象中去class{}内+static定义的函数 —————>原型对象中的方法。在class中加上static关键字定义的方法,会被分配到class中去,变为类本身的一个方法。注意:可以给class定义迭代器和生成器,有兴趣可以试试。注意:不推荐在类中直接给原型对象和class定义属性,如果需要添加可以选择在类的定义后直接手动添加,见代码。
class的继承及super关键字
extends
通过使用extends关键字,我们可以实现继承。之前我们已经知道了class的本质是原型链,那么继承是如何实现为原型链的呢。
实现以下代码再打印:
class Person{};
class Man extends Person{};
console.log(new Man());
根据打印结果,得到关系图:
在图中可发现,犹如两个链条互相夹合,如果Man类继承了Person类:
- Person的实例会成为Man实例的原型对象。
- class Person会成为class Man的原型对象。
这就是extends的本质行为,这一举动就通过原型链将两个类给关联起来了。
super
super关键字用于访问和调用一个对象的父对象上的函数。 ——mdn
关于super有很多麻烦的限制,但是我觉得与其把视线关注在什么不应该做上,不如把视线关注在应该怎么做上:
- 如果要在派生类(子类)中使用super,那么就应该在派生类构造器的一开始就调用super()
- super()可以理解为是super.constructor()
- super.fun()等同于Object.getPrototypeOf(this).fun.call(this),这里的this指向的改变至关重要。
super,它和this一样具有薛定谔性质,只有这个函数被调用时,你才会知道这个super到底指的是什么。令人喜悦的是,我们有再分配原则,由此谁来调用这些函数一开始就确定了。
看以下代码:
class Vehicle {
constructor() {
this.tire = 4;
}
identifyPrototype(id) {
console.log(id, this)
}
static identifyClass(id) {
console.log(id, this)
}
};
// 技巧:super 只能访问其原型对象上的方法和属性
class Bus extends Vehicle {
constructor() {
// 为bus实例 super为new Vehicle()
super();
console.log('bus构造 访问class:', super.identifyClass);
console.log('bus构造 访问prototype:', super.identifyPrototype);
}
identifyPrototype(id) {
// 为bus的原型对象 super为Vehicle.prototype
console.log('busprototype:');
super.identifyPrototype(id);
// super.identifyClass(id)
}
static identifyClass(id) {
// 为Bus的原型对象 class Vehicle
console.log('busClass:');
super.identifyClass(id);
}
};
console.dir(new Bus());
const bus = new Bus();
// 在Vehicle的prototype上
bus.identifyPrototype('bus');
// Bus的__proto__指向Vehicle
Bus.identifyClass('bus')
- 创建Bus实例时,调用constructor(),可以预知到Bus实例的原型是Vehicle的实例,又因为Vehicle的实例可以根据原型链找到Vehicle.prototype,因此
Vehicle上.prototype的contructor和 identifyPrototype方法均可以在Bus的中constructor调用 - Bus的普通函数,也主要给Bus的实例调用,而Bus的实例的__proto__Vehicle实例,同上。
- static定义的函数,主要由Bus类直接调用,因此super主要为Vehicle,super可以调用Vehicle类上的静态方法。