JavaScript建構函式及原型物件
JavaScript中沒有類的概念,所以其在物件建立方面與面嚮物件語言有所不同。
JS中物件可以定義為”無序屬性的集合”。其屬性可以包含基本值,物件以及函式。物件實質上就是一組沒有特定順序的值,物件中每個屬性、方法都有一個名字,每個名字都對映到了一個值,因此我們可以將物件想象稱為一個散列表。
JS是一種基於物件的語言,物件的概念在JS體系中十分的重要,因此有必要清楚地瞭解一下JS中物件建立的常用方法及各自的侷限性。
使用Object或物件字面量建立物件
在說工廠模式建立物件之前,我們不妨回顧一下JS中最基本的建立物件的方法,比如說我想建立一個student物件怎麼辦?最簡單地,new一個Object:
var student = new Object();
student.name = "easy";
student.age = "20";
這樣,一個student物件就建立完畢,擁有2個屬性name
以及age
,分別賦值為"easy"
和20
。
如果你嫌這種方法有一種封裝性不良的感覺,我們也可以使用物件字面量的方式來建立student物件:
var sutdent = {
name : "easy",
age : 20
};
這樣看起來似乎就完美了。但是馬上我們就會發現一個十分尖銳的問題:當我們要建立同類的student1,student2,…,studentn時,我們不得不將以上的程式碼重複n次。
var sutdent1 = {
name : "easy1",
age : 20
};
var sutdent2 = {
name : "easy2",
age : 20
};
...
var sutdentn = {
name : "easyn",
age : 20
};
能不能像工廠車間那樣,有一個車床就不斷生產出物件呢?我們看”工廠模式”。
工廠模式建立物件
JS中沒有類的概念,那麼我們不妨就使用一種函式將以上物件建立過程封裝起來以便於重複呼叫,同時可以給出特定介面來初始化物件:
function createStudent(name, age) { var obj = new Object(); obj.name = name; obj.age = age; return obj; } var student1 = createStudent("easy1", 20); var student2 = createStudent("easy2", 20); ... var studentn = createStudent("easyn", 20);
這樣一來我們就可以通過createStudent函式源源不斷地”生產”物件了。看起來已經高枕無憂了,但貪婪的人類總有不滿足於現狀的天性:我們不僅希望”產品”的生產可以像工廠車間一般源源不斷,我們還想知道生產的產品究竟是哪一種型別的。
比如說,我們同時又定義了”生產”水果物件的createFruit()函式:
function createFruit(name, color) {
var obj = new Object();
obj.name = name;
obj.color = color;
return obj;
}
var v1 = createStudent("easy1", 20);
var v2 = createFruit("apple", "green");
對於以上程式碼建立的物件v1、v2,我們用instanceof操作符去檢測,他們統統都是Object型別。我們的當然不滿足於此,我們希望v1是Student型別的,而v2是Fruit型別的。為了實現這個目標,我們可以用自定義建構函式的方法來建立物件。
建構函式模式建立物件
在上面建立Object這樣的原生物件的時候,我們就使用過其建構函式:
var obj = new Object();
在建立原生陣列Array型別物件時也使用過其建構函式:
var arr = new Array(10); //構造一個初始長度為10的陣列物件
在進行自定義建構函式建立物件之前,我們首先了解一下建構函式
和普通函式
有什麼區別。
其一,實際上並不存在建立建構函式的特殊語法,其與普通函式唯一的區別在於呼叫方法。對於任意函式,使用new操作符呼叫,那麼它就是建構函式;不使用new操作符呼叫,那麼它就是普通函式。
其二,按照慣例,我們約定建構函式名以大寫字母開頭,普通函式以小寫字母開頭,這樣有利於顯性區分二者。例如上面的new Array(),new Object()。
其三,使用new操作符呼叫建構函式時,會經歷(1)建立一個新物件;(2)將建構函式作用域賦給新物件(使this指向該新物件);(3)執行建構函式程式碼;(4)返回新物件;4個階段。
瞭解了建構函式
和普通函式
的區別之後,我們使用建構函式將工廠模式
的函式重寫,並新增一個方法屬性:
function Student(name, age) {
this.name = name;
this.age = age;
this.alertName = function(){
alert(this.name)
};
}
function Fruit(name, color) {
this.name = name;
this.color = color;
this.alertName = function(){
alert(this.name)
};
}
這樣我們再分別建立Student和Fruit的物件:
var v1 = new Student("easy", 20);
var v2 = new Fruit("apple", "green");
這時我們再來用instanceof操作符來檢測以上物件型別就可以區分出Student以及Fruit了:
alert(v1 instanceof Student); //true
alert(v2 instanceof Student); //false
alert(v1 instanceof Fruit); //false
alert(v2 instanceof Fruit); //true
alert(v1 instanceof Object); //true 任何物件均繼承自Object
alert(v2 instanceof Object); //true 任何物件均繼承自Object
這樣我們就解決了工廠模式
無法區分物件型別的尷尬。那麼使用構造方法來建立物件是否已經完美了呢?
我們知道在JS中,函式是物件。那麼,當我們例項化不止一個Student物件的時候:
var v1 = new Student("easy1", 20);
var v2 = new Student("easy2", 20);
...
var vn = new Student("easyn", 20);
其中共同的alertName()
函式也被例項化了n次,我們可以用以下方法來檢測不同的Student物件並不共用alertName()
函式:
alert(v1.alertName == v2.alertName); //flase
這無疑是一種記憶體的浪費。我們知道,this物件是在執行時基於函式的執行環境進行繫結的。在全域性函式中,this物件等同於window;在物件方法中,this指向該物件。在上面的建構函式中:
this.alertName = function(){
alert(this.name)
};
我們在建立物件(執行alertName函式之前)時,就將alertName()函式繫結在了該物件上。我們完全可以在執行該函式的時候再這樣做,辦法是將物件方法移到建構函式外部:
function Student(name, age) {
this.name = name;
this.age = age;
this.alertName = alertName;
}
function alertName() {
alert(this.name);
}
var stu1 = new Student("easy1", 20);
var stu2 = new Student("easy2", 20);
在呼叫stu1.alert()
時,this物件才被繫結到stu1上。
我們通過將alertName()函式定義為全域性函式,這樣物件中的alertName屬性則被設定為指向該全域性函式的指標。由此stu1和stu2共享了該全域性函式,解決了記憶體浪費的問題。
但是,通過全域性函式的方式解決物件內部共享的問題,終究不像一個好的解決方法。如果這樣定義的全域性函式多了,我們想要將自定義物件封裝的初衷便幾乎無法實現了。更好的方案是通過原型物件模式來解決。
原型模式建立物件
函式的原型物件
在瞭解如何使用原型模式建立物件之前,有必要先搞清楚什麼是原型物件。
我們建立的每一個函式都有一個prototype屬性,該屬性是一個指標,該指標指向了一個物件。對於我們建立的建構函式,該物件中包含可以由所有例項共享的屬性和方法。如下如所示:
在預設情況下,所有原型物件會自動包含一個constructor屬性,該屬性也是一個指標,指向prototype所在的函式:
物件例項和原型物件的關聯
在呼叫建構函式建立新的例項時,該例項的內部會自動包含一個[[Prototype]]指標屬性,該指標指便指向建構函式的原型物件。注意,這個指標關聯的是例項與建構函式的原型物件
而不是例項與建構函式
:
使用原型模型建立物件
直接在原型物件中新增屬性和方法
瞭解了原型物件之後,我們便可以通過在建構函式原型物件中新增屬性和方法來實現物件間資料的共享了。例如:
function Student() {
}
Student.prototype.name = "easy";
Student.prototype.age = 20;
Student.prototype.alertName = function(){
alert(this.name);
};
var stu1 = new Student();
var stu2 = new Student();
stu1.alertName(); //easy
stu2.alertName(); //easy
alert(stu1.alertName == stu2.alertName); //true 二者共享同一函式
以上程式碼,我們在Student的protptype物件中添加了name、age屬性以及alertName()方法。但建立的stu1和stu2中並不包含name、age屬性以及alertName()方法,而只包含一個[[prototype]]指標屬性。當我們呼叫stu1.name
或stu1.alertName()
時,是如何找到對應的屬性和方法的呢?
當我們需要讀取物件的某個屬性時,都會執行一次搜尋。首先在該物件中查詢該屬性,若找到,返回該屬性值;否則,到[[prototype]]指向的原型物件中繼續查詢。
由此我們也可以看出另外一層意思:如果物件例項中包含和原型物件中同名的屬性或方法,則物件例項中的該同名屬性或方法會遮蔽原型物件中的同名屬性或方法。原因就是“首先在該物件中查詢該屬性,若找到,返回該屬性值;”
擁有同名例項屬性或方法的示意圖:
上圖中,我們在訪問stu1.name是會得到”EasySir”:
alert(stu1.name); //EasySir
通過物件字面量重寫原型物件
很多時候,我們為了書寫的方便以及直觀上的”封裝性”,我們往往採用物件字面量直接重寫整個原型物件:
function Student() {
}
Student.prototype = {
constructor : Student,
name : "easy",
age : 20,
alertName : function() {
alert(this.name);
}
};
要特別注意,我們這裡相當於用物件字面量重新建立了一個Object物件,然後使Student的prototype指標指向該物件。該物件在建立的過程中,自動獲得了新的constructor屬性,該屬性指向Object的建構函式。因此,我們在以上程式碼中,增加了constructor : Student
使其重新指回Student建構函式。
原型模型建立物件的侷限性
原型模型在物件例項共享資料方面給我們帶來了很大的便利,但通常情況下不同的例項會希望擁有屬於自己單獨的屬性。我們將建構函式模型和原型模型結合使用即可兼得資料共享和”不共享”。
構造與原型混合模式建立物件
我們結合原型模式在共享方法屬性以及建構函式模式在例項方法屬性方面的優勢,使用以下的方法建立物件:
//我們希望每個stu擁有屬於自己的name和age屬性
function Student(name, age) {
this.name = name;
this.age = age;
}
//所有的stu應該共享一個alertName()方法
Student.prototype = {
constructor : Student,
alertName : function() {
alert(this.name);
}
}
var stu1 = new Student("Jim", 20);
var stu2 = new Student("Tom", 21);
stu1.alertName(); //Jim 例項屬性
stu2.alertName(); //Tom 例項屬性
alert(stu1.alertName == stu2.alertName); //true 共享函式
以上,在建構函式中定義例項屬性,在原型中定義共享屬性的模式,是目前使用最廣泛的方式。通常情況下,我們都會預設使用這種方式來定義引用型別變數。