今天的內容,與之前原型和原型鏈的知識有聯繫,可以看我的上一篇文章原型、原先鏈。
文章裏有詳細的介紹。因爲繼承的學習是建立在這些知識的基礎之上的。
首先來說下爲什麼要用繼承吧。當我們自定義構造函數並且實例化對象時,總會重複的書寫相同的屬性或者方法,這就讓我們的代碼高耦合了,簡單來說就是簡化程序優化代碼。當對象用到相同的屬性和方法時,只要繼承過來就行了,若需要自己特有的屬性方法再追加即可。這是我個人對爲什麼用繼承的理解。也許是比較片面的,歡迎大家指正。
構造函數實現繼承
構造函數的作用主要是提取了子構造函數實例和父構造函數實例共同的屬性,子構造函數直接繼承父構造函數的屬性,實例化時直接生成。這種方式的優點是減少代碼量,但對性能的優化沒有任何幫助就是單純的減少了代碼量。
構造函數實現繼承的核心是在子構造函數中用call或者apply方法把屬性繼承過來。
function Father(name, age) {
this.name = name;
this.age = age;
}
function Son(name, age) {
Father.call(this, name, age);
// Father.apply(this, [name, age]);
}
這裏Son構造函數中執行call方法的本質是:讓Son中的this對象執行Father構造函數的代碼,最後返回給this對象,Son中this的指向的是 —> Son實例化的對象。
var son = new Son()
this指向son實例對象
原型鏈實現繼承
利用原型鏈來實現繼承是傳統方式的繼承,主要是在我們定義自定義構造函數時,手動的設置構造函數原型對象的指向。
// 默認狀態下所以函數都是繼承自Object 這裏我顯示地寫出來
Person.prototype = Object.prototype;
function Person() {};
var person = new Person();
Father.prototype = person;
Father.prototype.name = 'haha';
function Father() {};
var father = new Father();
Son.prototype = father;
function Son() {};
var son = new Son();
// son沒有name屬性,但是son卻可以訪問,是因爲Son構造函數繼承了Father
// son --> __proto__ --> Son.prototype --> father.__proto__ --> Father.prototype.name
console.log(son.name);
console.log(son.toString()); // 繼承Object
son繼承father,father繼承person,person繼承object。就是這樣一級一級的手動設置原型對象。這種方式會覆蓋掉系統自動分配的原型對象,當我們打印出Son.prototype的原型時,就不是它自己了,就變成了Father.prototype。意思就是說Son.prototype的原型F變成了Father.prototype。
console.log(Son.prototype.__proto__ === Object.prototype); // false
console.log(Son.prototype.__proto__ === Father.prototype); // true
但在改變原型之前Son.prototype的原型是Object.prototype;因爲Object.prototype是一切對象的祖先,它是頂級對象。
// 默認狀態下所以函數都是繼承自Object 這裏我顯示地寫出來
Person.prototype = Object.prototype;
function Person() {};
var person = new Person();
Father.prototype = person;
Father.prototype.name = 'haha';
function Father() {};
var father = new Father();
// Son.prototype = father; Son原型不繼承father
function Son() {};
var son = new Son();
console.log(Son.prototype.__proto__ === Object.prototype); // true
// console.log(son.__proto__ === Son.prototype);
console.log(Son.prototype.__proto__ == Father.prototype); //false
這種繼承方式就是通過原型鏈實現的,從son對象訪問name屬性的整個過程。son先從自身找,本身上沒有name屬性,接着通過__proto__屬性找原型(Son.prototype),發現原型上也沒有,繼續找原型的原型(Son.prototype.__ proto__ === Father.prototype),找到了name屬性,最後輸出。
> son --> proto --> Son.prototype --> father.__ proto__ === Father.prototype --> Father.prototype.name
共享原型實現繼承
上面通過構造函數實現了屬性的繼承,屬性繼承了還需繼承方法。方法的繼承是通過原型實現的,共享原型,讓子構造函數的原型與父構造函數的原型相等,這樣子構造函數實例的對象就可以使用父構造函數上的方法了。
Father.prototype.sayName = function () {
console.log(this.name);
}
function Father(name, age) {
this.name = name;
this.age = age;
}
Son.prototype = Father.prototype; // 共享原型
function Son(name, age) {
Father.call(this, name, age);
// Father.apply(this, [name, age]);
}
var son = new Son('zs', 18);
son.sayName(); // zs
繼承父構造函數的方法時實現了,通常繼承的對象還可以添加自己的屬性和方法的,這裏Son構造函數對象是可以添加自己的屬性和方法。
Son.prototype = Father.prototype;
Son.prototype.height = 180;
Son.prototype.sayAge = function () {
console.log(this.age);
}
function Son(name, age) {
Father.call(this, name, age);
}
var son = new Son('zs', 18);
son.sayName();
son.sayAge();
console.log(son.height);
上面直接把父類構造函數的原型對象賦值給子類構造函數的原型對象,是可以實現繼承。按理說父類 和 子類 是兩個不同的對象,子類雖然繼承了父類的屬性和方法,但子類可以添加自己特有的屬性和方法。
當我們給子類構造函數原型對象添加自己的方法時,子類構造函原型對象會加上該方法,但是,父類構造函數原型對象也會增加這個方法,這樣的效果就不是我們想要的了。
添加自己的東西,不能影響別人,這裏Son添加時,不能影響Father。但是當我們再訪問Father.prototype時,發現添加在Son上的屬性和方法,Father上也有。
console.log(Son.prototype.constructor); // 此時Son打印的是Father 並不是Son
console.log(Father.prototype);
console.log(Son.prototype);
Son.prototype = Father.prototype; 重寫了Son原型對象,此時Son原型得構造器指向了Father而不是Son。所以我們需要讓Son重新指回Son構造函數。
Son.prototype.constructor = Son;
賦值的本質:Son.prototype把Father.prototype對象的地址賦值了一份,也就是Son原型和Father原型指向的是同一地址,引用的是同一個對象,所以當修改Son原型時,Father原型也會變,修改Father原型Son原型也會變,這就沒有達到對象分離得效果。
存在這裏問題的繼承,是不能使用的,發現問題就要解決問題。這就要說接下來的聖盃模式繼承。
相比之下最佳的繼承方案就是在共享原型的基礎上升級後的聖盃模式。
封裝 ---- 聖盃模式繼承
核心:就是把Son原型變成Father的實例對象。因爲Father的實例對象與Father.prototype(原型對象)是兩個對象,這樣就不存在Son.prototype和Father.prototype指向同一地址,引用同一對象。Son.prototype對象通過__proto__屬性就可以訪問到Father.prototype了。
// 3. 原型對象修改方案 ---> Son.prototype = new Father()
Son.prototype = new Father();
// Son.prototype = Father.prototype;
這樣修改,就完美的解決了共享原型的問題。
// 3. 原型對象修改方案 ---> Son.prototype = new Father()
Son.prototype = new Father();
// Son.prototype = Father.prototype;
Son.prototype.height = 180;
Son.prototype.sayAge = function () {
console.log(this.age);
}
Son.prototype.constructor = Son;
function Son(name, age) {
Father.call(this, name, age);
}
var son = new Son('zs', 18);
son.sayName();
son.sayAge();
console.log(son.height);
console.log(Son.prototype.constructor);
console.log(Father.prototype);
console.log(Son.prototype);
封裝inherit函數
// 封裝一個函數
// Origin --> 被繼承對象
// Target --> 繼承對象
// Target 繼承 Origin
function inherit(Target, Origin) {
function F() {};
F.prototype = Origin.prototyp;
Target.prototype = new Origin();
Target.prototype.constructor = Target;
//超類:記錄Target構造函數的原型到底繼承於誰
Target.prototype.uber = Origin.prototype;
}
通過調用函數的方式實現繼承
Father.prototype.name = 'father';
Father.prototype.age = 18;
Father.prototype.sayName = function () {
console.log(this.name);
}
function Father() {};
Son.prototype.name = 'Son';
Son.prototype.age = 18;
function Son() {};
// 繼承之前
var son = new Son();
var father = new Father();
father.sayName();
// son.sayName();
// 繼承之後
inherit(Son, Father);
var son1 = new Son();
son1.name = 'son1';
Son.prototype.sayAge = function () {
console.log(this.age);
}
son1.sayName();
son1.sayAge();
console.log(Son.prototype);
console.log(Father.prototype);
今天結束 —— 繼承了!!!!!!