前言
继承是 JavaScript 的重要概念。尽管 ES6 提供了的 class 关键字来快速定义类并轻松实现类的继承。但了解继承的不同实现方式及分析原因是深入了解 JavaScript 的很好方式。
我们从比较简单的方式——通过原型实现继承——开始。
原型链继承
了解下原型
在 JavaScript 中,每个实例对象都有一个私有属性(称之为__proto__
)指向它的构造函数的原型对象(prototype
)。并且原型对象也有一个原型对象。这种关系可以不断上溯,直至某个原型对象为 null
结束。
与其他语言相似, JavaScript 中的大多数对象都是的 Object (原型链顶端)的实例。
并且,最重要的是,当尝试访问一对象的成员时,JavaScript 不仅会在当前对象上寻找,还会顺着对象的原型,原型的原型……直到找到匹配的成员或在原型链的末尾停止寻找。
所以,原型链是不是很像父类和子类的关系?我们可以做些文章:
//有关父类
function Parent() {
this.description = "Parent";
}
Parent.prototype.getParent = function() {
return this.description;
}
//有关子类
function Child() {
this.childDescription = "Child";
}
Child.prototype.getChild = function() {
return this.childDescription;
}
//将子类原型指向父类对象
Child.prototype = new Parent();
//展示继承
let child = new Child();
console.log(child.getParent()); // Parent
复制代码
可以分析下过程:
- 首先定义了
Parent
的getParent
,该方法返回对象的property
- 接着,定义了
Child
的getChild
,该方法返回对象的property
- 然后,实例化
Child
,然后输出Child 实例对象
的getParent
函数的返回值。
Child
虽然没有定义 getParent
方法,但是由于 Child
的原型指向了 Parent
对象。所以按照规则,Child
若没有成员将顺着原型链寻找,Child
的原型为 Parent
对象,在 Parent
对象中找到了 getParent
方法。
这种继承方式简便却也存在局限性。你可能也发现了,由于 Child
的原型对象是不变的,所以如果有多个 Child
对象对父类成员进行了修改,其将互相影响。
//有关父类
function Parent() {
this.schedule = ["起床","刷牙","吃饭"];
}
//有关子类
function Child() {
this.childSchedule = ["赖床","逃课"];
}
//将子类原型指向父类对象
Child.prototype = new Parent();
//子类操作父类对象将互相影响
let c1 = new Child();
c1.schedule.push("赚钱");
console.log(c1.schedule);//[ '起床', '刷牙', '吃饭', '赚钱' ]
let c2 = new Child();
//受到意外的影响
console.log(c2.schedule);//[ '起床', '刷牙', '吃饭', '赚钱' ]
c2.schedule.push("买玩具");
console.log(c2.schedule);//[ '起床', '刷牙', '吃饭', '赚钱', '买玩具' ]
复制代码
这个问题的原因是每个子类访问到的父类对象都是相同的,如果每个子类访问的成员都是独立存在的不就行了?
借用构造函数继承
我们可以借用父类的构造函数来初始化子类,这样父类拥有的成员子类也拥有并互相独立。
//有关父类
function Parent() {
this.schedule = ["起床","刷牙","吃饭"];
}
//有关子类
function Child() {
//将 this 强制绑定到 Parent 方法
Parent.call(this);
}
let c1 = new Child();
c1.schedule.push("赚钱");
console.log(c1.schedule);//[ '起床', '刷牙', '吃饭', '赚钱' ]
let c2 = new Child();
//不受另一个实例对象的影响
console.log(c2.schedule);//[ '起床', '刷牙', '吃饭' ]
c2.schedule.push("买玩具");
console.log(c2.schedule);//[ '起床', '刷牙', '吃饭', '买玩具' ]
复制代码
将 Parent 方法的 this 绑定为子类对象,很巧妙地实现了继承。
但它并不能继承到父类的原型对象,也不能发挥出 JavaScript 原型的优势(函数重复声明)。
那么结合目前两种方法的优势不就行了?
组合继承
思路很简单:用原型链实现对原型属性和方法的继承,用借用构造函数方法来实现属性的继承。
//有关父类
function Parent(name) {
this.name = name;
this.schedule = ["起床", "刷牙", "吃饭"];
}
//将父类的方法定义在原型上
Parent.prototype.introduce = function () {
console.log("我是" + this.name + ",今天准备" + this.schedule);
}
//有关子类
function Child(name) {
Parent.call(this,name);
}
//将子类原型对象指向父类对象
Child.prototype = new Parent();
// 重写原型后构造器会丢失
Child.prototype.constructor = Child;
Child.prototype.childIntroduce = function(){
console.log(this.name);
};
let c1 = new Child("小明");
c1.schedule.push("逃学");
c1.introduce();//我是小明,今天准备起床,刷牙,吃饭,逃学
c1.childIntroduce();//小明
let c2 = new Child("小红");
c2.schedule.push("买玩具");
c2.introduce();//我是小红,今天准备起床,刷牙,吃饭,买玩具
c2.childIntroduce();//小红
复制代码
看看 c1 的结构:
可以看出继承的函数在原型上,而属性是对象独有的。
然而父类实例对象我们并没用到,并且处于无法访问的状态(子类属性屏蔽了同名的原型属性)。怎么办?
原型继承
因为我们只需要父类的原型,而非父类对象,所以只需取得父类原型并赋值给子类原型便可。但简单粗暴的:
Child.prototype = Parent.prototype;
复制代码
并不能满足我们的需求。你想:既然子类原型直接指向父类原型,那对子类原型添加成员父类原型岂不是也会被更改?
可以是一个中间对象来构建继承,这样原型链就会为:
Child 对象
—> Child.prototype
—> 中间对象
—> 中间对象.prototype
—> Parent.prototype
—> Object.prototype
—> null
当我们修改 Child 的原型时,只会对中间对象进行修改并不会影响到父类原型。
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this,name);
}
// 中间对象
function F() {
}
//中间对象的原型指向父类对象
F.prototype = Parent.prototype;
//子类原型指向中间对象
Child.prototype = new F();
Child.prototype.constructor = Child;
//向子类原型添加方法
Child.prototype.introduce = function() {
console.log(this.name);
}
let c = new Child("小明");
c.introduce();//小明
let p = new Parent();
p.sayHello()//报错:p.sayHello is not a function
复制代码
我们用 Object.create
方法轻便地实现同样的效果。
function Parent(name) {
this.name = name;
this.schedule = ["起床", "刷牙", "吃饭"];
}
//将父类的方法定义在原型上
Parent.prototype.introduce = function () {
console.log("我是" + this.name + ",今天准备" + this.schedule);
}
//有关子类
function Child(name) {
Parent.call(this,name);
}
//将子类原型对象指向父类对象
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let c1 = new Child("小明");
c1.schedule.push("逃学");
c1.introduce();//我是小明,今天准备起床,刷牙,吃饭,逃学
let c2 = new Child("小红");
c2.schedule.push("买玩具");
c2.introduce();//我是小红,今天准备起床,刷牙,吃饭,买玩具
复制代码
Object.create
方法创建一个新对象,并利用现有的对象来创建的新对象的__proto__
。也就是说Object.assign
会把Parent
原型上的函数拷贝到Child
的原型上,使Child
的所有实例都可用Parent
的方法。
我们看看现在 Child
实例对象的结构:
我们还可以利用 Object.assign
实现多继承:
function Parent(name) {
this.name = name;
this.schedule = ["起床", "刷牙", "吃饭"];
}
//将父类的方法定义在原型上
Parent.prototype.introduce = function () {
console.log("我是" + this.name + ",今天准备" + this.schedule);
}
//其他的父类
function Wang(name) {
this.name = "王" + name;
}
Wang.prototype.say = function(){
console.log("我是老王");
}
//有关子类
function Child(name) {
Parent.call(this,name);
Wang.call(this,name)
}
Child.prototype = Object.create(Parent.prototype);
Object.assign(Child.prototype, Wang.prototype);
Child.prototype.constructor = Child;
let c1 = new Child("小明");
c1.schedule.push("逃学");
c1.introduce();//我是王小明,今天准备起床,刷牙,吃饭,逃学
c1.say();//我是老王
复制代码
结尾
欢迎大家一起交流,如果代码有什么错误恳请指正啦~?