基于原型的继承
在编程中,有时候我们想要对一些代码扩展一些东西。
如:
- 有个
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 只返回自己的 key
alert(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(){
//代码报错: 意料之外的 super
super.run() //调用父类的run方法 //##########################这行
}, 1000)
....
}
}
在子类获取父类的constroctor
同样使用super
:
//重写Rabbit类
class Rabbit extends Animal {
constroctor(name, sex) {
super(name) //特别注意,super必须在this的上面 //##########################这行
this.sex = sex
}
}