js继承

126 阅读13分钟

继承

ES5继承

ES5的五种种继承方式:

  • 原型链继承
  • 构造函数继承,又叫对象冒充继承
  • 组合继承
  • 原型式继承
  • 寄生式继承(重要,完美继承)

1.原型链继承

将父类的实例化对象赋值给子类的原型上实现的继承

原型链继承是JavaScript中最基本的继承方式。每个对象都有一个原型对象,通过原型链将属性和方法沿着对象链传递下来。在原型链继承中,通过将子构造函数的原型对象指向父构造函数的实例,实现了继承。这意味着子对象可以访问父对象原型链上的属性和方法。

      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10;//给构造函数添加了原型属性

      function Per(){
        this.name = "xixi";
      }
      Per.prototype = new Person();//主要
      var per1 = new Per();
      console.log(per1.age);// 10
      // instanceof 判断元素是否在另一个元素的原型链上
      // per1 继承了Person的属性,返回true
      console.log(per1 instanceof Person); // true
  • 重点:让新实例的原型等于父类的实例。
  • 特点:
    • 1.原型链继承的缺点是,所有子对象共享同一个原型对象,对原型对象的修改会影响到所有子对象。
    • 2.实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
  • 缺点:
    • 1.新实例无法向父类构造函数传参。
    • 2.继承单一。
    • 3.所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)

2.构造函数继承(经典继承)

构造函数继承是通过在子构造函数中调用父构造函数来实现继承。在构造函数继承中,通过在子构造函数中使用**call()apply()**方法,将父构造函数的上下文设置为子对象的上下文,从而实现继承。

构造函数继承的本质:在子类构造函数内部调用父类构造函数

  • 通过callapply改变this指向,并执行了父类的构造函数
  • 缺点:只能继承超类的构造函数,无法继承原型链上的方法
function Person(name) {
  this.name = name
  this.color = ['red', 'yellow', 'blue']
}

function Student (name) {
  Person.call(this, name)
}

let stu1 = new Student('guoguo')
stu1.color.push('green')
console.log(stu1) 
// {name:'guoguoguo', color: ['red', 'yellow', 'blue', 'green']}

let stu2 = new Student('yuyuyu')
console.log(stu2)  
// {name:'guoguoguo', color: ['red', 'yellow', 'blue']}
      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10; //给构造函数添加了原型属性
      
      
      //构造函数继承
      function Con() {
        Person.call(this, 'aaa'); //重点
        this.age = 12;
      }

      var con1 = new Con();
      console.log('con1.name', con1.name); //aaa
      console.log('con1.age', con1.age); //12
      console.log(con1 instanceof Person); // false
  • 重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
  • 特点:
    • 1、只继承了父类构造函数的属性,没有继承父类原型的属性。
    • 2、解决了原型链继承缺点1、2、3。
    • 3、可以继承多个构造函数属性(call多个)。
    • 4、在子实例中可向父实例传参。
  • 缺点:
    • 1、只能继承父类构造函数的属性。
    • 2、无法实现构造函数的复用。(每次用每次都要重新调用)
    • 3、每个新实例都有父类构造函数的副本,臃肿。

经典继承可以解决原型链继承不能向父类传参和实例共享引用类型值问题,但是它的缺点:实例无法共享父类原型对象上的方法和属性

3.组合继承(原型链继承和构造函数继承)(常用)

前面的两种继承(构造函数继承,原型链继承)各有特点,把这两种继承组合起来称为组合继承

组合继承结合了原型链继承构造函数继承,既继承了父构造函数的属性,又继承了父构造函数原型对象上的方法。在组合继承中,通过调用父构造函数的方式实现属性的继承,通过将子构造函数的原型对象指向父构造函数的实例实现方法的继承。

      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10; //给构造函数添加了原型属性
      
      
      function SubType(name){
         Person.call(this, 'aaa'); //借用构造函数模式
      }
      SubType.prototype =  new Person();//原型链继承
      var sub = new SubType("gar");
      console.log(sub.name);//gar 继承了构造函数属性
      console.log(sub.age);// 10 继承了父类原型的属性
  • 重点:结合了两种模式的优点,传参和复用
  • 特点:
    • 1、可以继承父类原型上的属性,可以传参,可复用。
    • 2、每个新实例引入的构造函数属性是私有的。
  • 缺点:调用了两次父类构造函数(耗内存)(一次是在设置原型时,一次是在创建子对象时),子类的构造函数会代替原型上的那个父类构造函数。

