創建一個student對象
var student = new Object();
student.name = "easy";
student.age = "20";
這樣,一個student對象就創建完畢,擁有2個屬性name以及age,分別賦值為"easy"和20。
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
};
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函數源源不斷地”生產”對象了??雌饋硪呀浉哒頍o憂了,但貪婪的人類總有不滿足于現狀的天性:我們不僅希望”產品”的生產可以像工廠車間一般源源不斷,我們還想知道生產的產品究竟是哪一種類型的。
比如說,我們同時又定義了”生產”水果對象的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共享了該全局函數,解決了內存浪費的問題。
但是,通過全局函數的方式解決對象內部共享的問題,終究不像一個好的解決方法。如果這樣定義的全局函數多了,我們想要將自定義對象封裝的初衷便幾乎無法實現了。更好的方案是通過原型對象模式來解決。
一、基本概念
1、對象:屬性和方法的集合,即變量和函數的封裝。每個對象都有一個__proto__屬性,指向這個對象的構造函數的原型對象。
2、構造器函數:用于創建對象的函數,通過new關鍵字生成對象。函數名一般首字母大寫的。
3、原型對象:每個函數都有一個prototype屬性,它是一個指向原型對象的指針(原型對象在定義函數時同時被創建)
二、創建對象的方法
1、使用構造函數和原型對象共同創建
如上圖,構造器函數Person(),通過new關鍵字創建了兩個實例化對象p1、p2,這兩個新對象都繼承了,構造器Person()函數prototype屬性所指向的原型對象。通過構造函數創建實例對象p1和p2的時候,其中name、age、job這些是通過構造函數生成的(本地部分),sayName方法是通過繼承原型對象來實現共享的(遠程部分),這樣多個實例對象都是由本地(私有)和遠程(共享)兩部分組成。還是不清楚,沒關系我們上代碼。
function Person(name, age, job){//構造器函數
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {//設置構造器函數prototype指定的對象,即重置原型對象
constructor : Person,
sayName : function(){alert(this.name);}
}
var p1 = new Person("Tom", 29, "Teacher");//實例化對象p1
//{name:"Tom",age:29,job:"Teacher",__proto__:object},object即原型對象:Person.prototype指向的對象
var p2 = new Person("Jack", 27, "Doctor");//實例化對象p2
//{name:"Jack",age:27,job:"Doctor",__proto__:object}
2、僅使用原型對象創建
如上圖,使用Object.create方法從原型對象直接生成新的實例對象,新對象p1繼承原型對象的屬性和方法,但是這里沒有用到構造函數
var person={ classname:'human'}//將這個對象當做原型
var p1=Object.create(person)//生成實例對象
console.log(p1.classname)//human,相當于p1.__proto__.classname
構造函數是媽,原型對象是爸,實例對象是孩子。媽讓每個孩子擁有私有能力,爸讓它們擁有共有能力(這個共有能力其實都是爸代勞的/(ㄒoㄒ)/~~);沒有構造函數的情況下,可以直接理解為克隆哦~怎么樣,這樣應該能理解三者之間的關系了吧。
使用原型模型創建對象
直接在原型對象中添加屬性和方法
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 共享函數
以上,在構造函數中定義實例屬性,在原型中定義共享屬性的模式,是目前使用最廣泛的方式。通常情況下,我們都會默認使用這種方式來定義引用類型變量。