JavaScript的原型继承六种方式

28次阅读

共计 8616 个字符,预计需要花费 22 分钟才能阅读完成。

原型链继承

特点:
  • 基本思想:利用原型链让子类继承父类的属性和方法
  function Parent(name,country) {
    this.name = name
    this.sex = 'male'
    this.country = country
    this.identities = ["philosopher", "scientist", "artist"]
    this.sayName = function() {return 'I am a' + this.name}
  }
  Parent.prototype.honor = 'Champion'
  Parent.prototype.getPersonValue = function() {return this.name}
  function Child(name,city) {
    this.name = name
    this.age = 24
    this.city = city
  }
  // Child 的原型指向 Parent 的实例
  // 意味着 Child 的原型拥有 Parent 的实例的全部属性和方法
  // 也意味着 Child 的原型的__proto__属性指向 Parent 的原型
  // 所以最后 根据原型链的规则,Child 的实例拥有 Parent 实例的全部属性和方法,以及 Parent 的原型的全部属性和方法
  Child.prototype = new Parent('father')
  // 此时 Child 的原型的 constructor 指向为 Parent
    // 所以要修复 Child 的原型的 constructor 指向为 Child
  // 目的是 防止原型链的混乱
  Child.prototype.constructor = Child
  Child.prototype.getChildValue = function() {return this.city}
  Child.prototype.getParentValue = function() {return this.name}

  var parent = new Parent('father')
  var child1 = new Child('son','BeiJing')
  var child2 = new Child('son','BeiJing')
  
  child1.identities.push('doctor')

  console.log(child1.name) //son
  console.log(child1.sex) //male
  console.log(child1.country) //undefined
  // 所有 Child 的实例共享 identities 属性
  console.log(child1.identities) //["philosopher", "scientist", "artist", "doctor"]
  console.log(child2.identities) //["philosopher", "scientist", "artist", "doctor"]
  console.log(child1.sayName()) //I am a son
  console.log(child1.age) //24
  console.log(child1.city) //BeiJing
  console.log(child1.honor) //Champion
  // 子类原型重写父类原型方法,子类实例调用该方法会覆盖父类原型的这个方法,// 但是父类实例调用该方法还是父类原型的这个方法
  console.log('父类实例调用输出:' + parent.getParentValue()) // 父类实例调用输出:father
  console.log('子类实例调用输出:' + child1.getParentValue()) // 子类实例调用输出:son
  console.log(child1.getChildValue()) //BeiJing
缺点:
  • 根据 child.country 的输出的值是 undefined 可知,根据 原型链继承 原理,创建子类实例时,不能向父类的构造函数传递参数
  • Child.prototype.getChildValueChild.prototype.getParentValue 必须是写在 Child.prototype = new Parent(‘father’) 之后,不然就会被报错

Q1:为什么要写 Child.prototype.constructor = Child?不写,好像对输出结果没影响,那为什么呢?

为什么要修复子类原型的 constructor 的指向
https://stackoverflow.com/questions/8453887/why-is-it-necessary-to-set-the-prototype-constructor#

借用构造函数式继承

特点
  • 基本思想:子类构造函数的内部用超类构造函数,通过 call,apply 改变对象的 this 来继承
 function Parent(name,country) {
    this.name = name
    this.sex = 'male'
    this.country = country
    this.identities = ["philosopher", "scientist", "artist"]
    this.sayName = function() {return 'I am a' + this.name}
  }
  Parent.prototype.honor = 'Champion'
  Parent.prototype.getParentValue = function() {return this.name}
  function Child(city) {
    // 通过 call,apply 改变对象的 this 来继承
    Parent.call(this,'father')
    this.age = 24
    this.city = city
  }

  Child.prototype.getChildValue = function() {return this.city}

  var child = new Child('BeiJing')

  child.identities.push('doctor')
  
  console.log(child.name) //father
  console.log(child.sex) //male
  console.log(child.country) //undefined
  console.log(child.identities) //["philosopher", "scientist", "artist", "doctor"]
  console.log(child.sayName()) //I am a father
  console.log(child.age) //24
  console.log(child.city) //BeiJing
  console.log(child.getChildValue()) //BeiJing
  console.log(child.honor) //Uncaught TypeError: child.honor is not a function
  console.log(child.getParentValue()) //Uncaught TypeError: child.getParentValue is not a function

分析:

相当于 Parent 这个构造函数在 Child 构造函数中执行了一遍,并且将 Parent 构造函数的 this 绑定的变量都切换到 Child 构造函数上,这样就克服了第一种方式带来的问题

缺点:
  • 虽然解决了 原型链继承中不能传值的问题,子类实例无法复用父类原型对象的方法和属性

