javascript.prototype 和 继承 -- 继承实现的六种方式

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

虽然 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  •   面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意...
    霜天晓阅读 2,093评论 0 6
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,097评论 0 21
  • 前言 如果你觉得JS的继承写起来特别费劲,特别艰涩,特别不伦不类,我想说,我也有同感。尤其是作为一个学过Java的...
    光头韩阅读 456评论 0 2
  • 妈妈不会做饭。其实想想,也遗憾的。早些年在外留学,同宿舍的女孩说起妈妈熬煮的鸡汤,一脸的向往,我却一点感觉也没有,...
    海陵燕飞阅读 319评论 0 0
  • 放弃 放弃是一件非常容易的事。为什么?因为我们大家都体验过。水顺流而下,在重力的作用下自由流淌。但是如果想要逆流而...
    萤火之灯阅读 110评论 0 0