js中的继承-原型与原型链

2019-10-13  本文已影响0人  jadefan

面向对象的语言支持两种继承方式,接口继承和实现继承
js无法实现接口继承,只支持实现继承,主要通过原型链来实现。
具体实现方式有以下几种:

原型链

每个函数都有prototype(原型)属性,指向一个原型对象
原型对象都包含一个指向构造函数的指针(constructor)
通过函数实例化的对象中,都有一个指向原型对象的内部指针

通过这种特性,可以让一个对象A的原型对象等于另一个实例化的对象B,对象B的原型对象等于另一个实例化的对象C,这样层层递进,就构成了实例与原型的链条,也就是原型链

    function Person() {
      this.name = 'wang'
    } 
    Person.prototype.sayName = function () {
      console.log(this.name);
    }

    function Man() {
      this.age = 18;
    }
    //继承:子类的原型等于父类的实例对象
    Man.prototype = new Person(); 
    Man.prototype.sayAge = function () {
      console.log(this.age);
    }

    var p1 = new Man();
    p1.sayName();  //wang
    p1.sayAge();   //18

代码中子类的方法都可以正常使用,这就涉及到了对象的原型搜索机制

当访问一个实例属性时,首先在实例中搜索该属性,如果没有找到,则会继续搜索实例的原型,如果没有找到并且原型对象还有原型,则继续向上搜索,直到搜索到没有原型对象为止,也就是截止到Object,因为js中所有对象都继承自Object。
这也就能解释为什么所有实例都有toString()等公用方法。

对象识别

利用原型链继承实现的子类可以识别属于子类、父类、Object

    console.log(p1 instanceof Object);  //true
    console.log(p1 instanceof Person);  //true
    console.log(p1 instanceof Man);     //true
派生判断
    console.log(Object.prototype.isPrototypeOf(p1));  //true
    console.log(Person.prototype.isPrototypeOf(p1));  //true
    console.log(Man.prototype.isPrototypeOf(p1));     //true
原型链的问题:
  1. 属性为引用类型时,由于共享机制,操作实例属性会影响其他实例
  2. 无法在不影响其他实例的情况下,给父类的构造函数传参
    function Person() {
      this.name = 'wang'
      this.friends = ['zhang', 'liu']
    } 

    function Man() {
      this.age = 18;
    }
    //继承:子类的原型等于父类的实例对象
    Man.prototype = new Person();

    var p1 = new Man();
    var p2 = new Man();

    console.log(p1.friends);  //["zhang", "liu"]
    p2.friends.push('li');
    console.log(p1.friends);  //["zhang", "liu", "li"]

可以看到数组属性受到影响,所以实践中很少单独使用原型链。

借用构造函数

也叫伪造对象或经典继承,为解决原型链不足而生

基本思想:在子类构造函数内部调用父类构造函数

    function Person(name) {
      this.name = name;
      this.friends = ['zhang', 'liu']
    }

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

    var p1 = new Man('wang');
    var p2 = new Man('zhang');
    console.log(p1.name);  //wang
    console.log(p2.name);  //zhang
    console.log(p1.friends);  //["zhang", "liu"]
    p2.friends.push('li');
    console.log(p2.friends);  //["zhang", "liu", "li"]
    console.log(p1.friends);  //["zhang", "liu"]

可以看出即可以给父类的构造函数传参,引用类型的属性又互不影响

问题

如果仅用借用构造函数,就会碰到构造函数的共性问题,无法将函数复用造成资源浪费,所以借用构造函数的技术也很少单独使用。

组合继承

也叫伪经典继承,将原型链和借用构造函数组合来用,发挥二者之长
使用原型链实现对原型属性和方法的继承
使用借用构造函数实现对实例属性的继承

    function Person(name) {
      this.name = name;
      this.friends = ['zhang', 'liu']
    }
    Person.prototype.sayName = function () {
      console.log(this.name);
    } 
    
    function Man(name, age) {
      //用构造函数继承属性
      Person.call(this, name);
      this.age = age;
    }
    //用原型链继承实例
    Man.prototype = new Person();
    Man.prototype.sayAge = function () {
      console.log(this.age);
    }

    var p1 = new Man('wang', 18);
    console.log(p1.name); //wang
    p1.sayName()  //wang
    p1.sayAge();  //18

    console.log(p1.friends);  //["zhang", "liu"]
    console.log(p1.__proto__.friends);  //["zhang", "liu"]

代码中用到了2种方式一起实现了继承
通过对象的__proto__属性,可以访问到对象的原型
可以看到实例对象和实例的原型对象同时存在父类属性和方法
组合继承避免了各自的缺陷,融合了优点,成为最常用的继承模式
但也有不足之处:

...
Person.call(this, name);  //第二次执行Person()
...
Man.prototype = new Person();  //第一次执行Person()

组合继承会指向两次父类的构造函数,在性能上不够理想
所以可以采用寄生组合式继承优化实现

在不同场景下,还有其他可选择的轻量级继承方式

原型式继承

核心思想:原型可以基于已有对象创建新对象,同时不必创建自定义类型

    function object(obj) {
      function Fn() { }
      Fn.prototype = obj;
      return new Fn();
    }

在函数内部,先创建了一个临时的构造函数,再将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质是执行了传入对象的浅复制。

    var person = {
      name: 'wang',
    } 
    var p1 = object(person);
    console.log(p1.name);

在ES5中通过新增Object.create()方法规范了原型式继承

    var person = {
      name: 'wang',
      friends: ['zhang', 'liu']
    }
    var p1 = Object.create(person);
    console.log(p1.friends);  //["zhang", "liu"]
    var p2 = Object.create(person);
    p2.friends.push('li');
    console.log(p1.friends);  //["zhang", "liu", "li"]

通过Object.create()可实现原型式继承
这种方式适用于让一个对象与另一个对象保持类似的情况,可不用构造函数
问题:可以看出,存在引用类型的属性的共享问题,也就是会被其它实例修改

寄生式继承

实现思路:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象

    var person = { 
      name: 'wang',
      friends: ['zhang', 'liu']
    }
    function createObject(obj){
      var copy = Object.create(obj);
      copy.sayName = function(){
        console.log(this.name);
      }
      return copy;
    }

    var p1 = createObject(person);
    p1.sayName();  //wang

类似寄生构造函数和工厂模式,
适用于:主要考虑对象而不是自定义类型和构造函数的情况
问题:在函数体内部定义方法,会无法使函数复用而降低效率

寄生组合式继承

针对组合式继承模式指向两次父类构造函数的不足,来优化

//实现父类原型拷贝副本给子类
    function inheritPrototype(subObj, superObj) {
      var prototype = Object.create(superObj.prototype);  //创建对象
      prototype.constructor = subObj;                     //增强对象
      subObj.prototype = prototype;                       //指定对象
    }

    function Person(name) {
      this.name = name;
      this.friends = ['zhang', 'liu']
    }
    Person.prototype.sayName = function () {
      console.log(this.name);
    }

    function Man(name, age) {
      //用构造函数继承属性,只需执行一次Person()
      Person.call(this, name);
      this.age = age;
    } 

    //Man.prototype = new Person();
    inheritPrototype(Man, Person);       //拷贝父类原型的副本,无需执行构造函数
    Man.prototype.sayAge = function () {
      console.log(this.age);
    }
 
    var p1 = new Man('wang', 18);
    console.log(p1.name); //wang
    p1.sayName()  //wang

寄生组合式继承是引用类型最理想的继承方式。

上一篇下一篇

猜你喜欢

热点阅读