4.原型式继承

表面

原型式继承是通过使用一个临时构造函数和 Object.create() 方法来实现继承。

var parent = {
  name: 'Parent',
  sayHello: function() {
    console.log('Hello');
  }
};

var child = Object.create(parent);
console.log(child.name); // Parent
child.sayHello(); // Hello

在这个例子中,我们创建了一个parent对象,然后使用 Object.create() 方法创建了一个新对象child,并将其原型对象指向parent对象,实现了继承。原型式继承的本质是创建一个新对象,将其原型对象指向另一个已有的对象。这种方式可以实现属性和方法的继承,但是不能传递构造函数的参数。

Object.create() 是把现有对象的属性,挂到新建对象的原型上,新建对象为空对象

ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。

let person = {
  name: 'mjy',
  age: 19,
  hoby: ['唱', '跳'],
  showName() {
    console.log('my name is: ', this.name)
  }
}
 
let child1 = Object.create(person)
child1.name = 'xxt'
child1.hoby.push('rap')
let child2 = Object.create(person)
 
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap']

  • 优点:  不需要单独创建构造函数。
  • 缺点:  属性中包含的引用值始终会在相关对象间共享,子类实例不能向父类传参

原理

      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10; //给构造函数添加了原型属性
      
     // 原型式继承
      function content(obj){
        function F(){}
        F.prototype = obj;//继承了传入的参数
        return new F();//返回函数对象
      }
      var sup = new Person();// 拿到父类的实例
      var sup1 = content(sup);
      console.log(sup1.age);// 10 继承了父类函数的属性
  • 重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
  • 特点:类似于复制一个对象,用函数来包装。
  • 缺点:
    • 1、所有实例都会继承原型上的属性。
    • 2、无法实现复用。(新实例属性都是后面添加的)

5.寄生式继承

寄生式继承的思路与(寄生) 原型式继承 和 工厂模式 似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}
 
function createAnother(obj) {
  let clone = objectCopy(obj);
  clone.showName = function () {
    console.log('my name is:', this.name);
  };
  return clone;
}
 
let person = {
     name: "mjy",
     age: 18,
     hoby: ['唱', '跳']
}
 
let child1 = createAnother(person);
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
child1.showName(); // my name is: mjy
 
let child2 = createAnother(person);
console.log(child2.hoby); // ['唱', '跳', 'rap']

      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10; //给构造函数添加了原型属性
      

      // 寄生式继承
      function content(obj){
        function F(){}
        F.prototype = obj;//继承了传入的参数
        return new F();//返回函数对象
      }
      var sup = new Person();// 拿到父类的实例
      // 以上是原型式继承,给原型式继承再套个壳子传递参数
      function subObject(obj){
        var sub = content(obj);
        sub.name = "gar";
        return sub;
      }
      var sup2 = subObject(sup);
      // 这个函数经过声明之后就成了可增添属性的对象
      console.log(typeof subObject);// function
      console.log(typeof sup2);//object
      console.log(sup2.name);//gar ,继承了sub的属性
  • 重点:就是给原型式继承外面套了个壳子。
  • 优点:
    • 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
    • 写法简单,不需要单独创建构造函数。
  • 缺点:
    • 没用到原型,无法复用。
    • 通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.

6.寄生组合式继承(常用)

组合继承是常用的经典继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数;一次是在创建子类型的时候,一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

  • 寄生:在函数内返回对象然后调用
  • 组合:
    • 1、函数的原型等于另一个实例。
    • 2、在函数中用apply或者call引入另一个构造函数,可传参
  • 重点:修复了组合继承的问题
  • 优点:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
  • 缺点:代码复杂
function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}
 
function inheritPrototype(child, parent) {
  let prototype = objectCopy(parent.prototype);
  prototype.constructor = child;
  Child.prototype = prototype;
}
 
function Parent(name) {
  this.name = name;
  this.hoby = ['唱', '跳']
}
 
