JavaScript構造函數的prototype屬性
JavaScript中沒有類的概念,所以其在對象創建方面與面向對象語言有所不同。
JS中對象可以定義為”無序屬性的集合”。其屬性可以包含基本值,對象以及函數。對象實質上就是一組沒有特定順序的值,對象中每個屬性、方法都有一個名字,每個名字都映射到了一個值,因此我們可以將對象想象稱為一個散列表。
JS是一種基於對象的語言,對象的概念在JS體系中十分的重要,因此有必要清楚地了解一下JS中對象創建的常用方法及各自的局限性。
- 使用Object或對象字面量創建對象
- 工廠模式創建對象
- 構造函數模式創建對象
- 原型模式創建對象
- 構造與原型混合模式創建對象
使用Object或對象字面量創建對象
在說工廠模式創建對象之前,我們不妨回顧一下
var student = new Object();
student.name = "easy";
student.age = "20";
這樣,一個student對象就創建完畢,擁有2個屬性name以及age,分別賦值為"easy"和20。
如果你嫌這種方法有一種封裝性不良的感覺,我們也可以使用對象字面量的方式來創建student對象:
var sutdent = {
name : "easy",
age : 20
};
這樣看起來似乎就完美了。但是馬上我們就會發現一個十分尖銳的問題:當我們要創建同類的
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) {//相當於java的構造函數,只是沒有類的概念
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()函數,我們可以用以下方法來檢測不同的Student對象並不共用alertName()函數:
alert(v1.alertName == v2.alertName); //flase,因為this指向的是兩個不同的Student對象
我們知道,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.alertName()時,this對象才被綁定到stu1上,在沒有調用時,this對象就等於window。
我們通過將alertName()函數定義為全局函數,這樣對象中的alertName屬性則被設置為指向該全局函數的指針。由此stu1和stu2共享了該全局函數,解決了內存浪費的問題。
但是,通過全局函數的方式解決對象內部共享的問題,終究不像一個好的解決方法。如果這樣定義的全局函數多了,我們想要將自定義對象封裝的初衷便幾乎無法實現了。更好的方案是通過原型對象模式來解決。
原型模式創建對象
- 函數的原型對象
- 對象實例和原型對象的關聯
- 使用原型模型創建對象
- 原型模型創建對象的局限性
函數的原型對象
在了解如何使用原型模式創建對象之前,有必要先搞清楚什麽是原型對象。
我們創建的每一個函數都有一個prototype屬性,該屬性是一個指針,該指針指向了一個對象。對於我們創建的構造函數,該對象中包含可以由所有實例共享的屬性和方法。如下如所示:
Function.prototype===Function.__proto__//結果為true
Object.prototype指向Object構造函數的原型對象,該原型對象中的一些屬性和方法是所有實例共享的,也就是全局的屬性和方法,例如toString ()、valueOf()
Array.prototype指向Array構造函數的原型對象,同上有所有實例共性的屬性個方法。也就是常見的數組方法和屬性。
String.prototype等都是同理。
這個屬性在我們使用js系統或者自己創建的對象的時候,會默認的加上.任何對象都有constructor屬性,繼承自原型的,constructor會指向構造這個對象的構造器或者構造函數。
constructor可以被改寫,所以使用要小心。
在默認情況下,所有原型對象會自動包含一個constructor屬性,該屬性也是一個指針,指向prototype所在的函數:
每個對象都有constructor屬性,該屬性指向這個對象的構造函數。
JavaScript中因為函數也是對象,那麽構造函數也是一個對象。構造函數是函數的一個實例。因此構造函數對象的constructor指向構造函數對象的構造函數,這個構造函數是Function。所有函數的構造函數都是Function,所有函數都是Function的一個實例對象
Function.constructor===Function
Object.constructor===Function
Array.constructor===Function String.constructor===Function
對象實例和原型對象的關聯
在調用構造函數創建新的實例時,該實例的內部會自動包含一個[[Prototype]]指針屬性,該指針指便指向構造函數的原型對象。註意,這個指針關聯的是實例與構造函數的原型對象而不是實例與構造函數:
在 JavaScript 中, constructor 屬性返回對象的構造函數。
返回值是函數的引用,不是函數名:
JavaScript 數組 constructor 屬性返回 function Array() { [native code] }
JavaScript 數字 constructor 屬性返回 function Number() { [native code] }
JavaScript 字符串 constructor 屬性返回 returns function String() { [native code] }
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var ary=fruits.constructor;
alert(ary);//彈出function Array() { [native code] }
alert(fruits.constructor===ary);//彈出true
alert(fruits.constructor===Array);//也是彈出true
因此:Student.prototype.constructor===Student
使用原型模型創建對象
直接在原型對象中添加屬性和方法
了解了原型對象之後,我們便可以通過在構造函數原型對象中添加屬性和方法來實現對象間數據的共享了。例如:
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 共享函數
以上,在構造函數中定義實例屬性,在原型中定義共享屬性的模式,是目前使用最廣泛的方式。通常情況下,我們都會默認使用這種方式來定義引用類型變量。
JavaScript構造函數的prototype屬性