组合式继承

特点:
  • 基本思想:既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性
  function Parent(name,country) {
    this.name = name
    this.sex = 'male'
    this.country = country
    this.identities = ["philosopher", "scientist", "artist"]
    this.sayName = function() {return 'I am a' + this.name}
  }
  Parent.prototype.honor = 'Champion'
  Parent.prototype.getParentValue = function() {return this.name}
  function Child(city) {
    // 通过 call,apply 改变对象的 this 来继承
    // 第二次调用父类构造函数,为了获取父类构造函数上的属性
    Parent.call(this,'father')
    this.age = 24
    this.city = city
  }
  // Child 的原型指向 Parent 的实例
  // 意味着 Child 的原型拥有 Parent 的实例的全部属性和方法
  // 也意味着 Child 的原型的__proto__属性指向 Parent 的原型
  // 所以最后 根据原型链的规则,Child 的实例拥有 Parent 实例的全部属性和方法,以及 Parent 的原型的全部属性和方法
  // 第一次调用父类构造函,为了获取子类原型获取父类原型上的方法
  Child.prototype = new Parent()
  // 此时 Child 的原型的 constructor 指向为 Parent
    // 所以要修复 Child 的原型的 constructor 指向为 Child
  // 目的是 防止原型链的混乱
  Child.prototype.constructor = Child
  Child.prototype.getChildValue = function() {return this.city}
  
  var child = new Child('BeiJing')

  child.identities.push('doctor')
  
  console.log(child.name) //father
  console.log(child.sex) //male
  console.log(child.country) //undefined
  console.log(child.identities) //["philosopher", "scientist", "artist", "doctor"]
  console.log(child.sayName()) //I am a father
  console.log(child.age) //24
  console.log(child.city) //BeiJing
  console.log(child.getChildValue()) //BeiJing
  console.log(child.honor) //Champion
  console.log(child.getParentValue()) //father
缺点:
  • 组合是继承是 JS 最常用的继承模式,但组合式集成使用过程中会把父类构造函数被调用两次:第一次创建父类实例执行子类原型时候;第二次在子类构造函数内部执行时候。如果父类构造函数属性极多,会导致速度减慢

原型式继承

特点:
  • 背景:道格拉斯·克罗克福德 2006 提出的一种继承方式
  • 基本思想:基于一个对象,把这个对象作为原型,再根据需求将得到的对象进行修改
    (借助原型基于已有的对象创建新对象,同时还不必因此创建自定义类型)
  • 使用场景:在没有必要大量地创建构造函数,只想让一个对象与另一个对象保持类似情况下使用
// 如果传入一个参数时,这个函数的 object(o)  和 Object.creat(o)  效果一样
function object(o){
    // 创建一个临时构造函数 F
    function F(){}
    // 构造函数 F 的原型对象指向传入的对象
    F.prototype = o
    // 返回一个临时构造函数为 F 的对象
    return new F()}

  var person = {
    name: 'xiaoming',
    firends: ['lisi','zhangshan']
  }
  // 从返回值看,实际上 object(person) => new F()
  // 意味着 var anotherPerson => new F()
  // 意味着 anotherPerson.__proto__ => F.prototype
  // 又因为  F.prototype = o
  // 意味着 F.prototype => person
  // 意味着 anotherPerson.__proto__ => person
  // 意味着 anotherPerson 拥有对象 person 的方法和属性
  var anotherPerson = object(person)
  anotherPerson.name = 'yaoming'
  anotherPerson.sayName = function() {return this.name}
  anotherPerson.firends.push('wangwu')
  var yetAnotherPerson = object(person)

  console.log(anotherPerson.name) //yaoming
  console.log(anotherPerson.firends) //["lisi", "zhangshan", "wangwu"]
  console.log(anotherPerson.sayName()) //yaoming
  console.log(yetAnotherPerson.name) //xiaoming
  console.log(yetAnotherPerson.firends) //["lisi", "zhangshan", "wangwu"]
  console.log(yetAnotherPerson.sayName()) //Uncaught TypeError: yetAnotherPerson.sayName is not a function

// ECMAScript 5 有个新的规范写法:// 如果传入一个参数,object() 和 Object.create() 效果是一样的,由此文中代码可转变为:// var anotherPerson = object(person) => var anotherPerson = Object.create(person)

Q1:为什么 yetAnotherPerson.name 为 xiaoming 而不是 yaoming?

因为 anotherPerson 实例自身添加 name 属性的值,但是没有修改 anotherPerson 实例(notherPerson.proto)原型上的 name 的值,所以 yetAnotherPerson 实例的 name 还是 xiaoming

