JS继承那些事儿

737 阅读6分钟

前言

最近在排查巩固面试知识点的时候,发现继承,原型链这一块真是一生之敌,为了不让自己将来面临懵逼的困境,做个笔记总结一下。本文讲解JavaScript各种继承方式和优缺点,欢迎各位提出不同意见探讨。

什么是继承

继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。 面向对象的语言有一个标志,即拥有类的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象。在es6中,js终于引入了class类的概念(本质上还是对基于原型prototype的实现方式做了进一步的封装),现在的继承可以愈发贴近面向对象的写法。

//定义一个叫Animal的类
    class Animal {
        //构造函数constructor
        constructor(color){
            this.color = color;
        }
    }

在类语言中,对象基于模板来创建,然后由类来实例化对象。在没有引入类概念的时候,我们进行实例化对象其实更像是对另一个对象的克隆,被克隆的母体称为原型对象。

生成实例的方式 -- 构造函数

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。

所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。 如果这是一场面试,到这里的时候,面试官少不了要问你new操作符干了什么事情,如何自己实现一个_new来创建实例。不懂的同学可以回去复习这一块的知识~

 function Cat(name,color){
    this.name=name;
	   this.type = 'animal'
    this.color=color;
  }
    var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.name); // 大毛
  alert(cat1.color); // 黄色

仔细看上述代码,对于每一个实例对象,type属性都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。怎么避免这个问题呢?

Prototype 与 原型链

Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 JavaScript深入之从原型到原型链 点我复习

这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上,共享了同一份数据。

 function Cat(name,color){
    this.name = name;
    this.color = color;
  }
  Cat.prototype.type = "animal";
  
   var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // animal

原型链继承

function Parent () {
    this.name = 'kevin';
    this.names = ['kevin', 'daisy'];
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin
console.log(child1 instanceof Parent) // true

问题:

  • 父类应用类型的属性会被所有实例共享
// child2 的 names 也被修改了 因为实例上没有 引用在原型链上找到父类的names,并且修改了值
// child1.names = ['aaaa'] 不会影响child2 在实例上新增了names属性
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"] 
  • 创建实例的同时,不能向父类传参

构造函数继承

function Parent (age) {
    this.names = ['kevin', 'daisy'];
    this.age = age
}

function Child (age) {
    Parent.call(this,age);
}

var child1 = new Child(20);

child1.names.push('yayu');

console.log(child1.names,child1.age); // ["kevin", "daisy", "yayu"] 20
var child2 = new Child(30);

console.log(child2.names,child2.age); // ["kevin", "daisy"] 30

这种方式避免了原型链继承的引用类型属性被实例共享,同时也可以向父类传参数。 缺点:

  • 方法定义在构造函数中,每次创建实例都会执行一遍。

组合继承

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name) {
    Parent.call(this, name);

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy');

console.log(child2.name); // daisy
console.log(child2.colors); // ["red", "blue", "green"]

优点:

  • 融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

注意到多出了 Child.prototype.constructor = Child 更正了构造函数的指向。

  • 任何一个prototype对象都有一个constructor属性,指向它的构造函数。 如果没有Child.prototype = new Parent();这一行,Child.prototype.constructor是指向Child的;加了这一行以后,Child.prototype.constructor指向Parent
  1. 首先,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性,child1.constructor == Child.prototype.constructor
  2. 这样一来,child1.constructor == new Parent().constructor,实际上就是: child1.constructor == Parent,这显然会导致继承链的紊乱(child1是用构造函数Child生成的),因此我们必须手动纠正。

原型式继承

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

利用空对象作为中介,就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

缺点:

  • 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

寄生组合式继承

组合式继承虽然解决了我们的一些痛点,但是也并非完美的方案。比如:

  • 调用了两次父类构造函数
//一次是设置子类型实例的原型的时候:
Child.prototype = new Parent() 
//一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
等于 ==
Parent.call(this,name)

重复调用会带来什么问题呢?实例与原型上出现重复属性,比如child1Child.prototype都有一个colors属性,有没有办法可以避免这个问题呢,做到更加精简呢? 原型上属性冗余 我们为了让子类能够访问父类原型上的属性,使子类原型指向了父类的一个实例,从而在原型链上关联。既然出现这个问题是因为设置子类型原型时的调用,那么可不可以间接的让 Child.prototype 访问到 Parent.prototype

为什么说间接关联呢?

因为Child.prototype = Parent.prototype这样完成的继承是不合理的,父类子类的原型指向了同一个对象,任何对Child.prototype的修改,都会反映到Parent.prototype

看看如何实现,我们可以借助一个空对象可以做到优化:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('kevin', '18');

console.log(child1);

改变后: 最后我们封装一下这个继承方法:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

后记

欢迎各位共同讨论,本文参考了大佬的一些总结,如果觉得还行,请点赞讨论,谢谢@_@