美文网首页
javascript.prototype 和 继承 -- 继承实

javascript.prototype 和 继承 -- 继承实

作者: 人话博客 | 来源:发表于2019-02-28 18:48 被阅读0次

    通过之前的几篇博客,我已经知道了.

    虽然 javascript 不像传动的 java.Net 那样,有非常完毕的继承系统.

    但通过 JavaScript 的构造器函数对象的 .prototype 属性,我们可以完成一些类似于继承的操作.

    补充记忆:

    实例对象对原型对象的修改是COW(copy on write)


    简单的继承体系

    javascript中,有一种特别特殊,又被我们常常忽略掉的对象.

    那么就是函数对象.

    特殊之处在于,所有的函数都可以当做是构造器存在.

    当使用 new 来调用这个所谓的构造器(不管这个函数是否是以构造一个对象的功能作用而声明的).

    在此函数内部都会有一个 this 关键字.

    和普通调用函数不同的是.

    当时用 new 调用函数时,情况就会非常简单

    里面的this就是构造出来的那个对象.

    且这个对象默认会从构造器的 .prototype继承属性或者方法.

    同时还有一条非常隐蔽的链条.

    构造器的.prototype 同时也是继承 Object.prototype 的.

    function Animal (name) {
      this.name = name || 'Animal'
      this.sleep = function () {
        console.log(this.name + ' sleep')
      }
    }
    
    const cat = new Animal() // 用new调用,而不是像普通函数那样调用.于是 this 就指向明晰了,就是构造出来的 cat 对象.
    
    Animal.prototype.eat = function () {
      console.log(this.name + ' eat')
    }
    
    cat.eat() // 所有的构造出来的对象,都会从构造它的函数的prototype上继承.
    
    // 一条比较隐蔽的继承链(也就说所谓的原型链)
    console.log(AnimalAnimal.prototype.__proto__ === Object.prototype) // true
    
    

    一张图

    所有对象都从Object.prototype继承

    其中,画红色箭头就是时常会忽略,但是为什么原型链为什么会这么完整的核心.

    也就是为什么所有对象可以正常的调用 Object.prototype.functions的原因.


    实现继承的方式一 - 原型继承

    我们都知道,如果使用new关键字,把一个函数当构造器来使用,那么函数构造器是会返回一个对象的.

    且返回的这个对象,会从此构造器的prototype上继承一些属性.

    而客观存在的情况是,构造器prototype本身不是只读的.

    我们甚至可以修改覆盖它的配置.

    让它变成一个我们希望可以继承的对象.

    比如:

    function Animal() { }
    const parentObject = { 
      name: '我是被继承的数据',
      fn () {
        console.log('我是被继承的方法')
      }
    }
    Animal.prototype = parentObject
    const a = new Animal()
    console.log(a.name)
    a.fn()
    
    image.png

    有了这个基本的前提之后,就开始定义我们继承自 Animal 构造器的子类 Cat 了.

    function Animal (name) {
      this.name = name || 'Animal'
      this.sleep = function () {
        console.log(this.name + ' sleep')
      }
    }
    
    Animal.prototype.eat = function () {
      console.log(this.name + ' eat')
    }
    
    function Cat () { }
    
    Cat.prototype = new Animal('狗子')
    const cat = new Cat()
    
    console.log(cat.name)
    cat.sleep()
    cat.eat()
    

    原型继承的核心就是上述代码:Cat.prototype = new Animal('狗子')

    我们让自己定义的构造器的 prototype 对象指向父类构造器生成的对象.

    由于父类构造器生成的对象包含了,父类实例定义的所有属性以及父类构造器原型上的属性.

    所以,子类可以完整的从父类那里继承所有的属性.

    一张图

    image.png

    实现继承的方式二 -- 借用函数继承

    在说明这个这种继承方式之前,首先要稍微复习一下.

    JavaScript 中 函数作为对象,它除了和普通对象一样有 proto 属性以外.

    还有方法.

    其中就有两个比较常用的办法 call & apply.

    JavaScript 的 函数调用中.

    函数从来都是不独立调用的.

    在浏览器环境里.

    function somefn () {}
    somefn()
    
    // 等同于 
    
    someFn(window)
    
    

    对于一些其他的常用的函数调用模式.

    obj.method()
    // 
    其实等同于 method(obj)
    

    所以,函数的调用从来都不是独立存在的.都会默认有一个隐蔽的参数.

    我们可以通过 函数对象本身的 callapply 来显示的指定函数调用时的这个必备的参数是谁.

    obj.method.call(obj2)
    

    此时,在obj里定义的函数内部访问this不是 obj,而是 obj2了.

    有了上述复习.

    可以开始写构造器继承了.

    首先定义一个基类

    function Animal (name) {
      this.name = name || 'Animal'
      this.sleep = function () {
        console.log(this.name + ' sleep')
      }
    }
    
    

    然后定义子类 Cat

    function Cat (name) {
      Animal(this, name)
    }
    

    关键一句是在 Animal.call(this,name)

    虽然,之前,我们都把 Animal 当成构造器存在,要使用new关键字来调用.

    但是在这里,我们把 Animal当成普通函数而非构造器.

    利用普通函数的 call 方法,改变 this..

    const cat = new Cat('葫芦娃')
    console.log(cat.name)
    cat.sleep()
    

    这里的 this 是由 new Cat('葫芦娃') 来创建的,所以就表明了是 cat 的一个实例.

    结果:

    image.png

    这种继承方式有一个违反直觉的缺点:

    既然我们本意是让 Cat 继承自 Animal
    我们当然也希望 Cat 能当做原型继承那样能够正常的调用 Animal.prototype 上的方法.
    但这种方式不行.

    Animal本来是个构造函数.

    但是由于,借用函数继承,把它当成了一个普通的函数来使用.(调用.call方法)

    所以 new Cat() 对象,无法调用Animal函数定义在 prototype 上的属性和方法.

    function Animal (name) {
      this.name = name || 'Animal'
      this.sleep = function () {
        console.log(this.name + ' sleep')
      }
    }
    
    Animal.prototype.run = function () {
      console.log(`${this.name} run!`)
    }
    
    
    function Cat (name) {
      Animal.call(this, name)
    }
    const cat = new Cat('葫芦娃')
    console.log(cat.name) // 没问题
    cat.sleep() // 没问题
    cat.run()// cat.run is not a function
    
    

    一张图

    image.png

    红色的路径,压根就不在 Cat 的原型继承链条中,所以就无法使用到 Animal.prototype 上的属性和方法了.


    实现继承的方式三 -- 组合继承

    组合继承,组合的是:

    • 原型链继承
    • 借用函数继承

    这种方式的做法,是为了解决:

    借用函数构造方法,无法使用函数原型上的属性和方法而产生的.

    function Animal (name) {
      this.name = name
      this.eat = function () {
        console.log(`${this.name} eat`)
      }
    }
    Animal.prototype.run = function () {
      console.log(`${this.name} run`)
    }
    
    function Cat (name) {
    // 实例数据继承到了. name,eat()
      Animal.call(this,name)
    }
    
    // 原型数据继承到了 run()
    // 原型数据继承到了 run()
    Cat.prototype = new Animal('🐶') // 这样写,会造成两次Animal实例化.且没有自己的原型了.
    Cat.prototype = Animal.prototype // 这样写,不会造成两次Animal实例化,且没有自己的原型了.
    
    
    const cat = new Cat('🐶')
    cat.eat()
    cat.run()
    
    

    结果

    image.png
    • 使用 Animal.call() 来继承 Animal 的实例属性和方法.
    • 使用 Cat.prototype = Animal.prototype 来使用 Animal.prototype 属性和方法. 这样避免了两次调用Animal 构造函数,但是 Cat 没有自己的原型 prototype
    • 使用 Cat.prototype = new Animal() 会造成两次构造函数调用.第一次 new Animal() ,第二次:Animal.call(this,name) ,同样的让 Cat 也弃用了自己的原型 prototype

    实现继承的方式四 -- 原型式继承

    原型式继承的核心,其实很简单.

    需要提供一个被继承的对象.(这里不是函数,而是是实实在在的对象)

    然后把这个对象挂在到某个构造函数的prototype上.

    此时,如果我们使用这个构造函数的new,就可以创建出一个对象.

    这个对象就继承了上述提供的实实在在对象上的属性和方法了.

    function inherit (obj) {
      function Constructor () { } // 提供一个函数
      Constructor.prototype = obj // 设置函数的 prototype
      return new Constructor() // 返回这个函数实例化出来的对象.
    }
    
    function Animal (name) {
      this.name = name
      this.eat = function () {
        console.log(`${this.name} eat`)
      }
    }
    
    Animal.prototype.run = function () {
      console.log(`${this.name} run`)
    }
    
    const animal = new Animal('小猫')
    const cat = inherit(animal) // cat 要从animal对象上继承它所有的方法和属性.
    cat.eat()
    cat.run()
    
    

    结果:

    image.png

    这种继承方式,就是可以创建出一个继承自某个对象的对象.

    Object.create 方法内部差不多也是这么一个实现原理.

    const cat2 = Object.create(animal, {
      food: {
        writable: true,
        enumerable: true,
        configurable: true,
        value: '小鱼干'
      }
    }) // cat2 对象从 animal 对象上继承. 并扩展自己一个food属性.
      
    cat2.name = '小猫2'
    console.log(cat2.food)
    cat2.run()
    cat2.eat()
    

    从一个对象继承,而不是类.
    弱化的类的概念.


    实现继承的方式五 -- 寄生式继承

    寄生?

    寄生谁?

    就是把上述的 inherit 函数在包装一下.

    function inherit (obj) {
      if (typeof obj !== 'object') throw new Error('必须传入一个对象')
      function Constructor () { }
      Constructor.prototype = obj
      return new Constructor()
    }
    
    function createSubObj (superObject, options) {
      var clone = inherit(superObject)
      if (options && typeof options === 'object') {
        Object.assign(clone, options)
      }
      
      return clone
    }
    
    
    const superObject = {
      name: '张三',
      age: 22,
      speak () {
        console.log(`i am ${this.name} and ${this.age} years old!`)
      }
    }
    
    
    const subObject = createSubObj(superObject, {
      professional: '前端工程师',
      report : function () {
        console.log(`i am a ${this.professional}`)
      }
    })
    
    subObject.speak()
    subObject.report()
    

    结果:

    image.png

    仍然没有class的概念. 依然是从对象上继承.

    包装起来的意义在哪?

    仅仅只是包装起来了而已...可以渐进增加一下对象的感觉????


    实现继承的方式六 - 寄生组合式继承

    上面讲述的 原型式继承寄生式继承

    都是对象在参与,弱化了类的概念.

    而继承应该是由类来参与的.(之类说的的类来参与指的是让构造函数的prototype来参与)

    所以,寄生组合式继承还是让来参与继承.

    
    function inheritPrototype (SuperType, SubType) {
      if (typeof SuperType !== 'function' || typeof SubType !== 'function') {
        throw new Error('必须传递构造函数!')
      } 
    
      // 这个地方利用Object.create(Subtype.prototype) 
      // 非常巧妙的让Subtype.prototype对象继承自 SuperType.prototype.
      // 而不是去覆盖自己.
    // 特别注意:!!!!!!!!!!!!! Object.create 方法会返回一个对象 obj. obj.__proto__ = Object.create 函数接受的参数.
    // 所以,任何在此代码前给 obj 设置的属性和方法,都应该在此方法执行完毕之后在执行,否则会被覆盖.
    // 引用都变了,当然会时效.
      SubType.prototype = Object.create(SuperType.prototype)
    }
    
    inheritPrototype(SuperType, SubType)
    
    function SuperType (name) {
      this.name = name
      this.showName = function () {
        console.log('from SuperType:' + this.name)
      }
    }
    
    SuperType.prototype.super_protoProperty = 'SuperType原型属性'
    SuperType.prototype.super_protoFunction = function () {
      console.log('SuperType原型方法')
    }
    
    function SubType (name, age) {
      SuperType.call(this, name)
      this.age = age
      this.showAge = function () {
        console.log('from SubType:' + this.age)
      }
    }
    
    SubType.prototype.sub_protoProperty = 'SubType原型属性'
    SubType.prototype.sub_protoFunction = function () {
      console.log('SunType原型方法')
    }
    
    
    
    
    const sub = new SubType('张三', 22)
    sub.showAge()
    sub.showName()
    console.log(sub.super_protoProperty) // 拿不到 undefined
    sub.super_protoFunction() // 方法不存在.
    sub.sub_protoFunction() // 拿自己的原型没问题
    console.log(sub.sub_protoProperty) // 拿自己的原型没问题
    
    

    核心代码就是上述的

    SubType.prototype = Object.create(SuperType.prototype)

    这句代码利用 Object.create() 方法,非常巧妙的让
    SubType.prototype 继承 SuperType.prototype

    这儿做: SubType 既保留了自己的原型对象.又能从 SuperType 的原型上继承.

    运行结果:

    from SubType:22
    from SuperType:张三
    SuperType原型属性
    SuperType原型方法
    SunType原型方法
    SubType原型属性
    

    这样做法的好处非常明显.

    子类不光可以从父类继承实例属性.(SubType.call(this).
    还能从父类的原型继承属性 (SubType.prototype = Object.create(SubperType.prototype)

    一张图

    Subtype.prototype = Object.create(SuperType.prototype)
    • SubTypeSuperType.call(this) 继承到了 SuperType 的实例属性.
    • SubTypenew SubType 里声明了自己的属性.
    • 由于 Subtype.prototype 不是想原型组合集成那样是覆盖自己的原型,而是让原型对象继承子 SuperType.prototype.
    • 所以 SubType.prototype 原型对象仍然存在.所以 SubType 可以从自己的原型上继承.
    • 同时 Subtype.prototype : SuperType.prototype . 所以,Subtype 还可以从 SuperType.prototype 上继承属性.

    new SubType()

    • 自己的实例属性 --> bingo
    • 自己的原型对象 ---> bingo
    • 父类的实例属性 ---> bingo
    • 父类的原型对象 ---> bingo

    相关文章

      网友评论

          本文标题:javascript.prototype 和 继承 -- 继承实

          本文链接:https://www.haomeiwen.com/subject/owvkuqtx.html