關於JS繼承
關於JS繼承
關於繼承這個概念,是由面向物件衍生出來的。而JS身為一種基於面向物件而設計的語言,它和正統的面嚮物件語言又是有差別的。
面嚮物件語言
身為面嚮物件語言需要對開發者提供四種能力:
①:封裝 - 把相關的資訊(無論資料或方法)儲存在物件中的能力;
②:聚集 - 把一個物件儲存在另一個物件內的能力;
③:多型 - 編寫能以多種方法執行的函式或方法的能力;
④:繼承 - 有另一個或多個類得來類的屬性和方法的能力;
雖然JS都具備這種能力,然而它卻和正統的面嚮物件語言有差異,具體表現主要在第③、④點。
多型
多型主要有兩種實現形式:
①:過載(編譯時多型性) - 方法名相同,形參個數或型別不同;
②:重寫(執行時多型性) - 在繼承中,子類重寫父類的方法;
這兩種形式上JS和java/C#是表現形式是不一樣的。
過載:
// java程式碼示例
public void sum(int n, float m){
}
public void sum(int n, float m, int t){
}
public void sum(int n, float m, String t){
}
在java中這sum方法會分別在不同情況下呼叫。
// js程式碼示例
function sum(n,m){
}
function sum(n,m,t){
}
在JS中sum方法在前面定義的會被後面定義的覆蓋,因此JS用相同方法處理不用情況只能在sum方法內部對資料做識別判斷了。
重寫:
//java程式碼示例 public class Main { public static void main(String[] args) { Student a = new Student(); a.say(); } } class person{ public void say(){ System.out.println("11111"); } } class Student extends person{ public void say(){ System.out.println("22222"); } }
這裡Student繼承了person,對say方法進行了重寫,雖然重寫了say方法,但是父類方法不會被改變。
//js程式碼示例 function A(m){ this.m = m; } A.prototype.getM = function(){ console.log(100,this.m) } function B(n){ this.n = n; } B.prototype = new A(100); B.prototype.getN = function(){ console.log(this.n) } let b = new B(200); b.getM(); //列印 100,100 b.getM = function(){ console.log(1) } b.getM(); //列印 1 A.prototype.getM(); //列印 100,undefined b.__proto__.__proto__.getM = function(){ console.log(0,this.m) }; b.getM(); //列印 1 A.prototype.getM(); //列印 0,undefined
而JS對繼承的父類的重寫就有點不一樣了,JS是基於原型鏈的繼承。呼叫方法或物件時會在原型鏈中追溯,而當在例項b中添加了getM方法時,其實是攔截了原型鏈上的getM,因此這種攔截不會改動到A.prototype.getM,然而A.prototype.getM又不是絕對的私密的,在示例中可以看到利用b.__proto__.__proto__.getM重寫getM是會導致A.prototype.getM也被重寫的,所以js原型鏈繼承中是沒有完整意義上的私密屬性的。
繼承
和其他正統的面嚮物件語言不一樣,其他語言的繼承一般都是拷貝繼承,也就是子類會把父類中的方法和屬性拷貝的子類中實現繼承,而JS的繼承是把父類的原型放在子類的原型鏈上,利用原型鏈的查詢機制實現繼承的。
而且JS的繼承方式也是很多的,下面列舉一下比較常見的繼承方式。
一、原型鏈繼承
function A(m){
this.m = m;
}
A.prototype.getM = function(){
console.log(this.m)
}
function B(n){
this.n = n;
}
B.prototype = new A(100);
B.prototype.getN = function(){
console.log(this.n)
}
let b = new B(200);
b.getN(); //輸出 200
b.getM(); //輸出 100
特點:
1、非常純粹的繼承關係,例項是子類的例項,也是父類的例項;
2、父類新增屬性或其原型新增屬性,子類都能訪問;
缺點:
1、子類能重寫父類方法,如:b.__proto__.__proto__.getM = null,可以改寫父類方法;
2、父類的私有屬性和公有屬性都變成子類的公有屬性;
3、繼承時方法B原型上原有的方法或屬性會丟失,如:constructor建構函式;(解決:B.prototype = B);
4、子類建立例項時無法向父類傳參;
5、無法實現多繼承;
二、建構函式繼承
function A(m, n){
this.m=m;
this.n=n;
}
function B(m, n, t){
A.call(this, m, n);
this.t=t;
}
var b = new B(1, 2, 3);
console.log(b.m, b.n, b.t);
特點:
1、能在子類建立例項時向父類傳遞引數;
2、可以實現多繼承;
缺點:
1、只能繼承父類的屬性(不繼承原型鏈);
2、建立的例項攜帶著父類屬性,臃腫;
三、寄生組合繼承
function B(n){
this.n = n;
}
B.prototype.getB = function(name){
console.log(this.n, name);
}
function A(m){
B.call(this,200)
this.m = m;
}
// 我是舊時代的殘黨,新時代沒有適合承載我的船
// (function(){
// // 建立一個沒有例項方法的類
// var Super = function(){};
// Super.prototype = B.prototype;
// //將例項作為子類的原型
// A.prototype = new Super();
// })();
// ECMAScript 5 通過新增 Object.create()方法規範化了原型式繼承。
A.prototype = Object.create(B.prototype);
A.prototype.constructor = A;
A.prototype.getA = function(name){
console.log(this.m, name)
}
var aa = new A(100);
aa.getA("A");
aa.getB("B");
特點:
1、可以繼承父類的屬性及其原型上的屬性;
2、既是子類的例項,也是父類的例項;
3、不存在引用屬性共享問題;
4、父類可接收傳參;