一道題弄清楚JavaScript繼承演化史

  1. 寫出一個(gè)構(gòu)造函數(shù) Animal
  • 輸入:空
  • 輸出:一個(gè)新對(duì)象衡奥,該對(duì)象的共有屬性為 {行動(dòng): function(){}}券膀,沒有自有屬性
  1. 再寫出一個(gè)構(gòu)造函數(shù) Human
  • Human 繼承 Animal
  • 輸入:一個(gè)對(duì)象胡本,如 {name: 'Frank', birthday: '2000-10-10'}
  • 輸出:一個(gè)新對(duì)象可霎,該對(duì)象自有的屬性有 name 和 birthday,共有的屬性有物種(人類)拿愧、行動(dòng)和使用工具
  1. 再寫出一個(gè)構(gòu)造函數(shù) Asian
  • Asian 繼承 Human
  • 輸入:一個(gè)對(duì)象短蜕,如 {city: '北京', name: 'Frank', birthday: '2000-10-10' }
  • 輸出:一個(gè)新對(duì)象氢架,該對(duì)象自有的屬性有 name city 和 bitrhday,共有的屬性有物種忿危、行動(dòng)和使用工具和膚色

即达箍,最后一個(gè)新對(duì)象是 Asian 構(gòu)造出來的,Asian 繼承 Human铺厨,Human 繼承 Animal缎玫。

首次嘗試

在 JavaScript 中,繼承是基于原型來實(shí)現(xiàn)的解滓。在 ES6 之前赃磨,沒有類的概念,ECMAScript 2015 中引入的 JavaScript 類實(shí)質(zhì)上是 JavaScript 現(xiàn)有的基于原型的繼承的語法糖洼裤。類語法不會(huì)為JavaScript引入新的面向?qū)ο蟮睦^承模型邻辉。

JS 的作者為了吸引 Java 開發(fā)者來寫 JS 代碼,盡量在用函數(shù)模擬 Java 類的實(shí)現(xiàn)腮鞍。但是 JS 中函數(shù)是一等公民值骇,而不是像 Java 一樣函數(shù)是類的附庸。在 JS 中移国,構(gòu)造函數(shù)就是類吱瘩。

而因?yàn)槭腔谠偷睦^承,我們得知道一個(gè)重要公式:
實(shí)例.__proto__ === 構(gòu)造函數(shù).prototype

那么首先迹缀,按需求寫好構(gòu)造函數(shù)的基本內(nèi)容使碾。

(1)構(gòu)造函數(shù) Animal

function Animal() {}
Animal.prototype.move = function() {
    console.log('I am moving...')
}

(2)構(gòu)造函數(shù) Human

function Human(person) {
    // 借用構(gòu)造繼承
    Animal.call(this)  // 繼承Animal的私有屬性
    this.name = person.name || 'Unnamed'
    this.birthday = person.birthday || '1970-01-01'
}

// 注意,如果寫成 Human.prototype = {} 形式祝懂,會(huì)直接打破原型鏈票摇,繼承會(huì)出錯(cuò)
Human.prototype.species = 'Human'
Human.prototype.toolManipulating = function() {
    console.log('I can use tools!')
}

(3)構(gòu)造函數(shù) Asian

function Asian(person) {
    // 借用構(gòu)造繼承
    Human.call(this, person) // 繼承Human的私有屬性
    this.city = person.city || 'Beijing'
}
Asian.prototype.skin = 'yellow'

根據(jù)原型鏈的公式,如果要 Human 繼承自 Animal砚蓬,那么我們最快的實(shí)現(xiàn)方法就是在代碼中 添加 Human.prototype.__proto__ = Animal.prototype 即可矢门。如:

function Animal() {}
Animal.prototype.move = function() {
    console.log('I am moving...')
}

function Human(person) {
    Animal.call(this)  // 繼承Animal的私有屬性
    this.name = person.name || 'Unnamed'
    this.birthday = person.birthday || '1970-01-01'
}
Human.prototype.species = 'Human'
Human.prototype.toolManipulating = function() {}
Human.prototype.__proto__ = Animal.prototype

