一、原型、原型鏈
原型是Javascript中的繼承的基礎,JavaScript的繼承主要依靠原型鏈來實現的。
原型
在JavaScript中,我們創建一個函數A(就是聲明一個函數), 就會為該函數創建一個prototype
屬性。而且也會在內存中創建一個對象B,A函數的屬性 prototype 指向這個對象B( 即:prototype的屬性的值是這個對象 )。這個對象B就是函數A的原型對象,簡稱函數的原型。這個原型對象B 默認會有一個屬性 constructor, constructor屬性指向函數A ( 意思就是說:constructor屬性的值是函數A )。
看下面的代碼:
<body>
<script type="text/javascript">
/*
聲明一個函數,則這個函數默認會有一個屬性叫 prototype 。而且瀏覽器會自動按照一定的規則
創建一個對象,這個對象就是這個函數的原型對象,prototype屬性指向這個原型對象。這個原型對象
有一個屬性叫constructor 執行了這個函數
注意:原型對象默認只有屬性:constructor。其他都是從Object繼承而來,暫且不用考慮。
*/
function Person () {
}
</script>
</body>
下面的圖描述了聲明一個函數之后發生的事情:
當把一個函數作為構造函數 (理論上任何函數都可以作為構造函數) 使用new創建對象的時候,那么這個對象就會存在一個默認的不可見的屬性,來指向了構造函數的原型對象。 這個不可見的屬性我們一般用 [[prototype]] 來表示,只是這個屬性沒有辦法直接訪問到。
看下面的代碼:
<body>
<script type="text/javascript">
function Person () {
}
/*
利用構造函數創建一個對象,則這個對象會自動添加一個不可見的屬性 [[prototype]], 而且這個屬性
指向了構造函數的原型對象。
*/
var p1 = new Person();
</script>
</body>
觀察下面的示意圖:
原型鏈
每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。假如讓一個原型對象等於另一個類型的實例,那么此時的原型對象將包含一個指向另一個原型的指針,相應的,另一個原型中也包含一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條,這就是我們說的原型鏈。
實現原型鏈有一種基本模式,代碼如下:
function SuperType() {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.prototype
}
function SubType () {
this.subprototype = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subprototype
}
var instance = new SubType();
alert(instance.getSuperValue()); // true
以上代碼定義了兩個類型:SuperType和SubType。每個類型分別有一個屬性和方法。它們的主要區別是SubType繼承了SuperType,而繼承是通過創建SuperType實例。換句話說,原來存在於SuperType的實例中的所有屬性和方法,現在也存在於SubType.prototype中了。在確立了繼承關系之后,我們給SubType.prototype添加了一個方法,這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法。這個例子中的實例及構造函數和原型之間的關系如圖:
二、繼承
繼承的方法:1.原型鏈繼承;2.構造函數繼承;3.組合繼承; 4.原型式繼承; 5.寄生式繼承 ;6.寄生組合式繼承;
我們先定義一個父類
原型鏈繼承
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.Subproperty = false;
}
SubType.prototype = new SuperType(); // 繼承了SuperType
SubType.prototype.getSubValue = function() {
return this.Subproperty ;
}
var instance = new SubType();
alert(instance.getSuperValue()) // true
繼承原理:通過讓子類的原型等於父類的實例,來實現繼承。
優點:繼承了超類型的構造函數的所有屬性和方法。
缺點:1、在創建子類實例時,無法向超類型的構造函數傳參,繼承單一。
2、所有新實例都會共享父類實例的屬性。(原型上的屬性是共享的,一個實例修改了原型引用類型的屬性,另一個實例的原型屬性也會被修改!)
function SuperType() {
this.colors = ['red','blue','green'];
}
function SubType() {
}
SubType.prototype = new SuperType(); // 繼承方法
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red','blue','green','black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red','blue','green','black']
構造函數繼承
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
SuperType.call(this); // 繼承了SuperType
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green']
在子類的內部調用父類,通過call改變父類中this的指向。等於是復制父類的實例屬性給子類。
優點:可以在子類構造函數中,向超類型構造函數傳遞參數。
缺點:只繼承了父類構造函數的屬性,沒有繼承父類原型的屬性。所有的方法都在構造函數中定義,無法實現復用,影響性能。
組合繼承 (原型鏈+構造函數)
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;
}
SubType.prototype = new SuperType(); // 繼承方法
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance1 = new SubType('xxx', 15);
instance1.colors.push('black');
console.log(instance1.colors); // ['red','blue','green','black']
instance1.sayNAme(); // xxx
instance1.sayAge(); // 15
var instance2 = new SubType('yyy', 12);
console.log(instance2.colors); // ['red','blue','green']
instance2.sayNAme(); // yyy
instance2.sayAge(); // 12
使用原型鏈實現對原型屬性和方法的繼承,通過構造函數來實現對實例屬性的繼承。這樣既通過在原型上定義方法實現了函數的復用,又能夠保證每個實例都有它自己的屬性。
缺點:調用兩次父類構造函數。
三、與原型有關的幾個屬性和方法
2.1 prototype屬性
prototype 存在於構造函數中 (其實任意函數中都有,只是不是構造函數的時候prototype我們不關注而已) ,他指向了這個構造函數的原型對象。
參考前面的示意圖。
2.2 constructor屬性
constructor屬性存在於原型對象中,他指向了構造函數
看下面的代碼:
<script type="text/javascript">
function Person () {
}
alert(Person.prototype.constructor === Person); // true
var p1 = new Person();
//使用instanceof 操作符可以判斷一個對象的類型。
//typeof一般用來獲取簡單類型和函數。而引用類型一般使用instanceof,因為引用類型用typeof 總是返回object。
alert(p1 instanceof Person); // true
</script>
我們根據需要,可以Person.prototype 屬性指定新的對象,來作為Person的原型對象。
但是這個時候有個問題,新的對象的constructor屬性則不再指向Person構造函數了。
看下面的代碼:
<script type="text/javascript">
function Person () {
}
//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數
Person.prototype = {
name:"志玲",
age:20
};
var p1 = new Person();
alert(p1.name); // 志玲
alert(p1 instanceof Person); // true
alert(Person.prototype.constructor === Person); //false
//如果constructor對你很重要,你應該在Person.prototype中添加一行這樣的代碼:
/*
Person.prototype.constructor = Person; //讓constructor重新指向Person函數
*/
</script>
2.3 _ _ proto _ _屬性(注意:左右各是2個下划線)
用構造方法創建一個新的對象之后,這個對象中默認會有一個不可訪問的屬性 [[prototype]] , 這個屬性就指向了構造方法的原型對象。
但是在個別瀏覽器中,也提供了對這個屬性[[prototype]]的訪問(chrome瀏覽器和火狐瀏覽器。ie瀏覽器不支持)。訪問方式:p1.proto
但是開發者盡量不要用這種方式去訪問,因為操作不慎會改變這個對象的繼承原型鏈。
<script type="text/javascript">
function Person () {
}
//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數
Person.prototype = {
constructor : Person,
name:"志玲",
age:20
};
var p1 = new Person();
alert(p1.__proto__ === Person.prototype); //true
</script>
2.4 hasOwnProperty() 方法
大家知道,我們用去訪問一個對象的屬性的時候,這個屬性既有可能來自對象本身,也有可能來自這個對象的[[prototype]]屬性指向的原型。
那么如何判斷這個對象的來源呢?
hasOwnProperty方法,可以判斷一個屬性是否來自對象本身。
<script type="text/javascript">
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
//sex屬性是直接在p1屬性中添加,所以是true
alert("sex屬性是對象本身的:" + p1.hasOwnProperty("sex"));
// name屬性是在原型中添加的,所以是false
alert("name屬性是對象本身的:" + p1.hasOwnProperty("name"));
// age 屬性不存在,所以也是false
alert("age屬性是存在於對象本身:" + p1.hasOwnProperty("age"));
</script>
所以,通過hasOwnProperty這個方法可以判斷一個對象是否在對象本身添加的,但是不能判斷是否存在於原型中,因為有可能這個屬性不存在。
也即是說,在原型中的屬性和不存在的屬性都會返回fasle。
如何判斷一個屬性是否存在於原型中呢?
2.5 in 操作符
in操作符用來判斷一個屬性是否存在於這個對象中。但是在查找這個屬性時候,現在對象本身中找,如果對象找不到再去原型中找。換句話說,只要對象和原型中有一個地方存在這個屬性,就返回true
<script type="text/javascript">
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
alert("sex" in p1); // 對象本身添加的,所以true
alert("name" in p1); //原型中存在,所以true
alert("age" in p1); //對象和原型中都不存在,所以false
</script>
回到前面的問題,如果判斷一個屬性是否存在於原型中:
如果一個屬性存在,但是沒有在對象本身中,則一定存在於原型中。
<script type="text/javascript">
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
//定義一個函數去判斷原型所在的位置
function propertyLocation(obj, prop){
if(!(prop in obj)){
alert(prop + "屬性不存在");
}else if(obj.hasOwnProperty(prop)){
alert(prop + "屬性存在於對象中");
}else {
alert(prop + "對象存在於原型中");
}
}
propertyLocation(p1, "age");
propertyLocation(p1, "name");
propertyLocation(p1, "sex");
</script>