javascript.prototype 和 继承 -- 继承实

2019-02-28  本文已影响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

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

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

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

然后把这个对象挂在到某个构造函数的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)

new SubType()

上一篇下一篇

猜你喜欢

热点阅读