- 寫出一個(gè)構(gòu)造函數(shù) Animal
- 輸入:空
- 輸出:一個(gè)新對(duì)象衡奥,該對(duì)象的共有屬性為 {行動(dòng): function(){}}券膀,沒有自有屬性
- 再寫出一個(gè)構(gòu)造函數(shù) Human
- Human 繼承 Animal
- 輸入:一個(gè)對(duì)象胡本,如 {name: 'Frank', birthday: '2000-10-10'}
- 輸出:一個(gè)新對(duì)象可霎,該對(duì)象自有的屬性有 name 和 birthday,共有的屬性有物種(人類)拿愧、行動(dòng)和使用工具
- 再寫出一個(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í)行下面的操作:
- 創(chuàng)建一個(gè)全新的對(duì)象
- 這個(gè)新對(duì)象會(huì)被執(zhí)行
[[Prototype]]
連接 - 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的
this
- 如果函數(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.prototype
中 constructor
的指向寞酿,第二行代碼就是為了修正它的指向家夺。
回顧上面的代碼,用到了組合繼承伐弹,即原型鏈繼承 + 借用構(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)存 - 能夠正常使用
instanceof
和isPrototypeOf
開發(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):
skin
和 species
都被作為實(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)容扛禽。本文沒有涉及到“深拷貝繼承”這種方式锋边。
參考: