JavaScript 繼承機(jī)制的設(shè)計(jì)思想就是杯聚,原型對(duì)象的所有屬性和方法,都能被實(shí)例對(duì)象共享抒痒。所以我們只要改變對(duì)象的原型幌绍,就可以做到繼承。
先設(shè)置一個(gè)統(tǒng)一的父類Person
// 定義一個(gè)Person類
function Person(name) {
// 屬性
this.name = name || 'Person';
// 實(shí)例方法
this.sleep = function () {
console.log(this.name + '正在偷懶~~~!');
}
}
// 原型方法
Person.prototype.work = function (job) {
console.log(this.name + '的工作是:' + job);
};
1傀广、原型鏈繼承
將父類的實(shí)例作為子類的原型
function Chinese(address) {
this.address = address || "earth";
}
//要在改變?cè)蛯?duì)象之后
Chinese.prototype.skin = '黃色';
Chinese.prototype = new Person();
// Test Code
let chinese = new Chinese('亞洲中國');
console.log(chinese.skin);//undefined
console.log(chinese.name);//Person
console.log(chinese.address);//亞洲中國
chinese.work('IT');//Person的工作是:IT
chinese.sleep();//Person正在偷懶~~~颁独!
console.log(chinese instanceof Person); //true
console.log(chinese instanceof Chinese); //true
let beijing = new Chinese('北京');
console.log(beijing.name);//Person
優(yōu)點(diǎn):
- 非常純粹的繼承關(guān)系,實(shí)例是子類的實(shí)例伪冰,也是父類的實(shí)例誓酒;
- 父類新增原型方法/原型屬性,子類都能訪問到糜值;
- 簡單丰捷,易于實(shí)現(xiàn)。
缺點(diǎn):
- 要想為子類新增原型屬性和方法寂汇,得注意一下要在改變?cè)蛯?duì)象之后病往,如上面代碼的skin原型屬性,(算不上缺點(diǎn)骄瓣,注意點(diǎn))停巷;
- 無法實(shí)現(xiàn)多繼承;
- 來自原型對(duì)象的所有屬性被所有實(shí)例共享榕栏;
- 創(chuàng)建子類實(shí)例時(shí)畔勤,無法向父類構(gòu)造函數(shù)傳參。
2扒磁、構(gòu)造繼承
使用父類的構(gòu)造函數(shù)來增強(qiáng)子類實(shí)例庆揪,等于是復(fù)制父類的實(shí)例屬性給子類(沒用到原型)
function Chinese(address,name) {
Person.call(this,name);
this.address = address || "earth";
}
//Test Code
let chinese = new Chinese('亞洲中國','北京');
console.log(chinese.name);//北京
console.log(chinese.address);//亞洲中國
chinese.sleep();//北京正在偷懶~~~!
console.log(chinese instanceof Person); //false
console.log(chinese instanceof Chinese); //true
let shanghai = new Chinese('亞洲中國','上海');
console.log(shanghai.name);//上海
console.log(shanghai.address);//亞洲中國
shanghai.sleep();//上海正在偷懶~~~妨托!
console.log(shanghai instanceof Person); //false
console.log(shanghai instanceof Chinese); //true
chinese.work('IT');//Uncaught TypeError: chinese.work is not a function
優(yōu)點(diǎn):
- 解決了1中缸榛,子類實(shí)例共享父類引用屬性的問題;
- 創(chuàng)建子類實(shí)例時(shí)兰伤,可以向父類傳遞參數(shù)内颗;
- 可以實(shí)現(xiàn)多繼承(call多個(gè)父類對(duì)象)。
缺點(diǎn):
- 實(shí)例并不是父類的實(shí)例敦腔,只是子類的實(shí)例均澳;
- 只能繼承父類的實(shí)例屬性和方法,不能繼承原型屬性/方法符衔;(上面代碼中原型方法work就會(huì)報(bào)錯(cuò))找前;
- 無法實(shí)現(xiàn)函數(shù)復(fù)用,每個(gè)子類都有父類實(shí)例函數(shù)的副本判族,影響性能躺盛。
3、實(shí)例繼承
為父類實(shí)例添加新特性五嫂,作為子類實(shí)例返回
function Chinese(address,name) {
this.address = address || "earth";
var instance = new Person();
instance.name = name || '北京人';
return instance;
}
// Test Code
var chinese = new Chinese('亞洲中國','上海人');
console.log(chinese.name);//上海人
chinese.sleep();//上海人正在偷懶~~~!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // false
優(yōu)點(diǎn):
- 不限制調(diào)用方式,不管是new 子類()還是子類(),返回的對(duì)象具有相同的效果沃缘。
缺點(diǎn):
- 實(shí)例是父類的實(shí)例躯枢,不是子類的實(shí)例;
- 不支持多繼承槐臀。
4锄蹂、拷貝繼承
把父對(duì)象的屬性,全部拷貝給子對(duì)象
function deepCopy(p, c) {
var c = c || {};
let mp = new p();
for (var i in mp) {
if (typeof mp[i] === 'object') {
c[i] = (mp[i].constructor === Array) ? [] : {};
deepCopy(mp[i], c[i]);
} else {
c[i] = mp[i];
}
}
return c;
}
function Chinese(address) {
this.address = address || "earth";
}
let chinese= new deepCopy(Person,Chinese);
console.log(chinese.name);//上海人
chinese.sleep();//上海人正在偷懶~~~水慨!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // false
console.log(chinese instanceof Chinese); // false
上面寫的是深拷貝得糜,遞歸父類所以的屬性和方法。
優(yōu)點(diǎn):
- 支持多繼承晰洒。
缺點(diǎn):
- 效率較低朝抖,內(nèi)存占用高(因?yàn)橐截惛割惖膶傩裕?/li>
- 無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)谍珊。
5治宣、組合繼承
通過調(diào)用父類構(gòu)造,繼承父類的屬性并保留傳參的優(yōu)點(diǎn)砌滞,然后通過將父類實(shí)例作為子類原型侮邀,實(shí)現(xiàn)函數(shù)復(fù)用
function Chinese(address,name) {
Person.call(this,name);//第一次調(diào)用父類構(gòu)造器
this.address = address || "earth";
}
Chinese.prototype = new Person();//第二次調(diào)用父類構(gòu)造器
Chinese.prototype.constructor=Chinese;
let chinese = new Chinese('亞洲中國','上海人');
console.log(chinese.name);//上海人
console.log(chinese.address);//亞洲中國
chinese.sleep();//上海人正在偷懶~~~!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
為了防止原型鏈紊亂贝润,編程時(shí)務(wù)必要遵守一點(diǎn):如果替換了prototype對(duì)象绊茧,那么,下一步必然是為新的prototype對(duì)象加上constructor屬性打掘,并將這個(gè)屬性指回原來的構(gòu)造函數(shù)华畏。
特點(diǎn):
- 彌補(bǔ)了方式2的缺陷,可以繼承實(shí)例屬性/方法胧卤,也可以繼承原型屬性/方法
- 既是子類的實(shí)例唯绍,也是父類的實(shí)例
- 不存在引用屬性共享問題
- 可傳參
- 函數(shù)可復(fù)用
缺點(diǎn):
- 調(diào)用了兩次父類構(gòu)造函數(shù),生成了兩份實(shí)例(子類實(shí)例將子類原型上的那份屏蔽了)
6枝誊、 原型繼承(很多文章喜歡叫寄生組合繼承)
通過這種方式况芒,砍掉父類的實(shí)例屬性,這樣叶撒,在調(diào)用兩次父類的構(gòu)造的時(shí)候绝骚,就不會(huì)初始化兩次實(shí)例方法/屬性,避免的組合繼承的缺點(diǎn)
function Chinese(address, name) {
Person.call(this, name);//第一次調(diào)用父類構(gòu)造器
this.address = address || "earth";
}
(function () {
// 創(chuàng)建一個(gè)沒有實(shí)例方法的類
var Super = function () { };
Super.prototype = Person.prototype;
//將實(shí)例作為子類的原型
Chinese.prototype = new Super();
})();
Chinese.prototype.constructor = Chinese;
let chinese = new Chinese('亞洲中國', '北京人');
console.log(chinese.name);//北京人
console.log(chinese.address);//亞洲中國
chinese.sleep();//北京人正在偷懶~~~祠够!
chinese.work('IT');//北京人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
這種方式是最好的压汪,但是實(shí)現(xiàn)有點(diǎn)復(fù)雜,需要寄托一個(gè)臨時(shí)的類古瓤,這樣在改變子類的prototype對(duì)象時(shí)止剖,僅僅實(shí)例化了一個(gè)擁有父類prototype對(duì)象的類腺阳,并沒有實(shí)例化父類的方法和屬性。
歸根到底穿香,就是想辦法讓子類擁有父類的實(shí)例屬性和方法亭引,還有父類的原型屬性和方法。
總結(jié)
JavaScript的原型繼承實(shí)現(xiàn)的最佳方式就是:
1皮获、定義子類的構(gòu)造函數(shù)焙蚓,并在內(nèi)部用call()調(diào)用希望“繼承”的構(gòu)造函數(shù),并綁定this洒宝;
2购公、借助中間函數(shù)F實(shí)現(xiàn)原型鏈繼承,最好通過封裝的函數(shù)完成(可以復(fù)用)雁歌;
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
3宏浩、繼續(xù)在子類的構(gòu)造函數(shù)的原型上定義新方法。
7将宪、 Object.create()(終極大招)
Object.create()方法創(chuàng)建一個(gè)新對(duì)象绘闷,使用現(xiàn)有的對(duì)象來提供新創(chuàng)建的對(duì)象的__proto__
。
let person = {
name: "Person",
sleep: function(value) {
console.log(`My name is ${this.name}. I am ${value}`);
}
};
let child = Object.create(person);
console.log(child.name);//Person
child.name='child';
console.log(child.name);//child
child.sleep('sleeping!');//My name is child. I am sleeping!
上例中较坛,child通過Object.create()創(chuàng)建印蔗,原型為對(duì)象person。child具有person的所以屬性和方法丑勤。
7.1 語法
Object.create(proto, [propertiesObject])
參數(shù):
proto
:新創(chuàng)建對(duì)象的原型對(duì)象华嘹。
propertiesObject
:可選。如果沒有指定為 undefined
法竞,則是要添加到新創(chuàng)建對(duì)象的可枚舉屬性(即其自身定義的屬性耙厚,而不是其原型鏈上的枚舉屬性)對(duì)象的屬性描述符以及相應(yīng)的屬性名稱。這些屬性對(duì)應(yīng)Object.defineProperties()
方法直接在一個(gè)對(duì)象上定義新的屬性或修改現(xiàn)有屬性岔霸,并返回該對(duì)象薛躬。")的第二個(gè)參數(shù)。
返回值
一個(gè)新對(duì)象呆细,帶著指定的原型對(duì)象和屬性型宝。
第六點(diǎn)中的繼承進(jìn)行改造
//將這個(gè)方法改造
(function () {
// 創(chuàng)建一個(gè)沒有實(shí)例方法的類
var Super = function () { };
Super.prototype = Person.prototype;
//將實(shí)例作為子類的原型
Chinese.prototype = new Super();
})();
//使用Object.create(),實(shí)現(xiàn)
Chinese.prototype =Object.create(Person.prototype);
完整代碼如下:
// 定義一個(gè)Person類
function Person(name) {
// 屬性
this.name = name || "Person";
// 實(shí)例方法
this.sleep = function() {
console.log(this.name + "正在偷懶~~~!");
};
}
// 原型方法
Person.prototype.work = function(job) {
console.log(this.name + "的工作是:" + job);
};
function Chinese(address, name) {
Person.call(this, name); //第一次調(diào)用父類構(gòu)造器
this.address = address || "earth";
}
Chinese.prototype = Object.create(Person.prototype);
Chinese.prototype.constructor = Chinese;
let chinese = new Chinese("亞洲中國", "北京人");
console.log(chinese.name); //北京人
console.log(chinese.address); //亞洲中國
chinese.sleep(); //北京人正在偷懶~~~絮爷!
chinese.work("IT"); //北京人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
是不是比上面都要簡單趴酣,其實(shí)就兩步:
第一步使用Object.create,創(chuàng)建一個(gè)實(shí)例對(duì)象(__proto__
指向父類的prototype)坑夯,然后把子類的prototype對(duì)象指向這個(gè)對(duì)象岖寞,這樣:子類的prototype指向Object.create創(chuàng)建的實(shí)例對(duì)象,這個(gè)實(shí)例對(duì)象的__proto__
又指向父類的prototype柜蜈。
記得要改變子類的constructor的指向仗谆,將constructor屬性指回原來的構(gòu)造函數(shù)指巡。
第二步在之類中調(diào)用父類構(gòu)造器。
這樣就完美隶垮,繼承了父類的私有屬性和方法厌处,也繼承了父類的原型鏈上的方法和屬性。
如果你希望能繼承到多個(gè)對(duì)象岁疼,則可以使用混入的方式
//子類
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 繼承一個(gè)父類
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它類
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do a thing
};
Object.assign會(huì)把 OtherSuperClass
原型上的函數(shù)拷貝到 MyClass
原型上,使 MyClass 的所有實(shí)例都可用 OtherSuperClass 的方法缆娃。
PS:
我們知道JavaScript的對(duì)象模型是基于原型實(shí)現(xiàn)的捷绒,特點(diǎn)是簡單,缺點(diǎn)是理解起來比傳統(tǒng)的類-實(shí)例模型要困難贯要,最大的缺點(diǎn)是繼承的實(shí)現(xiàn)需要編寫大量代碼暖侨,并且需要正確實(shí)現(xiàn)原型鏈。其實(shí)新的關(guān)鍵字class從ES6開始正式被引入到JavaScript中崇渗。class的目的就是讓定義類更簡單字逗。有興趣的可以去找些資料看看,和java的class一樣的用法宅广。
Class繼承
class Earth {
work(job) {
console.log(this.name + '的工作是:' + job);
}
}
class Person extends Earth {
constructor(name) {
super();// 記得用super調(diào)用父類的構(gòu)造方法!
this.name = name;
}
sleep() {
console.log(this.name + '正在偷懶~~~葫掉!');
}
}
class Chinese extends Person {
constructor(name, address) {
super(name); // 記得用super調(diào)用父類的構(gòu)造方法!
this.address = address;
}
}
let mChinese = new Chinese('上海');
mChinese.sleep();//上海正在偷懶~~~!
mChinese.work('IT');//上海的工作是:IT
是不是很簡單o(∩_∩)o 哈哈跟狱!注意一下俭厚,不是所有的主流瀏覽器都支持ES6的class。如果一定要現(xiàn)在就用上驶臊,記得babel工具轉(zhuǎn)碼挪挤。