缺點(diǎn):在生產(chǎn)環(huán)境中使用 __proto__ 會(huì)引起嚴(yán)重的性能問題
因?yàn)樵S多瀏覽器優(yōu)化了原型,嘗試在調(diào)用實(shí)例之前猜測(cè)方法在內(nèi)存中的位置颅和,但是動(dòng)態(tài)設(shè)置原型干擾了所有的優(yōu)化傅事,甚至可能使瀏覽器為了運(yùn)行成功,使用完全未經(jīng)優(yōu)化的代碼進(jìn)行重編譯峡扩。

二次嘗試(組合繼承)

既然不能使用 __proto__,那么要怎樣才將 Human 添加到 Animal 的原型鏈中去呢障本?

答案是教届,用 new

new 是 JS 之父為我們封裝好的語法糖驾霜,當(dāng)使用 new 來調(diào)用函數(shù)時(shí)案训,會(huì)自動(dòng)執(zhí)行下面的操作:

  1. 創(chuàng)建一個(gè)全新的對(duì)象
  2. 這個(gè)新對(duì)象會(huì)被執(zhí)行 [[Prototype]] 連接
  3. 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this
  4. 如果函數(shù)沒有返回其他對(duì)象,那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象

其中第二步即能將新實(shí)例對(duì)象綁定到原型鏈中粪糙。

我們想得到的是 Human.prototype.__proto__ === Animal.prototype强霎,那么 Human.prototype 整體作為 Animal 的一個(gè)實(shí)例就好了,即

Human.prototype = new Animal()
Human.prototype.constructor = Human

上面的第一行代碼蓉冈,Human 擯棄了自己的原型城舞,強(qiáng)行賦了一個(gè)新值,而同時(shí)更改了 Human.prototypeconstructor 的指向寞酿,第二行代碼就是為了修正它的指向家夺。

回顧上面的代碼,用到了組合繼承伐弹,即原型鏈繼承 + 借用構(gòu)造繼承拉馋。

(1)原型鏈繼承

核心思想:將父類的實(shí)例作為子類的原型
優(yōu)點(diǎn):父類方法可以復(fù)用
缺點(diǎn):

  • 無法實(shí)現(xiàn)多繼承
  • 多個(gè)實(shí)例對(duì)引用類型的操作會(huì)被篡改惨好,因?yàn)榘妙愋偷脑蜁?huì)被所有實(shí)例共享
  • 不能向父類構(gòu)造函數(shù)傳遞參數(shù)

因此煌茴,原型鏈繼承一般不單獨(dú)使用。

(2)借用構(gòu)造繼承

核心思想:子類構(gòu)造函數(shù)內(nèi)部調(diào)用父類構(gòu)造函數(shù)日川。
優(yōu)點(diǎn):

  • 可以向父類構(gòu)造函數(shù)傳遞參數(shù)
  • 可以實(shí)現(xiàn)多繼承(call多個(gè)父類對(duì)象)
    缺點(diǎn):
  • 只能繼承父類的實(shí)例屬性和方法蔓腐,不能繼承原型屬性和方法
  • 無法實(shí)現(xiàn)復(fù)用,每個(gè)子類都有父類實(shí)例函數(shù)的副本逗鸣,影響性能

這兩個(gè)繼承總體被稱為組合繼承合住。

組合繼承完整代碼如下:

function Animal() {}
Animal.prototype.move = function() {
    console.log('I am moving...')
}

function Human(person) {
    // 第一次調(diào)用 Animal
    Animal.call(this) //借用構(gòu)造函數(shù),繼承了Animal且向父類傳參
    this.name = person.name || 'Unnamed'
    this.birthday = person.birthday || '1970-01-01'
}

Human.prototype = new Animal() // 第二次調(diào)用 Animal
Human.prototype.constructor = Human