Q2:为什么 anotherPerson.firends.push(‘wangwu’),yetAnotherPerson.firends 输出的结果跟 anotherPerson.firends 一样?

因为 anotherPerson 实例先从自身去查找属性,发现没有找到,然后根据继承关系往上找,发现在原型有这个属性,后给属性追加值,又因为 anotherPerson 实例和 yetAnotherPerson 实例指向同一个原型,所以当 anotherPerson 实例修改后,yetAnotherPerson 实例调用也发生变化

Q3:为什么 yetAnotherPerson.sayName() 会报错?

因为 anotherPerson 实例自身添加 sayName 方法,但是不是给 anotherPerson 实例(notherPerson.proto)原型上添加 sayName 方法,所以 yetAnotherPerson.sayName()会报错

缺点:
  • 引用类型的属性始终都是共享相应的值,多个实例对引用类型进行操作就会把值修改

寄生式继承

特点:
  • 基本思想:创建一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象
  • 使用场景:如果考虑的对象不是自定义类型和构造函数情况下,寄生式继承还是有用的,任何能够返回新对象的函数都适用于此模式。但是使用寄生式继承给对象添加函数,由于不能做到函数复用而降低效率,这一点跟构造函数式继承的效果一样
// 如果传入一个参数时,这个函数的 object(o)  和 Object.creat(o)  效果一样 
function object(o){
    // 创建一个临时构造函数 F
    function F(){}
    // 构造函数 F 的原型对象指向传入的对象
    F.prototype = o
    // 返回一个临时构造函数为 F 的对象
    return new F()}

  var person = {
    name: 'xiaoming',
    firends: ['lisi','zhangshan']
  }

  function createAnother(o){
    // 通过调用函数创建一个新对象
    var clone = object(o)
    // 以为对象新增方法的方式增强对象
    clone.sayHi = function(){return this.name}
    // 返回这个对象
    return clone
  }
  var anotherPerson = createAnother(person)
  var yetAnotherPerson = createAnother(person)
  // 具有实例原型的 person 的所有属性和方法,也可以有自己的方法
  console.log(anotherPerson.firends) //["lisi", "zhangshan"]
  console.log(anotherPerson.sayHi()) //xiaoming
  console.log(yetAnotherPerson.firends) //["lisi", "zhangshan"]
  console.log(yetAnotherPerson.sayHi()) //xiaoming

// ECMAScript 5 有个新的规范写法:// 如果传入一个参数,object() 和 Object.create() 效果是一样的,由此文中代码可转变为://  var clone = object(o) => var clone = Object.create(o)

寄生组合式继承

特点:
  • 背景:组合继承是 JS 最常用的继承模式,因为子类构造函数的原型上拥有了父类构造函数的实例属性,所以在原型连上出现多一份属性,造成内存消耗问题。无论什么情况下,都会去调用两次父类构造函数。寄生组合继承就是为了降低调用父类构造函数的内存开销
  • 基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定的子类型的原型而调用超类的构造函数。换句话说,用一个 F 空的构造函数去取代执行了 Parent 这个构造函数
  • 区别:寄生组合式继承 和 组合式继承区别在于,不在一次实例的调用中执行两次父类的构造函数
  function inheritPrototype(Child,Parent){
    // 将新创建的对象(即副本)赋值给子类型的原型
    Child.prototype = Object.create(Parent.prototype)
    // 修复子类的原型的 constructor 的指向,避免原型链混乱
    Child.prototype.constructor = Child
  }


  function Parent(name,country) {
    this.name = name
    this.sex = 'male'
    this.country = country
    this.identities = ["philosopher", "scientist", "artist"]
    this.sayName = function() {return 'I am a' + this.name}
  }
  Parent.prototype.honor = 'Champion'
  Parent.prototype.getParentValue = function(){return this.age}

  function Child(name,city) {
    // 通过 call,apply 改变对象的 this 来继承
    Parent.call(this,name)
    this.age = 24
    this.city = city
  }

  inheritPrototype(Child,Parent)

  Child.prototype.getChildValue = function() {return this.name}


  var child = new Child('father','Beijing')
  child.identities.push('doctor')

  console.log(child.name) //father
  console.log(child.sex) //male
  console.log(child.country) //undefined
  console.log(child.identities) //["philosopher", "scientist", "artist", "doctor"]
  console.log(child.sayName()) //I am a father
  console.log(child.age) //24
  console.log(child.city) //BeiJing
  console.log(child.getChildValue()) //father
  console.log(child.honor) //Champion
  console.log(child.getParentValue()) //24
  console.log(child.getChildValue()) //father

正文完
 0