什么是继承?为何那么多种?一步步来!

前言

继承是 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
复制代码

可以分析下过程:

  1. 首先定义了 ParentgetParent,该方法返回对象的 property
  2. 接着,定义了 ChildgetChild,该方法返回对象的 property
  3. 然后,实例化 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 的结构:

捕获.PNG

可以看出继承的函数在原型上,而属性是对象独有的。

然而父类实例对象我们并没用到,并且处于无法访问的状态(子类属性屏蔽了同名的原型属性)。怎么办?

原型继承

因为我们只需要父类的原型,而非父类对象,所以只需取得父类原型并赋值给子类原型便可。但简单粗暴的:

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 的方法。

MDN Object.create()

我们看看现在 Child 实例对象的结构:

捕获.PNG

我们还可以利用 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();//我是老王
复制代码

MDN Object.assign()

结尾

欢迎大家一起交流,如果代码有什么错误恳请指正啦~?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享