JavaScript原型及继承

原型

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象叫做原型对象,用途是包含可以由特定类型的所有实例共享的属性和方法。

如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的对象原型。不知道你是不是明白这个字面意思,我第一次看是不怎么明白,从头捋一遍创建实例的过程:创建对象的时候,调用构造函数,构造函数有一个prototype属性,是一个指针,指向原型对象,这里解释一下指针,指针是编程语言中的一个概念,它存储的是一个内存地址,可以通过指针获取它指向内存地址的值,也就是我们可以通过prototype获取它指向的原型对象的属性和值,不严谨的讲,prototype也是可以叫做实例的原型对象。

使用对象原型的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数里面定义对象实例的信息,而是可以将这些信息直接添加到原型上面。

如下面例子所示:


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

   var person = new Person();
   person.sayName();//Lele

prototype原型对象里包含了所有实例共享的属性和方法,默认情况下,所有的原型对象都会默认获得一个constructor(构造函数)属性,这个属性包含一个指向当前prototype属性所在函数的指针,拿上面的例子来说就是Person.prototype.constructer指向Person。而通过这个构造函数,我们还可以继续为原型对象添加对象和方法。

创建自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其它方法都是从Object继承而来的,当调用构造函数创建一个新实例之后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中将这个指针叫做[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性_proto/_;而在其他实现中,这个属性对脚本是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型之间,以前面使用的Person构造函数和Person.prototype创建实例的代码为例,图1-1展示了各个对象之间的关系:
图1-1

继承

前面我们讲了原型的概念,另外,在ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型去继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含指向原型对象的内部指针。那么,如果我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。实现原型链有一种基本模式,其代码大致如下:

  function SuperType () {
      this.property = true
  }

  SuperType.prototype.getSuperValue = function () {
      return this.property
  }

  function SubType() {
      this.subproperty = false
  }
  //继承了superType
  SubType.prototype = new SuperType();

  var instance = new SubType();
  console.log(instance.getSuperValue());//property
  console.log(instance.subproperty)//subproperty

以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是SubType继承了SuperType。而继承是通过创建SuperType实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确立了继承关系之后,我们给SubType.prototype添加一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。

在上面的代码中,我们没有使用SubType默认提供的原型;而是给它换了一个新原型;这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的全部属性和方法,而且其内部还有一个指针,指向SuperType的原型。最终结果就是这样的:instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法依然在SuperType.prototype中,但property则位于Subtype.prototype中。这是因为prototype是一个实例属性,而getSuperValue()则是一个原型方法。既然SubType.prototype现在是SuperType的实例,那么property当然就位于该实例中了。此外需要注意的是instance.constuctor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写的缘故。

通过实现原型链,本质上扩展了原型搜索机制,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向前。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:

  1. 搜索实例
  2. 搜索SuberTyp.prototype
  3. 搜索SuperType.prototype

最后一步才会找到该方法,在找不到属性或者方法的情况下,搜索过程总是要一环一环的前行到原型链末端才会停下来。

默认的原型

我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的,所有函数的默认原型都是Object的实例,因此默认原型都包含一个内部指针,指向Object.prototype.

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系,第一种方法是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。实例代码如下:

  console.log(instance instanceof Object); //true
  console.log(instance instanceof SuperType); //true
  console.log(instance instanceof SubType); //true

第二种方式是使用isPrototypeOf,同样,只要是原型链中出现过的原型,都可以说是该原型链所派生出来的实例的原型,因此isPrototypeOf()方法也会返回true

  console.log(Object.prototype.isPrototypeOf(instance));
  console.log(SuperType.prototype.isPrototypeOf(instance));
  console.log(SubType.prototype.isPrototypeOf(instance));

谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型的语句之后。

 function SuperType () {
      this.property = true
  }

  SuperType.prototype.getSuperValue = function () {
      return this.property
  }

  function SubType() {
      this.subproperty = false
  }

  //继承了superType
  SubType.prototype = new SuperType();
  SubType.prototype.getValue = function () {
      return this.subproperty
  };
  SubType.prototype.getSuperValue = function () {
      return this.subproperty;
  }
  var superinstance = new SuperType()
  var instance = new SubType();
  console.log(instance.getValue());
  console.log(instance.getSuperValue());
  console.log(superinstance.getSuperValue())

从代码的运行结果可以看出,子类的getSuperValue()方法已经覆盖了超类。但在超类的示例中,调用的依然是超类的方法,因为在子类中定义和超类相同的方法时,如果在子类实例级别调用该方法,会屏蔽掉超类里面的方法,还有一点需要注意的是,不能使用字面量创建原型方法,因为会重写原型链。

原型链问题

原型链虽然强大,但是它也存在一些问题。其中最大的问题来自包含引用类型值的原型。因为原型是所有实例共享的,所以实例不会有自己独立的属性。

构造函数继承

构造函数与原型继承是截然不同的一种类型,原型链继承是共享所有的属性和方法,而构造函数则是重写所有的属性和方法,我们先来看一下代码:

  function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
  }
  
  //实现构造函数继承
  var anoterPerson = function (name, age, job) {
      Person.call(this, name, age, job)
  }
  
  var Person1 = new Person('Lele', 12, 'student');
  var Person2 = new anoterPerson('Mary', 13, 'student');
  
  console.log(Person1.name); //Lele
  console.log(Person2.name); // Mary

