前面咱們複習了JS建立對象的幾種方式,在裏面也提到了原型和原型鏈的概念,而這兩個東西在咱們這一篇文章中也會常常用到,JS的繼承就是根據原型鏈來進行的。在這裏咱們不討論ES6
中Class
的Extends
繼承,只討論ES5
中常見的六種繼承方式。git
在上一節建立對象中,咱們提到,每個實例都有本身的原型,而這個實例能夠經過prototype
這個屬性找到他對應的原型。原型鏈繼承的原理就來自於這裏。這種繼承方式應該是全部繼承方法裏面最簡單的一個,固然也會存在不少的缺點。該方法繼承的核心在於一個地方:子類型的原型爲父類型原型的一個實例。下面咱們舉一個例子來講明:github
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// 定義一個私有方法
Internet.prototype.getIp = function() {
}
// children
function Iot(mac) {
this.mac = mac;
this.setMac = function() {
}
}
// 子類型的原型爲父類型的一個實例,咱們經過new操做符來建立
Iot.prototype = new Internet(); // 物聯網繼承了互聯網的全部屬性,替換物聯網的原型
let Iotxh = new Iot('6A421');
let Iotce = new Iot('6A410');
console.log(Iotxh);
console.log(Iotce);
複製代碼
這裏咱們能夠在瀏覽器中去查看一下輸出的結果,能夠看到打印出來的對象的__proto__
屬性,也就是上一節提到的指向該實例的構造函數的隱式原型的一個指針,是咱們的Internet
實例,這樣就能夠經過Iotxh.prototype
訪問到父類Internet
上的私有屬性,而後再經過父類實例的prototype
就能夠訪問到父類原型上的方法。因此到最後,咱們父類的全部的私有方法和公有方法和屬性,均可以當作是子類的公有屬性。segmentfault
可是在這裏有兩個地方是須要咱們注意的,分別是:瀏覽器
由於在JS中,咱們操做基本數據類型的值的時候是直接修改其對應內存中的值,而當咱們在修改引用類型的時候,咱們修改的是這個地址所對應的值,全部引用這個地址的變量都將會受到影響,這個在咱們上一節的建立對象當中也提到過。例如咱們修改Iotxh
中的ip
,那麼Iotce
中的ip
也會受到影響。bash
當咱們須要在繼承後的子類中添加新的方法或者是重寫父類的方法的時候,要放到咱們替換原型以後進行。若是在替換原型以前進行,那麼在咱們替換原型以後,原來添加的東西仍是在原來的原型上,最終是無效的。函數
老規矩,前面的方法,確定都是有不少缺陷的,原型鏈繼承的缺陷在於如下幾點: 1.沒法實現多繼承 2.來自原型對象的全部屬性被全部實例共享 3.建立子類實例時,沒有辦法向父類構造函數傳參 4.上面提到的兩個須要注意的地方post
爲了解決部分上面提到的問題,咱們繼續複習構造函數繼承的繼承方式。ui
這個方法的核心在於this
指針的指向問題,也就是說,咱們經過修改父類構造函數的this
指針指向來實現繼承。先擼代碼:this
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// 給父類的原型添加一個方法
Internet.prototype.getIp() {
}
// children
function Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
}
let Iotxh = new Iot('6A421','Weily','192.168.1.1');
console.log(Iotxh);
複製代碼
在這裏的繼承也有一個地方是須要注意的,單純的構造函數的繼承方式,只是實現部分的繼承,由於咱們每次繼承的時候都至關於調用了一次父類型的構造函數,那麼當父類的原型還有構造函數外的方法和屬性,子類是拿不到這些方法和屬性的。例如上面的getIp()方法,就是沒法經過這種方式繼承的。spa
OK,簡單的複習這種繼承方式,咱們談一下他的缺點: 1.建立的實例並非父類的實例,只是子類的實例 2.只能繼承父類的實例屬性和方法,不能繼承原型屬性和方法 3.沒法實現函數複用,每一個子類都有父類的實例函數的副本,浪費內存
按照套路,講完了兩種繼承方式,咱們確定有一種他們結合的更優的繼承方式。這種繼承的方式能夠歸納爲:經過調用父類的構造函數,繼承父類的屬性並能進行傳參,並經過將子類的原型指向父類的實例,實現函數的複用。
老規矩,咱們仍是先擼代碼:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// add methods
Internet.prototype.setNmae = function() {
}
// children 構造繼承
function Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
this.getMac = function() {
}
}
// 原型繼承
Iot.prototype = new Internet();
// 將子類的構造函數從Internet修復到Iot
Iot.prototype.constructor = Iot;
// 爲子類的原型添加新方法
Iot.prototype.getName() {
}
let iotxh = new Iot('6A421', 'Weily', '192.168.1.1');
let iotce = new Iot('6A410', 'ce', '192.168.1.2');
console.log(iotxh);
複製代碼
這種方式融合了前面兩種繼承方式的優勢,是JS中最經常使用的繼承模式,可是並非最優的繼承模式。固然,這種方式也存在必定的缺點,分別是: 1.不管何時都會調用兩次構造函數,第一次調用是在建立子類的原型時,另外一次是在子類的構造函數的內部,子類最終會包含父類的所有實例屬性,但咱們不得不在調用子類構造函數時重寫這些屬性。
這種方式就開發來講,彷佛用得不多,基本上沒有看到使用這種方式進行繼承的,他的原理是Object.creat()
的模擬實現,將傳入的對象做爲建立的對象的原型,先上代碼:
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
// father
let internet = {
name: 'weily',
ip: '192.168.1.1',
person:['ybw','wkm']
}
// 繼承
let iotxh = createObj(internet);
let iotes = createObj(internet);
iotxh.name = 'Weily';
console.log(iotes.name); // weily
iotxh.person.push('ll');
console.log(iotes.person); // ['ybw', 'wkm', 'll']
複製代碼
上面舉了兩個例子,想必你們也看出來了,這種繼承方式的缺點也是會共享值。至於爲何修改name屬性不影響,是由於那行代碼是爲iotxh建立了一個本身的name屬性。
這種繼承方式的核心在於一個地方,構造一箇中間件,讓中間件的prototype
代理父類的prototype
。先上代碼:
// 構造的中間件
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original) {
// 經過調用函數建立一個新對象
var clone = object(original);
// 以某種方式來加強這個對象
clone.sayHi = function() {
alert("hi");
}
return clone;
}
var person = {
name: "Bert",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi
複製代碼
經過中間件代理的方式,咱們可讓子類的原型間接的和父類聯繫起來。
缺點: 每次建立對象的時候都會建立一遍方法。
OK,前面複習了五種繼承方式,各有各的好處,固然缺點也很多,因此,咱們有了最優的繼承方式,寄生組合繼承。
這種繼承方式咱們要達到的效果是,完美繼承父類的屬性和方法,且只調用一次父類的構造函數,最終子類的原型鏈還要保持不變。那麼,咱們須要如何來實現這個需求呢?
回想一下,組合繼承方式已經能完美的實現除了調用一次父類構造函數以外的所有需求,那麼,咱們須要作的就是去解決這一個需求,很巧的是,咱們上面提到的寄生繼承方式,偏偏就是隻調用了一次構造函數,那麼解決辦法就有了,經過中間件,間接的實現這個需求。
在組合繼承中,咱們第二次調用構造函數,是爲了讓子類的原型訪問到父類的原型,那麼咱們經過中間件的方式,也能實現這樣的功能,上代碼:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.person = ['ybw', 'wkm'];
}
// add methods
Internet.prototype.changeName = function() {
}
// children
funciton Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
}
// 中間件
let F = function() {}
F.prototype = Internet.prototype;
Iot.prototype = new F();
let iotxh = new Iot('Weily', '192.168.1.1');
console.log(iotxh);
複製代碼
最後,咱們來封裝一下這個最優的繼承方法,便於之後咱們開發中的使用:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
let prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
prototype(Child, Parent);
複製代碼
OK,咱們的ES5中的六種寄生方式就複習完了,ES6中的extend繼承,咱們後面在複習ES6的時候再來複習。歡迎關注個人博客博客
JavaScript深刻之繼承的多種方式和優缺點 JavaScript常見的六種繼承方式 JavaScript寄生繼承的理解