javascript.prototype 和 繼承 -- 繼承實現(xiàn)的六種方式

通過之前的幾篇博客,我已經知道了.

雖然 javascript 不像傳動的 java.Net 那樣,有非常完畢的繼承系統(tǒng).

但通過 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的原因.


實現(xiàn)繼承的方式一 - 原型繼承

我們都知道,如果使用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

實現(xiàn)繼承的方式二 -- 借用函數繼承

在說明這個這種繼承方式之前,首先要稍微復習一下.

JavaScript 中 函數作為對象,它除了和普通對象一樣有 proto 屬性以外.

還有方法.

其中就有兩個比較常用的辦法 call & apply.

JavaScript 的 函數調用中.

函數從來都是不獨立調用的.

在瀏覽器環(huán)境里.

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('葫蘆娃') 來創(chuàng)建的,所以就表明了是 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 上的屬性和方法了.


實現(xiàn)繼承的方式三 -- 組合繼承

組合繼承,組合的是:

  • 原型鏈繼承
  • 借用函數繼承

這種方式的做法,是為了解決:

借用函數構造方法,無法使用函數原型上的屬性和方法而產生的.

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

實現(xiàn)繼承的方式四 -- 原型式繼承

原型式繼承的核心,其實很簡單.

需要提供一個被繼承的對象.(這里不是函數,而是是實實在在的對象)

然后把這個對象掛在到某個構造函數的prototype上.

此時,如果我們使用這個構造函數的new,就可以創(chuàng)建出一個對象.

這個對象就繼承了上述提供的實實在在對象上的屬性和方法了.

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

這種繼承方式,就是可以創(chuàng)建出一個繼承自某個對象的對象.

Object.create 方法內部差不多也是這么一個實現(xiàn)原理.

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()

從一個對象繼承,而不是類.
弱化的類的概念.


實現(xiàn)繼承的方式五 -- 寄生式繼承

寄生?

寄生誰?

就是把上述的 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的概念. 依然是從對象上繼承.

包裝起來的意義在哪?

僅僅只是包裝起來了而已...可以漸進增加一下對象的感覺????


實現(xiàn)繼承的方式六 - 寄生組合式繼承

上面講述的 原型式繼承寄生式繼承

都是對象在參與,弱化了類的概念.

而繼承應該是由類來參與的.(之類說的的類來參與指的是讓構造函數的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 設置的屬性和方法,都應該在此方法執(zhí)行完畢之后在執(zhí)行,否則會被覆蓋.
// 引用都變了,當然會時效.
  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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末尔当,一起剝皮案震驚了整個濱河市阔挠,隨后出現(xiàn)的幾起案子任柜,更是在濱河造成了極大的恐慌,老刑警劉巖商叹,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伙狐,死亡現(xiàn)場離奇詭異皇忿,居然都是意外死亡葡幸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門朗恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來湿颅,“玉大人,你說我怎么就攤上這事粥诫∮秃剑” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵怀浆,是天一觀的道長谊囚。 經常有香客問我,道長执赡,這世上最難降的妖魔是什么镰踏? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮沙合,結果婚禮上奠伪,老公的妹妹穿的比我還像新娘。我一直安慰自己首懈,他們只是感情好绊率,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著究履,像睡著了一般滤否。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挎袜,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天顽聂,我揣著相機與錄音肥惭,去河邊找鬼盯仪。 笑死紊搪,一個胖子當著我的面吹牛,可吹牛的內容都是我干的全景。 我是一名探鬼主播耀石,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爸黄!你這毒婦竟也來了滞伟?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤炕贵,失蹤者是張志新(化名)和其女友劉穎梆奈,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體称开,經...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡亩钟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鳖轰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片清酥。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蕴侣,靈堂內的尸體忽然破棺而出焰轻,到底是詐尸還是另有隱情,我是刑警寧澤昆雀,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布辱志,位于F島的核電站,受9級特大地震影響狞膘,放射性物質發(fā)生泄漏袁梗。R本人自食惡果不足惜颈墅,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漾肮,春花似錦、人聲如沸橄唬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渠缕。三九已至鸽素,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亦鳞,已是汗流浹背馍忽。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工棒坏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遭笋。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓坝冕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瓦呼。 傳聞我的和親對象是個殘疾皇子喂窟,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容

  • ??面向對象(Object-Oriented磨澡,OO)的語言有一個標志,那就是它們都有類的概念质和,而通過類可以創(chuàng)建任意...
    霜天曉閱讀 2,097評論 0 6
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,104評論 0 21
  • 前言 如果你覺得JS的繼承寫起來特別費勁稳摄,特別艱澀,特別不倫不類饲宿,我想說厦酬,我也有同感。尤其是作為一個學過Java的...
    光頭韓閱讀 457評論 0 2
  • 媽媽不會做飯褒傅。其實想想弃锐,也遺憾的。早些年在外留學殿托,同宿舍的女孩說起媽媽熬煮的雞湯霹菊,一臉的向往,我卻一點感覺也沒有支竹,...
    海陵燕飛閱讀 320評論 0 0
  • 放棄 放棄是一件非常容易的事旋廷。為什么?因為我們大家都體驗過礼搁。水順流而下饶碘,在重力的作用下自由流淌。但是如果想要逆流而...
    螢火之燈閱讀 111評論 0 0