从代码我们可以看出,这种继承方式重写了父类的构造函数,相当于每一次创建新实例,都要把构造函数的内容重新执行一下,包括可以共用的方法,这种方式会造成内存的浪费。

组合继承

正常情况下,继承包括不共享的属性和可共享的方法,显然,单一的原型继承或者构造函数继承都有一定的局限性,那么有没有一种方式,能够结合原型继承和构造函数继承的优点,在实现继承时只重写需要重写的部分呢?有的,就是我们马上要介绍的组合继承,先看一段代码:

  function Person(name, age, job) {
     this.name = name;
     this.age = age;
     this.job =job;
  }

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

  function anoterPerson(name, age, job) {
      //继承属性
      Person.call(this, name, age, job)
  }

  //继承方法
  anoterPerson.prototype = new Person();

  var Person1 = new anoterPerson('Lele', 12, 'student');
  var Person2 = new anoterPerson('Mary', 13, 'student');

  Person1.sayName() //Lele
  Person2.sayName() //Mary

构造函数Person定义了三个属性和一个方法,anoterPerson通过构造函数继承的方式继承了Person的构造函数属性,通过原型链继承的方式继承了Person的原型方法,实现了取构造函数继承和原型继承两种方法长处的继承。

原型式继承

道格拉斯.克罗克福德在2006年写了一篇文章,题目为Prototype Inheritance in JavaScript(JavaScript中的原型式继承),在这篇文章中,他介绍了一种实现继承的方法。这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建对象,同时还不必因此创建自定义类型,为了达到这个目的,他给出了如下函数:

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

在object()的内部,先创建了一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。来看下面的例子:

  var Person = {
     name: 'Nicholas',
     friends: ['Shelly', 'Court', 'Van']
  }

  var yetAnotherPerson = object(Person)

  yetAnotherPerson.name = 'mary';
  yetAnotherPerson.friends.push('Lele');
  console.log(yetAnotherPerson.friends) //"Shelly", "Court", "Van", "Lele"

  var yetAnotherPerson1 = object(Person)
  yetAnotherPerson1.friends.push('MM');

  console.log(yetAnotherPerson.friends) //"Shelly", "Court", "Van", "Lele", "MM"
  console.log(yetAnotherPerson1.friends) // "Shelly", "Court", "Van", "Lele", "MM"
  console.log(Person.friends) //"Shelly", "Court", "Van", "Lele", "MM"

这种原型式继承,必须有一个对象可以作为对象的基础,如果有这么一个对象的话,可以把它传给object()函数,然后再根据具体的需求加以修改,在例子 中,Person是一个基础对象,于是我们把它传入object()函数中,然后该函数就会返回一个新对象,这个新对象将Person作为原型,所以它的原型中就包含一个基本类型和一个引用类型,这就意味着Person.friends不仅属于Person所有,也被所有实例共享。