// 注意撒璧,如果寫成 Human.prototype = {} 形式透葛,會(huì)直接打破原型鏈,繼承會(huì)出錯(cuò)
Human.prototype.species = 'Human'
Human.prototype.toolManipulating = function() {
    console.log('I can use tools!')
}
function Asian(person) {
    Human.call(this, person)
    this.city = person.city || 'Beijing'
}

Asian.prototype = new Human({})
Asian.prototype.constructor = Asian

Asian.prototype.skin = 'yellow'

var jay = new Asian({ name: 'Jay', birthday: '1979-01-18', city: 'Taiwan' })

注意:當(dāng)我們需要在子類中添加新的方法或者是重寫父類的方法時(shí)候卿樱,切記一定要放到替換原型的語句之后僚害。

測(cè)試一下:

基本功能算是實(shí)現(xiàn)了。但是,我們發(fā)現(xiàn)實(shí)例 jay 和它的原型 Asian.prototype 里面有重疊的屬性萨蚕。小結(jié)一下:

優(yōu)點(diǎn):

  • 父類的方法可以被復(fù)用
  • 父類的引用屬性不會(huì)被共享
  • 子類構(gòu)建實(shí)例時(shí)可以向父類傳遞參數(shù)
  • 實(shí)例屬性/方法靶草、原型屬性/方法均可以繼承

缺點(diǎn):

  • 調(diào)用了兩次父類的構(gòu)造函數(shù),第一次給子類的原型添加了父類的屬性岳遥,第二次又給子類的構(gòu)造函數(shù)添加了父類的屬性奕翔,從而覆蓋了子類原型中的同名參數(shù),浪費(fèi)性能

三次嘗試(寄生組合式繼承)

這里參考了由 Douglas Crockford 提出的原型式繼承浩蓉。他的想法是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象派继,同時(shí)還不必因此創(chuàng)建自定義類型。如下:

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

本質(zhì)上講捻艳,原型式繼承是對(duì)參數(shù)對(duì)象的一個(gè)淺復(fù)制驾窟,因此,它會(huì)有和原型鏈繼承一樣的問題认轨,即引用屬性所有實(shí)例共享绅络。

我們將代碼稍作變形,如下:

// 1.空函數(shù) F
function F() {}

// 2.把 F 的原型指向 Animal.prototype
F.prototype = Animal.prototype

// 3.把 Human 的原型指向一個(gè)新的 F 對(duì)象嘁字,F(xiàn) 對(duì)象的原型正好指向 Animal.prototype
Human.prototype = new F()

// 4.將 Human 原型的構(gòu)造函數(shù)修復(fù)為 Human
Human.prototype.constructor = Human

函數(shù) F 僅用于橋接恩急,我們僅創(chuàng)建了一個(gè) new F() 實(shí)例,沒有改變?cè)械脑玩溔2⑶矣捎?F 是空對(duì)象假栓,所以幾乎不占內(nèi)存。

如果把繼承這個(gè)動(dòng)作用一個(gè) inherits() 函數(shù)封裝起來霍掺,還可以隱藏 F 的定義匾荆,并簡(jiǎn)化代碼:

function inherits(Child, Parent) {
    function F() {}
    F.prototype = Parent.prototype
    Child.prototype = new F()
    Child.prototype.constructor = Child
}

所以寄生組合式繼承的全部代碼如下:

function Animal() {}

Animal.prototype.move = function() {
    console.log('I am moving...')
}

function Human(person) {
    Animal.call(this) //借用構(gòu)造函數(shù),繼承了Animal且向父類傳參
    this.name = person.name || 'Unnamed'
    this.birthday = person.birthday || '1970-01-01'
}

inherits(Human, Animal)

// 注意杆烁,如果寫成 Human.prototype = {} 形式牙丽,會(huì)直接打破原型鏈,繼承會(huì)出錯(cuò)
Human.prototype.species = 'Human'
Human.prototype.toolManipulating = function() {
    console.log('I can use tools!')
}

