JavaScript的9種繼承實(shí)現(xiàn)方式歸納
不同于基于類(lèi)的編程語(yǔ)言,如 C++ 和 Java,JavaScript 中的繼承方式是基于原型的。同時(shí)由于 JavaScript 是一門(mén)非常靈活的語(yǔ)言,其實(shí)現(xiàn)繼承的方式也非常多。
首要的基本概念是關(guān)于構(gòu)造函數(shù)和原型鏈的,父對(duì)象的構(gòu)造函數(shù)稱(chēng)為Parent,子對(duì)象的構(gòu)造函數(shù)稱(chēng)為Child,對(duì)應(yīng)的父對(duì)象和子對(duì)象分別為parent和child。
對(duì)象中有一個(gè)隱藏屬性[[prototype]](注意不是prototype),在 Chrome 中是__proto__,而在某些環(huán)境下則不可訪(fǎng)問(wèn),它指向的是這個(gè)對(duì)象的原型。在訪(fǎng)問(wèn)任何一個(gè)對(duì)象的屬性或方法時(shí),首先會(huì)搜索本對(duì)象的所有屬性,如果找不到的話(huà)則會(huì)根據(jù)[[prototype]]沿著原型鏈逐步搜索其原型對(duì)象上的屬性,直到找到為止,否則返回undefined。
1.原型鏈繼承:
原型鏈?zhǔn)?JavaScript 中實(shí)現(xiàn)繼承的默認(rèn)方式,如果要讓子對(duì)象繼承父對(duì)象的話(huà),最簡(jiǎn)單的方式是將子對(duì)象構(gòu)造函數(shù)的prototype屬性指向父對(duì)象的一個(gè)實(shí)例:
function Parent() {}
function Child() {}
Child.prototype = new Parent()
這個(gè)時(shí)候,Child的prototype屬性被重寫(xiě)了,指向了一個(gè)新對(duì)象,但是這個(gè)新對(duì)象的constructor屬性卻沒(méi)有正確指向Child,JS 引擎并不會(huì)自動(dòng)為我們完成這件工作,這需要我們手動(dòng)去將Child的原型對(duì)象的constructor屬性重新指向Child:
Child.prototype.constructor = Child
以上就是 JavaScript 中的默認(rèn)繼承機(jī)制,將需要重用的屬性和方法遷移至原型對(duì)象中,而將不可重用的部分設(shè)置為對(duì)象的自身屬性,但這種繼承方式需要新建一個(gè)實(shí)例作為原型對(duì)象,效率上會(huì)低一些。
2.原型繼承(非原型鏈):
為了避免上一個(gè)方法需要重復(fù)創(chuàng)建原型對(duì)象實(shí)例的問(wèn)題,可以直接將子對(duì)象構(gòu)造函數(shù)的prototype指向父對(duì)象構(gòu)造函數(shù)的prototype,這樣,所有Parent.prototype中的屬性和方法也能被重用,同時(shí)不需要重復(fù)創(chuàng)建原型對(duì)象實(shí)例:
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
但是我們知道,在 JavaScript 中,對(duì)象是作為引用類(lèi)型存在的,這種方法實(shí)際上是將Child.prototype和Parent.prototype中保存的指針指向了同一個(gè)對(duì)象,因此,當(dāng)我們想要在子對(duì)象原型中擴(kuò)展一些屬性以便之后繼續(xù)繼承的話(huà),父對(duì)象的原型也會(huì)被改寫(xiě),因?yàn)檫@里的原型對(duì)象實(shí)例始終只有一個(gè),這也是這種繼承方式的缺點(diǎn)。
3.臨時(shí)構(gòu)造器繼承:
為了解決上面的問(wèn)題,可以借用一個(gè)臨時(shí)構(gòu)造器起到一個(gè)中間層的作用,所有子對(duì)象原型的操作都是在臨時(shí)構(gòu)造器的實(shí)例上完成,不會(huì)影響到父對(duì)象原型:
var F = function() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
同時(shí),為了可以在子對(duì)象中訪(fǎng)問(wèn)父類(lèi)原型中的屬性,可以在子對(duì)象構(gòu)造器上加入一個(gè)指向父對(duì)象原型的屬性,如uber,這樣,可以在子對(duì)象上直接通過(guò)child.constructor.uber訪(fǎng)問(wèn)到父級(jí)原型對(duì)象。
我們可以將上面的這些工作封裝成一個(gè)函數(shù),以后調(diào)用這個(gè)函數(shù)就可以方便實(shí)現(xiàn)這種繼承方式了:
function extend(Child, Parent) {
var F = function() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
Child.uber = Parent.prototype
}
然后就可以這樣調(diào)用:
extend(Dog, Animal)
4.屬性拷貝:
這種繼承方式基本沒(méi)有改變?cè)玩湹年P(guān)系,而是直接將父級(jí)原型對(duì)象中的屬性全部復(fù)制到子對(duì)象原型中,當(dāng)然,這里的復(fù)制僅僅適用于基本數(shù)據(jù)類(lèi)型,對(duì)象類(lèi)型只支持引用傳遞。
function extend2(Child, Parent) {
var p = Parent.prototype
var c = Child.prototype
for (var i in p) {
c[i] = p[i]
}
c.uber = p
}
這種方式對(duì)部分原型屬性進(jìn)行了重建,構(gòu)建對(duì)象的時(shí)候效率會(huì)低一些,但是能夠減少原型鏈的查找。不過(guò)我個(gè)人覺(jué)得這種方式的優(yōu)點(diǎn)并不明顯。
5.對(duì)象間繼承:
除了基于構(gòu)造器間的繼承方法,還可以?huà)侀_(kāi)構(gòu)造器直接進(jìn)行對(duì)象間的繼承。即直接進(jìn)行對(duì)象屬性的拷貝,其中包括淺拷貝和深拷貝。
淺拷貝:
接受要繼承的對(duì)象,同時(shí)創(chuàng)建一個(gè)新的空對(duì)象,將要繼承對(duì)象的屬性拷貝至新對(duì)象中并返回這個(gè)新對(duì)象:
function extendCopy(p) {
var c = {}
for (var i in p) {
c[i] = p[i]
}
c.uber = p
return c
}
拷貝完成之后對(duì)于新對(duì)象中需要改寫(xiě)的屬性可以進(jìn)行手動(dòng)改寫(xiě)。
深拷貝:
淺拷貝的問(wèn)題也顯而易見(jiàn),它不能拷貝對(duì)象類(lèi)型的屬性而只能傳遞引用,要解決這個(gè)問(wèn)題就要使用深拷貝。深拷貝的重點(diǎn)在于拷貝的遞歸調(diào)用,檢測(cè)到對(duì)象類(lèi)型的屬性時(shí)就創(chuàng)建對(duì)應(yīng)的對(duì)象或數(shù)組,并逐一復(fù)制其中的基本類(lèi)型值。
function deepCopy(p, c) {
c = c || {}
for (var i in p) {
if (p.hasOwnProperty(i)) {
if (typeof p[i] === 'object') {
c[i] = Array.isArray(p[i]) ? [] : {}
deepCopy(p[i], c[i])
} else {
c[i] = p[i]
}
}
}
return c
}
其中用到了一個(gè) ES5 的Array.isArray()方法用于判斷參數(shù)是否為數(shù)組,沒(méi)有實(shí)現(xiàn)此方法的環(huán)境需要自己手動(dòng)封裝一個(gè) shim。
Array.isArray = function(p) {
return p instanceof Array
}
但是使用instanceof操作符無(wú)法判斷來(lái)自不同框架的數(shù)組變量,但這種情況比較少。
6.原型繼承:
借助父級(jí)對(duì)象,通過(guò)構(gòu)造函數(shù)創(chuàng)建一個(gè)以父級(jí)對(duì)象為原型的新對(duì)象:
function object(o) {
var n
function F() {}
F.prototype = o
n = new F()
n.uber = o
return n
}
這里,直接將父對(duì)象設(shè)置為子對(duì)象的原型,ES5 中的 Object.create()方法就是這種實(shí)現(xiàn)方式。
7.原型繼承和屬性拷貝混用:
原型繼承方法中以傳入的父對(duì)象為原型構(gòu)建子對(duì)象,同時(shí)還可以在父對(duì)象提供的屬性之外額外傳入需要拷貝屬性的對(duì)象:
function ojbectPlus(o, stuff) {
var n
function F() {}
F.prototype = o
n = new F()
n.uber = o
for (var i in stuff) {
n[i] = stuff[i]
}
return n
}
8.多重繼承:
這種方式不涉及原型鏈的操作,傳入多個(gè)需要拷貝屬性的對(duì)象,依次進(jìn)行屬性的全拷貝:
function multi() {
var n = {}, stuff, i = 0,
len = arguments.length
for (i = 0; i < len; i++) {
stuff = arguments[i]
for (var key in stuff) {
n[i] = stuff[i]
}
}
return n
}
根據(jù)對(duì)象傳入的順序依次進(jìn)行拷貝,也就是說(shuō),如果后傳入的對(duì)象包含和前面對(duì)象相同的屬性,后者將會(huì)覆蓋前者。
9.構(gòu)造器借用:
JavaScript中的call()和apply()方法非常好用,其改變方法執(zhí)行上下文的功能在繼承的實(shí)現(xiàn)中也能發(fā)揮作用。所謂構(gòu)造器借用是指在子對(duì)象構(gòu)造器中借用父對(duì)象的構(gòu)造函數(shù)對(duì)this進(jìn)行操作:
function Parent() {}
Parent.prototype.name = 'parent'
function Child() {
Parent.apply(this, arguments)
}
var child = new Child()
console.log(child.name)
這種方式的最大優(yōu)勢(shì)就是,在子對(duì)象的構(gòu)造器中,是對(duì)子對(duì)象的自身屬性進(jìn)行完全的重建,引用類(lèi)型的變量也會(huì)生成一個(gè)新值而不是一個(gè)引用,所以對(duì)子對(duì)象的任何操作都不會(huì)影響父對(duì)象。
而這種方法的缺點(diǎn)在于,在子對(duì)象的構(gòu)建過(guò)程中沒(méi)有使用過(guò)new操作符,因此子對(duì)象不會(huì)繼承父級(jí)原型對(duì)象上的任何屬性,在上面的代碼中,child的name屬性將會(huì)是undefined。
要解決這個(gè)問(wèn)題,可以再次手動(dòng)將子對(duì)象構(gòu)造器原型設(shè)為父對(duì)象的實(shí)例:
Child.prototype = new Parent()
但這樣又會(huì)帶來(lái)另一個(gè)問(wèn)題,即父對(duì)象的構(gòu)造器會(huì)被調(diào)用兩次,一次是在父對(duì)象構(gòu)造器借用過(guò)程中,另一次是在繼承原型過(guò)程中。
要解決這個(gè)問(wèn)題,就要去掉一次父對(duì)象構(gòu)造器的調(diào)用,構(gòu)造器借用不能省略,那么只能去掉后一次調(diào)用,實(shí)現(xiàn)繼承原型的另一方法就是迭代復(fù)制:
extend2(Child, Parent)
使用之前實(shí)現(xiàn)的extend2()方法即可。
相關(guān)文章
JS創(chuàng)建或填充任意長(zhǎng)度數(shù)組的小技巧匯總
在JavaScript 中,我們往往會(huì)遇到需要使用某些默認(rèn)值來(lái)填充數(shù)組的情況,那么都有哪些方式可以完成這樣的任務(wù)呢?這篇文章主要給大家介紹了關(guān)于JS創(chuàng)建或填充任意長(zhǎng)度數(shù)組的小技巧,需要的朋友可以參考下2021-10-10webpack-url-loader 解決項(xiàng)目中圖片打包路徑問(wèn)題
這篇文章主要介紹了webpack-url-loader 解決項(xiàng)目中圖片打包路徑問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02js實(shí)現(xiàn)一個(gè)簡(jiǎn)易計(jì)算器
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)一個(gè)簡(jiǎn)易計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07微信小程序?qū)崿F(xiàn)運(yùn)動(dòng)步數(shù)排行功能(可刪除)
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)運(yùn)動(dòng)步數(shù)排行功能(可刪除),實(shí)現(xiàn)代碼也很簡(jiǎn)單,需要的朋友可以參考下2018-07-07javascript setTimeout和setInterval計(jì)時(shí)的區(qū)別詳解
window對(duì)象有兩個(gè)主要的定時(shí)方法,分別是setTimeout 和 setInteval 他們的語(yǔ)法基本上相同,但是完成的功能取有區(qū)別。2013-06-06微信小程序?qū)崿F(xiàn)之手勢(shì)鎖功能實(shí)例代碼
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)之手勢(shì)鎖功能的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07JS實(shí)現(xiàn)商品倒計(jì)時(shí)實(shí)現(xiàn)代碼
JS實(shí)現(xiàn)商品倒計(jì)時(shí)實(shí)現(xiàn)代碼,需要的朋友可以參考一下2013-05-05JavaScript制作簡(jiǎn)單網(wǎng)頁(yè)計(jì)算器
這篇文章主要為大家詳細(xì)介紹了JavaScript制作簡(jiǎn)單網(wǎng)頁(yè)計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08javascript中window.open在原來(lái)的窗口中打開(kāi)新的窗口(不同名)
本文給大家介紹使用window.open在原來(lái)的窗口中打開(kāi)新的窗口,涉及到win.open新窗口相關(guān)知識(shí),對(duì)本文感興趣的朋友參考下2015-11-11