ECMAScript5通过新增Object.create()方法规范化了原型式继承,这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个作为新对象定义额外属性的对象,在传入一个对象的情况下,Object.creat()与object()方法的行为相同。

  var Person = {
     name: 'Nicholas',
     friends: ['Shelly', 'Court', 'Van']
  }

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

  var yetAnotherPerson = Object.create(Person)

  yetAnotherPerson.name = 'mary';
  yetAnotherPerson.friends.push('Lele');
  console.log(yetAnotherPerson.friends)

  var yetAnotherPerson1 = Object.create(Person,{name: {value: 'Grey'}})
  yetAnotherPerson1.friends.push('MM');

  console.log(yetAnotherPerson.friends)
  console.log(yetAnotherPerson1.name) //Grey
  console.log(Person.friends)

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,而且同样也是由克罗克福德推而广之的。寄生式继承的思路和寄生式构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后在像真的是它做了所有工作一样返回对象。一下代码是示范了寄生式继承模式:

  var Person = {
     name: 'Nicholas',
     friends: ['Shelly', 'Court', 'Van']
  }

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

  function crateAnoter(original) {
      var clone = object(original); //通过调用函数创建一个新对象
      clone.sayHi = function () { //以某种方式来增强对象
          console.log('hi')
      }
      return clone //返回这个对象
  }

  var personOne = new crateAnoter(Person);
  personOne.sayHi() //hi

在这个例子中,createAnother()函数接受了一个对象,也就是将要作为新对象基础的对象,然后把这个对象赋值给objcet()函数,将返回的结果赋值给clone,再为clone添加新方法。例子中代码基于Person创建了一个新对象,personOne。新对象不仅具有Person的所有属性和方法,而且还有自己的sayHi()方法。

这主要是考虑对象而不是自定义类型和构造函数的情况下,寄生式继承模式时使用的object()函数不是必须的;任何能够返回新对象的函数都适用于此模式。

寄生组合继承

前面说过,组合继承是JavaScript最常用的继承模式;不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型的构造函数。一次是在创建子类型的原型的时候,一次是在子类型构造函数内部。代码如下:

  function Person(name, age, job) {
     this.name = name;
     this.age = age;
     this.job =job;
  }

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

  function anoterPerson(name, age, job) {
      //继承属性
      Person.call(this, name, age, job) //第一次调用Person
  }

  //继承方法
  anoterPerson.prototype = new Person(); //第二次调用Person

  var Person1 = new anoterPerson('Lele', 12, 'student');
  var Person2 = new anoterPerson('Mary', 13, 'student');

  Person1.sayName() //Lele
  Person2.sayName() //Mary

好在我们已经找到了解决这个问题的方法–寄生组合继承。
所谓的寄生组合继承,即通过借用构造函数来继承属性,通多原型链的混成形式来继承方法。基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非也就是一个超类原型的副本而已。本质上,就是使用寄生式继承来继承超类的原型,然后在将结果指定给子类型的原型。寄生式组合继承的基本模式如下:

 function inHeritPrototype(subType, superType) {
        var prototype = objcet(superType.prototype)//创建对象
        prototype.constructor = subType; //增强对象
        subType.prototype = prototype; //指定对象
    }

这个示例中,inHeritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数,子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor。最后一步将新创建的对象(即副本)赋值给子类型的原型。这样我们就可以调用inHeritPrototype()函数的语句,去替换前面例子中为子类型赋值的语句了,代码如下:


        <!--构造函数继承-->
    function objcet(objet) {
        var F = function () {}
        F.prototype = objet;
        return new F()
    }

    /*定义父类*/
    function superType(name, age) {
        this.name = name;
        this.age = age;
    }
    /*为父类添加方法*/
    superType.prototype.sayName = function () {
        console.log(this.name)
    }

    /*实现子类的构造函数继承*/
    function subType(name, age) {
        superType.call(this, name, age)
    }

    /*实现子类的原型属性继承*/
    function inHeritPrototype(subType, superType) {
        var prototype = objcet(superType.prototype)
        prototype.constructor = subType;
        subType.prototype = prototype;
    }

    inHeritPrototype(subType, superType);
    subType.prototype.sayAge = function () {
        console.log(this.age)
    }

    var person1 = new subType('Lele', 12);
    var person2 = new subType('Mary', 14);

    person1.sayName();
    person2.sayName();
    person1.sayAge();
    person2.sayAge();

猜你喜欢

转载自blog.csdn.net/DayDreamWMM/article/details/82597215