function Asian(person) {
    Human.call(this, person)
    this.city = person.city || 'Beijing'
}

inherits(Asian, Human)

Asian.prototype.skin = 'yellow'

/****************** Helper ***********************/
function inherits(Child, Parent) {
    function F() {}
    F.prototype = Parent.prototype
    Child.prototype = new F()
    Child.prototype.constructor = Child
}

測(cè)試:

var jay = new Asian({ name: 'Jay', birthday: '1979-01-18', city: 'Taiwan' })
var someone = new Asian({})
someone.move = function () {
    console.log('THIS FUCNTION HAS BEEN CHANGED!!')
}

總結(jié)一下兔魂,寄生組合式繼承的優(yōu)點(diǎn):

  • 避免了父類的引用屬性被共享
  • 可以復(fù)用繼承函數(shù)在爺孫三代甚至多代烤芦,提高了代碼的復(fù)用性
  • F 是空函數(shù),幾乎不占內(nèi)存析校,相當(dāng)于只在子類構(gòu)造函數(shù)中調(diào)用了一次父類构罗,更省內(nèi)存
  • 能夠正常使用 instanceofisPrototypeOf

開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。

四次嘗試(Object.create())

以上的方法智玻,均是在遠(yuǎn)古時(shí)期遂唧,無可奈何做的一些妥協(xié)技巧。但是到了 ECMAScript 5吊奢,官方發(fā)糖盖彭,通過新增 Object.create() 方法規(guī)范化了原型式繼承。

Object.create() 接收兩個(gè)參數(shù):

  • 一個(gè)用作新對(duì)象原型的對(duì)象
  • (可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象

直接在需要原型鏈繼承的地方,改為 Child.prototype = Object.create(Father.prototype) 即可召边。

全部代碼如下:

function Animal() {}

Animal.prototype.move = function() {
    console.log('I am moving...')
}

function Human(person) {
    Animal.call(this) //借用構(gòu)造函數(shù)铺呵,繼承了Animal且向父類傳參
    this.name = person.name || 'Unnamed'
    this.birthday = person.birthday || '1970-01-01'
}

Human.prototype = Object.create(Animal.prototype)

// 注意,如果寫成 Human.prototype = {} 形式隧熙,會(huì)直接打破原型鏈片挂,繼承會(huì)出錯(cuò)
Human.prototype.species = 'Human'
Human.prototype.toolManipulating = function() {
    console.log('I can use tools!')
}

function Asian(person) {
    Human.call(this, person)
    this.city = person.city || 'Beijing'
}

Asian.prototype = Object.create(Human.prototype)

Asian.prototype.skin = 'yellow'

var jay = new Asian({ name: 'Jay', birthday: '1979-01-18', city: 'Taiwan' })

五次嘗試(ES6 extends)

版本來到 ECMAScript 6,引入了 class 類贞盯,同時(shí)引入了 extends 繼承宴卖,這和傳統(tǒng)的面向?qū)ο蟮恼Z言更接近了。

基本上邻悬,ES6 的 class 可以看作只是一個(gè)語法糖,它的絕大部分功能随闽,ES5 都可以做到父丰,新的 class 寫法只是讓對(duì)象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已掘宪。

ES6 繼承的結(jié)果和寄生組合繼承相似蛾扇。

區(qū)別:

  • 寄生組合繼承是先創(chuàng)建子類實(shí)例 this 對(duì)象,然后再對(duì)其增強(qiáng)
  • ES6先將父類實(shí)例對(duì)象的屬性和方法魏滚,加到 this 上面(所以必須先調(diào)用 super 方法)镀首,然后再用子類的構(gòu)造函數(shù)修改 this

代碼如下:

class Animal {
    move() {
        console.log('I am moving...')
    }
}

class Human extends Animal {
    constructor(person) {
        super()
        this.name = person.name || 'Unnamed'
        this.birthday = person.birthday || '1970-01-01'
    }

    species = 'Human'

    toolManipulating() {
        console.log('I can use tools!')
    }
}