Parent.prototype.showName = function () {
  console.log('my name is:', this.name);
}
 
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
 
inheritPrototype(Child, Parent);
Child.prototype.showAge = function () {
  console.log('my age is:', this.age);
}
 
let child1 = new Child("mjy", 18);
child1.showAge(); // 18
child1.showName(); // mjy
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
 
let child2 = new Child("yl", 18);
child2.showAge(); // 18
child2.showName(); // yl
console.log(child2.hoby); // ['唱', '跳']
      // 父类
      function Person(name) {
        // 给构造函数添加了参数
        this.name = name;
        this.sum = function () {
          console.log('this.name', this.name);
        };
      }
      Person.prototype.age = 10; //给构造函数添加了原型属性
      // 寄生式继承
      function content(obj) {
        function F() {}
        F.prototype = obj; //继承了传入的参数
        return new F(); //返回函数对象
      }
      // content 就是F实例的另一种表示法
      var con = content(Person.prototype);
      // con 实例(F实例)的原型继承了父类函数的原型
      // 上速更像是原型链继承,只不过只继承了原型属性

      //组合
      function Sub() {
        Person.call(this); //这个继承了父类的构造函数的属性
      } //解决了组合式两次调用构造函数属性的缺点

      //重点
      Sub.prototype = con; //继承了con实例
      con.constructor = Sub; //一定要修复实例
      var sub1 = new Sub();

      //Sub的实例就继承了构造函数属性,父类实例,con的函数属性
      console.log(sub1.age); //10

结语:

继承这些知识点与其说是对象的继承,更像是函数的功能用法,如何用函数做到复用,组合,这些和使用继承的思考是一样的。上述几个继承的方法都可以手动修复他们的缺点,但就是多了这个手动修复就变成了另一种继承模式。
这些继承模式的学习重点是学它们的思想,不然你会在coding书本上的例子的时候,会觉得明明可以直接继承为什么还要搞这么麻烦。就像原型式继承它用函数复制了内部对象的一个副本,这样不仅可以继承内部对象的属性,还能把函数(对象,来源内部对象的返回)随意调用,给它们添加属性,改个参数就可以改变原型对象,而这些新增的属性也不会相互影响。

总结ES5继承

ES5 中,我们使用组合继承,通过原型链继承继承原型上的公共属性和公共方法,而通过经典继承函数继承实例属性。继承中,如果不把子类的构造函数再指回自身构造函数,就会很混乱,子类的类型居然是父类类型。如下案例,如果不使用该语句:Dog.prototype.constructor= Dog,那么Dog的实例居然是Animal

function Animal(name,age,weight){
  this.name=name
  this.age=age
  this.weight=weight
}

Animal.prototype={
  constructor:Animal,
  sayName(){
    console.log(this.name);
  }
}

function Dog(name,age,weight,type){
  // 借用构造函数继承属性
  Animal.call(this,name,age,weight)
  this.type=type
}

// 继承父类的方法
Dog.prototype=new Animal()

// 将子类构造函数指回自身构造函数
Dog.prototype.constructor= Dog

let d1 = new Dog('旺财',5,'6kg','柴犬')
console.log(d1);
d1.sayName()

ES5对比ES6的继承,麻烦太多太多,以后的实际工作也不会使用;

但是在面试的时候,面试官可能会问,多学一点总没错。

ES6继承

参考链接:es6.ruanyifeng.com/#docs/class…

在ES6中引入了类的概念,通过class关键字和extends关键字可以实现类的继承。

class Parent {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log('Hello');
  }
}

class Child extends Parent {
  constructor(name) {
    super(name);
  }
}

const child = new Child('Child');
console.log(child.name); // Child
child.sayHello(); // Hello

这个例子中,我们定义了一个Parent类,通过extends关键字实现子类Child对父类Parent的继承。子类使用super关键字调用父类的构造函数,并可以访问父类的属性和方法。ES6类继承提供了更加语法简洁和面向对象的继承方式。

ES5继承区别ES6继承

继承机制

ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。

ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。

这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。

是否可继承原生的构造函数

ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上。 由于父类的内部属性无法获取,导致无法继承原生的构造函数

ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

ES6 允许继承原生构造函数定义子类

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。 这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。

这就使的在es6之中extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。