JavaScript 建立物件與繼承總結和回顧

常好樂發表於2017-09-19

建立模擬“類”物件:

Javascript沒有”類“的概念。但可以通過建構函式和物件結合的形式來模擬出一個“類”的形式出來。並通過這樣的形式,做到“類”的私有封裝和公有屬性和方法,甚至可以做到繼承的目的。

工廠模式:

function createPerson(name,age,job){  
 var obj = new Object();  
 obj.name = name;  
 obj.age = age;  
 obj.job = job;  
 o.sayName = function(){  
    alert(this.name);  
};  
 return obj;  
}  
var person1 = createPerson(Joson,22,Teacher)  
var person2 = createPerson(Matt,24,Doctor)複製程式碼

實際上,工廠模式之所以叫做工廠模式,就是將一個 obj的物件放進一個具體函式裡面去加工,這裡的例子裡 function createPerson函式就相當於一個工廠。然後對工廠裡面屬性和方法進行賦值和定義,在工廠加工的最後,一定要返回這個對obj象。當在外面引用這個函式並例項化時,這時候,這個obj物件的工廠就起到了一個“類”的作用。

建構函式模式:

function Person(name,age,job){  
 this.name = name;  
 this.age = age;  
 this.job = job;  
 this.sayName = function(){  
    alert(this.name);  
};  
}  
var person1 = New Person(Joson,22,Teacher)  
var person2 = New Person(Matt,24,Doctor)複製程式碼

建構函式模式在工廠模式基礎上做出的改動在於:
1.沒有顯式建立obj物件
2.直接將屬性和方法賦給this (這裡的this指向後面New 出來的Person物件)
3.沒有return語句
注意,在建構函式模式中,建構函式的函式名首字母一定要大寫!

建構函式只是將工廠模式在函式內部顯式建立obj物件改為了在外部 New的地方隱式建立物件。並且將this指標指向這個隱式建立的物件中來。
因此,這裡New語句乾的事有:
1.在Person建構函式內部建立一個物件。
2.將this指標指向這個建立的物件,也就是改變作用域。
3.在建構函式中隱式return這個物件。
4.呼叫這個建構函式,並例項化。

所以,如果建立一個函式,當僅僅只用宣告函式的方式去呼叫它,那麼它就是一個普通的函式(其中的this就指向了window),當用New的方式去呼叫一個函式,它就成了一個建構函式。
建構函式模式的問題在於,建構函式內部的屬性和方法都是私有的,外部無法訪問到內私有的屬性和方法,並且不同例項化後的物件的相同方法,也都是不同的。例如,上面的person1 和 person2,如果我們alert(person1.sayName === person2.sayName);它將返回false。因為person1和person2例項化訪問的都是獨立且不公有的屬性和方法。

原型模式:

在建立每個函式之初,js會自動有一個prototype(原型)屬性,這個屬性是一個指標,指向該函式的一個後花園物件。而這個所謂的後花園的物件就可以封裝一些特定型別的屬性和方法,這些屬性和方法很棒在於他們都是共享共有的屬性和方法。

function Person(){  
};  
 Person.prototype.name = Jason; Person.prototype.age = 22   
Person.prototype.job = Teacher; Person.prototype.sayName = function(){  
 alert(this.name);  
}  
var person1 = New Person()person1.sayName();  
var person2 = New Person()person2.sayName();  
alert(person1,sayName = person2.sayName)複製程式碼