class Asian extends Human {
    constructor(person) {
        super(person)
        this.city = person.city || 'Beijing'
    }

    skin = 'Yellow'
}

var jay = new Asian({ name: 'Jay', birthday: '1979-01-18', city: 'Taiwan' })

但是測(cè)試我們發(fā)現(xiàn):

skinspecies 都被作為實(shí)例屬性處理了鼠次,查過了 API 才知道更哄,這是實(shí)例屬性的新寫法罷了。

class A {
    name = 'Jay'
}

相當(dāng)于

class A {
    constructor() {
        this.name = 'Jay'
    }
}

那么腥寇,要怎樣才能寫出 ES6 中類的公有屬性(即原型屬性)呢成翩?

class Animal {
    move() {
        console.log('I am moving...')
    }
}

class Human extends Animal {
    constructor(person) {
        super()
        this.name = person.name || 'Unnamed'
        this.birthday = person.birthday || '1970-01-01'
    }

    toolManipulating() {
        console.log('I can use tools!')
    }
}

Human.prototype.species = 'Human'

class Asian extends Human {
    constructor(person) {
        super(person)
        this.city = person.city || 'Beijing'
    }
}

Asian.prototype.skin = 'Yellow'
var jay = new Asian({ name: 'Jay', birthday: '1979-01-18', city: 'Taiwan' })

嗯?那這不是回到 ES5 的老路上了么赦役?就沒有 ES6 的優(yōu)雅寫法么麻敌?

抱歉,目前真沒有掂摔。

查詢可知一些方法术羔,諸如使用 getter 方法或者在 constructor 中添加如下代碼:

if(!this.__proto__.species) {
       this.__proto__.species= "Human"
}

但是這些看起來都不是優(yōu)雅的方案,希望未來跟進(jìn)吧乙漓。

總結(jié)

繼承是 JS 面試中臣独考的點(diǎn),牽扯到你對(duì)原型簇秒、原型鏈的理解鱼喉,對(duì)原型繼承流派和面向?qū)ο罅髋傻倪x擇偏好,也能拓展出在錯(cuò)綜復(fù)雜的繼承關(guān)系中對(duì) this 的掌握能力,總之是必須要掌握的內(nèi)容扛禽。本文沒有涉及到“深拷貝繼承”這種方式锋边。

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市编曼,隨后出現(xiàn)的幾起案子豆巨,更是在濱河造成了極大的恐慌,老刑警劉巖掐场,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件往扔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡熊户,警方通過查閱死者的電腦和手機(jī)萍膛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嚷堡,“玉大人蝗罗,你說我怎么就攤上這事◎蚪洌” “怎么了串塑?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)北苟。 經(jīng)常有香客問我桩匪,道長(zhǎng),這世上最難降的妖魔是什么友鼻? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任傻昙,我火速辦了婚禮,結(jié)果婚禮上桃移,老公的妹妹穿的比我還像新娘屋匕。我一直安慰自己,他們只是感情好借杰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布过吻。 她就那樣靜靜地躺著,像睡著了一般蔗衡。 火紅的嫁衣襯著肌膚如雪纤虽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天绞惦,我揣著相機(jī)與錄音逼纸,去河邊找鬼。 笑死济蝉,一個(gè)胖子當(dāng)著我的面吹牛杰刽,可吹牛的內(nèi)容都是我干的菠发。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贺嫂,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼滓鸠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起第喳,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤糜俗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后曲饱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悠抹,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年扩淀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了楔敌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡驻谆,死狀恐怖梁丘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旺韭,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布掏觉,位于F島的核電站区端,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏澳腹。R本人自食惡果不足惜织盼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酱塔。 院中可真熱鬧沥邻,春花似錦、人聲如沸羊娃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蕊玷。三九已至邮利,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垃帅,已是汗流浹背延届。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贸诚,地道東北人方庭。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓厕吉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親械念。 傳聞我的和親對(duì)象是個(gè)殘疾皇子头朱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容