基于原型的继承
在编程中,有时候我们想要对一些代码扩展一些东西。
如:
- 有个
user对象及属性和方法。 - 希望将
admin和guest作为基于user稍加修改的变体。 - 想要重用
user中的内容,而不是复制/重新实现它的方法,只是在这基础上构建一个新的对象。
原型继承便能够实现这样的需求!
Prototype
在JavaScript语言中,对象有个特殊的隐藏属性
Prototype,这个属性要么为null,要么就是对另一个对象的引用。该对象被称为“原型”。

当想要从 object 中读取一些原本没有的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这种行为称为“原型继承”。
原型在内部是被隐藏的,但我们可以借由 __proto__ 找到 Prototype 如下:
let animal = {eats: true}let rabbit = {jumps: true}rabbit.__protp__ = animal;//将 animal 设置为 rabbit 的原型。console.log(rabbit.eats);//true,rabbit里面没有eats,就顺着Prototype引用寻找console.log(rabbit.jumps);//true
在这我们可以说 animal 是 rabbit 的原型,或者 rabbit 的原型是从 animal 继承而来的。
animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这种属性被称为“继承”。
注意:
__proto__的值可以是对象,也可以是null。其他类型都会被忽略。- 只能有一个
Prototype。一个对象不能从其他两个对象获得继承。 - 引用不能形成闭环。如果我们试图在一个闭环中分配
__proto__,JavaScript 会抛出错误。
写入不使用原型
- 原型仅用于读取属性。
- 对于写入/删除操作可以直接在对象上进行。
- 写入/删除操作不会影响原型对象,只会影响当前对象。
let animal = {eats: true,walk() {/* rabbit 不会使用此方法 */}};let rabbit = {__proto__: animal};rabbit.walk = function() {console.log('输出成功')};rabbit.walk(); // 输出成功
上面的代码,rabbit.walk() 将立即在对象中找到该方法并执行,而无需使用原型。
this的值
例子:
let user = {name: "John",surname: "Smith",set fullName(value) {[this.name, this.surname] = value.split(" ");},get fullName() {return `${this.name} ${this.surname}`;}};let admin = {__proto__: user,isAdmin: true};alert(admin.fullName); // John Smith (*)// setter triggers!admin.fullName = "Alice Cooper"; // (**)
上面的代码:
- 在
set fullName(value)中this的值是什么? - 属性
this.name和this.surname被写在哪里:在user还是admin?
因为, this 根本就不受原型的影响。在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。
for…in 循环
for..in 循环也会迭代继承的属性。
如:
let animal = {eats: true};let rabbit = {jumps: true,__proto__: animal};// Object.keys 只返回自己的 keyalert(Object.keys(rabbit)); // jumps// for..in 会遍历自己以及继承的键for(let prop in rabbit) alert(prop); // jumps,然后是 eats
基于Class的继承
类继承是一个类扩展另一个类的一种方式。
extends关键字用来创建一个普通类或者内建对象的子类。 继承的.prototype必须是一个Object或者null。
关键字extends
语法:
class ChildClass extends ParentClass { ... }//ChildClass 继承 ParentClass
例子:
假设有class Animal
class Animal {constructor(name) {this.name = name;}run() {console.log(`${this.name} 跑起来`);}stop() {console.log(`${this.name} 停下来`);}}let animal = new Animal("My animal");
图形化表示
创建一个子类 Rabbit :
class Rabbit extends Animal {hide() {console.log(`${this.name} 隐藏!`);}}let rabbit = new Rabbit("Rabbit");rabbit.run(); // Rabbit跑起来rabbit.hide(); //Rabbit隐藏
Class Rabbit 的对象可以访问例如 rabbit.hide() 等 Rabbit 的方法,还可以访问例如 rabbit.run() 等 Animal 的方法。
图示:
在查找 rabbit.run 方法的过程:
- 查找对象
rabbit(没有run)。 - 查找它的原型,即
Rabbit.prototype(有hide,但没有run)。 - 查找它的原型,即(由于
extends)Animal.prototype,在这儿找到了run方法。
和 Prototype 类似,class的 class Child extends Parent 和 Child.prototype.__proto__=== Parent.prototype 差不多。
在子类调用父类方法
调用方式为使用super:
当不想重写父类方法,又想扩展父类的方法,就可以如下写发
//重写Rabbit类class Rabbit extends Animal {hide() {super.run() //调用父类的run方法 //##########################这行console.log(`${this.name} 隐藏!`);}}let rabbit = new Rabbit("Rabbit");rabbit.hide() //Rabbit 跑起来 //Rabbit 隐藏!
同时需要注意,箭头函数是没有自己的super,如下:
//重写Rabbit类//例子1:如果使用了箭头函数class Rabbit extends Animal {hide() {setTimeout(() => {//代码没有报错super.run() //调用父类的run方法 //##########################这行}, 1000)....}}//例子2:如果使用了普通函数class Rabbit extends Animal {hide() {setTimeout(function(){//代码报错: 意料之外的 supersuper.run() //调用父类的run方法 //##########################这行}, 1000)....}}
在子类获取父类的constroctor
同样使用super:
//重写Rabbit类class Rabbit extends Animal {constroctor(name, sex) {super(name) //特别注意,super必须在this的上面 //##########################这行this.sex = sex}}