此時Person函式為一個空函式,由於後面又用了New語句將Person函式變成了物件。所有的屬性和方法定義在該Person物件的原型(prototype)上。而原型具有共有共享例項屬性和方法的特性,所以最後的alert最後會返回一個true。
這裡也可以用alert(Person.prototype.isPrototypeOf(person1))來檢測該例項是否指向Person的原型。
在原型模式中:
每個例項裡有一個[[prototype]]指標,他始終指向建構函式的原型。程式碼在讀取某個物件的某個屬性時,優先檢索例項內部的屬性,若例項內部沒有,再去通過[[prototype]]指標,去檢索原型裡有沒有這個屬性和方法。
每個原型中都有一個constructor屬性,它也是一個指標,指向該建構函式內部。所以Person.prototype.constructor === Person。原型模式也正是因為有constructor這個屬性才達到了共享屬性和方法的效果。
注意一點,雖然可以通過物件的例項訪問儲存在原型中的屬性和方法,但卻不能通過物件例項重寫修改原型中的屬性和方法。如果我們在外部例項寫一個
person1.name = "Fred"
最後alert(person1.name)返回Fred,這個Fred只是遮蔽了原型中的Jason,但並沒有修改原型中的Jason,也就是說alert(person2.name)時,還是會返回原型中的Jason。用delete方法可以去掉這種例項上的遮蔽:
delete person1.name;
alert(person1.name)返回Jason
使用hasOwnProperty()方法可以檢查一個屬性是否來自於例項,如果返回true,則來自於例項,如果返回false,則來自於原型。
alert(person1.hasOwnProperty("name"); 返回false則表示這個name屬性來自於原型。
person1.name = "Fred";
alert(person1.hasOwnProperty("name"); 返回true則表示這個name屬性來自於例項。

混合式建構函式模式和原型模式:

原型模式的優點也是它的缺點,只有共有屬性方法,卻沒有私有屬性方法。所有例項都共享一個原型。這是不符合大眾需求的。在原型模式中,我們定義的建構函式為一個空函式。所以,我們只要把自己想要私有的屬性和方法放進建構函式裡面就可以實現屬性和方法的私有化,在外部無法訪問。把想要共有的屬性和方法放進原型中,即可。

極簡主義模式:

上面的混合了建構函式和原型模式寫法的程式碼量還是太多太繁雜了,為此,以後市面上最常用的一種寫法為此:

var Dog= {  
        //此處放共有屬性和方法  

        sound:"汪汪汪"  
        Dog.jump = function(){ dosomething };  


     creatFn: function(){  
    //函式內部放私有方法和屬性  
      var dog= {};  
      dog.name = "狗狗";  
      dog.makeSound = function(){ alert(Dog.sound); };  
                 dog.changeSound = function(param){Dog.sound = param};  
      return dog;  
    }  
  };  
var dog1 = Dog.creatFn();  
  dog1.makeSound(); // 汪汪汪  

var dog2 = Dog.createFn();  
     dog2.changeSound("嗚嗚嗚");  
     dog1.makeSound();//“嗚嗚嗚”複製程式碼

此模式先建立一個Dog物件,在對面內部放置公有的屬性和方法,通過物件字面量的形式建立一個函式,在函式內部再建立一個空的dog物件,在函式內部就可以放置私有的屬性和方法了。最後再在函式內部返回這個dog物件就完成了一個模擬“類”極簡寫法的實現。其實就是在工廠模式外面再包一層物件。

繼承:

原型鏈繼承:

先簡單回顧一下建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標constructor,而每個例項都包含一個指向原型物件的內部指標[[prototype]]。那麼如果我們將一個函式的原型物件指向另一個建構函式的例項,會怎樣呢?顯然,此時就形成了一個原型鏈,原型鏈上則實現了繼承的特性。

function SuperType(){  
   this.SuperItem = true;  
} //建立一個父函式  

SuperType.prototype.getSuperValue = function(){  
   return this.SuperItem;  
} //寫入一個父函式的原型公有方法  

function SubType(){  
   this.SubItem = false;  
} //建立一個子函式  
SubType.prototype = new SuperType();//將父建構函式指向子函式的原型,實現了原型鏈的連結,使子建構函式繼承了父建構函式。SubType.prototype.getSubValue = function(){ return this.SubItem;} //寫入一個子函式的原型公有方法  
SubType.prototype.getSuperVallue = function(){ return false;} //(紅色一段)在SubType的原型中重寫改寫父類中的方法var instance = new SubType();  
alert(instance.getSuperValue)  //返回false 因為被紅色的一行程式碼改寫了複製程式碼

最後的例項化是例項化的子建構函式SubType,最後卻可以去呼叫父建構函式的getSuperValue的方法,則SubType已經繼承了SuperType。
注意寫原型鏈繼承的順序必須是先建立父函式,寫父函式原型,再建立子函式,再做原型鏈的連結,再才去寫子函式的原型,或者像紅色一行程式碼一樣去通過子函式的原型是重寫改寫父函式或父函式原型的方法。並且,紅色那一行程式碼改寫的父函式原型的方法只是遮蔽掉父函式原型的方法,並非真正改寫了SuperType.prototype裡的方法。
原型鏈繼承的缺點還是因為原型的關係,例項出來的instance1和instance2會共享原型鏈中的資料,很多時候我們不需要共享時,則這種方法就不可用了,並且也無法傳參。

別忘了原型:

記住,所有的函式(js內建函式也好,自定義函式也會)的預設原型都是Object的例項。也就是說Object.prototype裡面寫了很多js內建的屬性和方法,比如toString(),valueof()等。
借用建構函式(類式繼承):
借用建構函式就很好地解決了原型鏈繼承的問題,通過借用建構函式模式繼承,例項出來的instance1和instance2的資料不會共享,是相互獨立的。借用建構函式是通過apply()或call()來進行繼承,由於這兩個方法第二個引數可以進行傳參,所以,子函式可以向父函式進行傳參,也解決了原型鏈繼承做不到的問題。

function SuperType(name){  
    this.name = name;  
    this.colors = ["red","blue","green"];  
}  
function SubType(){  
    SuperType.call(this,"Jason");//此處實現SubType對SuperType的繼承,也就是將SuperType的作用域指向了this的作用域(此處this是SubType的作用域),Jaon為向父類傳遞的一個引數  
    this.age = 22; //為了避免SyperType被重寫改寫,此處在子型別中最好再額外加一個子型別自己的屬性。  
}  

var instance1 = new SubType();  //例項化子建構函式  
instance1.colors.push("blac");  
alert(instance1.colors); //"red,blue,green,blac"  
alert(instance1.name); //"Jason"  

var instance2 = new SubType();  
alert(instance1.colors); //"red,blue,green"  
通過借用建構函式的形式,例項化的instance1和instance2各自的函式類的屬性都是相互獨立不共享的。並且可以通過call方法的加入第二個引數從子類向父類傳參。但要想做到既要資料共享又要資料獨立,則也可以進行混合式繼承。複製程式碼

混合式繼承:

混合式繼承則是將私有的屬性和方法封裝在函式體裡面,把共享屬性和方法放在函式的原型裡面即可。並通過SubType.prototype = new SuperType()的方法對共享屬性和方法做繼承連結,通過SuperType.call(this)的方法對私有屬性和方法做繼承連結。

function Parent(age){  
    this.name = ['mike','jack','smith'];  
    this.age = age;  
}  
Parent.prototype.run = function () {  
    return this.name  + ' are both' + this.age;  
};  
function Child(age){  
    Parent.call(this,age);//借用建構函式繼承  
}  
Child.prototype = new Parent();//原型鏈繼承  
var test = new Child(21);  
alert(test.run());//返回mike,jack,smith are both21複製程式碼

混合式繼承也不是沒有缺點,他的缺點在於做了兩次繼承,就呼叫了兩次父函式,這樣做並不是最理想的方法。

原型式繼承:

和上面講的原形鏈繼承只有一字之差,但是不是同一個內容。我們所說的原型式繼承,就是我們上節課所講的,通過JS內建的Object.create()方式來建立新的類。

var box = {  
    name : 'Jaon',  
    arr : ['brother','sister','baba']  
};  
var b1 = Object.create(box,{name:{value:"Greg"}});//第一個引數為要繼承的物件名,接受第二個引數可以去改變原物件裡面的某個屬性值  
alert(b1.name);//"Greg"  

b1.name = 'mike'; //會覆蓋物件裡的屬性  
alert(b1.name);//mike  

alert(b1.arr);//brother,sister,baba  
b1.arr.push('parents');  
alert(b1.arr);//brother,sister,baba,parents  

var b2 = Object.create(box);  
alert(b2.name);//Jason  
alert(b2.arr);//brother,sister,baba,parents複製程式碼

如果低等級瀏覽器沒有這個Object.create()方法,那麼可以自定義這個方法:

function Create(o){  
     function F(){}  
     F.prototype = o;  
     return new F();  
 }//事實上,呼叫這個函式也是在做建構函式的處理複製程式碼

在沒有必要興師動眾地建立建構函式,而僅僅只想讓一個物件做“類”一樣的修改,則完全可以用原型式繼承,不過別忘了,原型式繼承也是為了共享它的屬性和方法。

寄生式繼承:

function creatAnother(original){  
    var clone=new Object(original);  
    clone.sayHi=function(){  
        alert("hi")  
    };  
    return clone;  
}  
var person={  
    name:"haorooms",  
    friends:["hao123","zhansan","lisi"]  
}  

var antherPerson=creatAnother(person);  
antherPerson.sayHi();//hi複製程式碼

寄生式繼承和原型式繼承是一樣的思路。這裡createAnother函式就是一個用語封裝繼承過程的函式(此例子就是將一個sayHi的方法繼承到person物件裡去)。將person物件放到createAnother函式裡去加工一遍,就可以將sayHi方法繼承到person物件裡面去了。

寄生混合式繼承:

由於混合式繼承會有呼叫兩次父函式的缺點,則寄生混合式繼承則可以解決這個問題。

function inheritPrototype (subType,superType) {  
    var prototype = Object.create(superType.prototype);  
    prototype.constructor = subType;  
    subType.prototype = prototype;  
};  

function SuperType (name) {  //父類  
    this.name = name;  
    this.colors = ['red', 'blue', 'green'];  
}  
SuperType.prototype.sayName = function () {    //寫入一個父類原型中的方法  
    alert(this.name);  
}  
function SubType(name, age) {   //子類  
    //繼承屬性  
    SuperType.call(this,name);  
    this.age = age;  
}  
//繼承方法  
inheritPrototype(SubType,SuperType);  

SubType.prototype.sayAge = function () {   //寫入一個子類原型中的方法  
    alert(this.age);  
}  

var instance = new SubType("Jason",22);  
alert(instance.sayName()); //返回Jason  
alert(instance,sayAge());  //返回22複製程式碼

如此,就完美做到了既只呼叫一次父函式,又可以通過借用建構函式繼承私有屬性值和子類對父類進行傳參,又可以通過寄生式繼承繼承共享的方法(私有化是為了在例項化兩個建構函式時,例項1和例項2相互之間是獨立的屬性,共享化則反之,此處不做多餘的具體體現)。

相關文章