javascript是一種基於對象的語言,但它沒有類的概念,所以又和實際面向對象的語言有區別,面向對象是javascript中的難點之一。現在就我所理解的總結一下,便於以后復習:
一、創建對象
1、創建自定義對象最簡單的方式就是創建Object的實例,並在為其添加屬性和方法,如下所示:
var cat = new Object(); //以貓咪為對象,添加兩個屬性分別為貓咪姓名和貓咪花色,並添加一個方法,說出貓咪的姓名 cat.name = "cc"; cat.color = "white"; cat.say = function(){ alert(this.name); }
近幾年,對象字面量成為創建對象的首選方式,如下:
var cat = { name: "cc", color: "white", say: function(){ alert(this.name); } }
以上兩種方式都可以用來創建單個對象,但這些方式具有明顯的缺點:在創建多個同一類型的對象的時候,會產生很多重復代碼,因此,人們開始使用工廠模式的一種變體來解決這個問題。
2、工廠模式創建對象
利用工廠模式的思想,用一個函數來封裝創建對象的細節,並提供特定接口來實現創建對象,上面的例子可以寫成如下:
function createCat(name,color){ var o = new Object(); o.name = name; o.color = color; o.say = function(){ alert(this.name); } return o; } var cat1 = createCat("cc","white"); var cat2 = createCat("vv","black");
函數createCat()可以根據接受到的貓咪的姓名和花色來創建一個貓咪對象,我們可以重復調用這個函數,每次調用都會返回一個包含兩個屬性和一個方法的對象,該對象表面了貓咪的姓名和花色,並可以說出貓咪的姓名。
利用工廠模式創建對象可以很好的解決上面的定義大量重復代碼的問題,但我們卻無法真正區分對象的類型,我們只能知道創建的對象是Object,而無法識別cat1和cat2都是貓咪對象。為解決這個問題,javascript提出了一種構造函數模式。
3、構造函數模式
我們知道在ECMAScript中定義了很多原生的構造函數,比如說Array、Date,我們可以用這些原生構造函數創建特定類型的對象。此外,javascript也允許我們創建自定義的構造函數,定義對象類型的屬性和方法,從而用於創建某一類的對象。我們利用構造函數模式重寫上面的例子如下:
function Cat(name,color){ this.name = name; this.color = color; this.say = function(){ alert (this.name) } } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","black");
我們可以用Cat()函數代替createCat()函數,相對於createCat()函數,Cat()函數存在以下不同:
1> 沒有顯示的創建對象;
2> 直接將屬性和方法付給了this對象;
3> 沒有return語句;
同時,我們還注意到Cat()函數的函數名首字母大寫。按照書寫規范,構造函數最好都以大寫字母開頭,其它函數都以小寫字母開頭,當然小寫字母開頭的函數也可以當成構造函數來創建對象。其實構造函數本質就是一個函數,只不過用它來創建對象。
當我們使用Cat()構造函數創建一個實例對象時,必須使用new運算符,使用這種方式創建對象會經過以下4個步驟:
1> 創建一個新對象
2> 將構造函數中this指向新創建的對象
3> 執行構造函數中的語句(即為新對象添加屬性和方法)
4> 返回這個新對象
在上面的例子中,分別創建了貓咪的兩個不同實例,這兩個實例對象都有一個constructor(構造函數)屬性,用於指向創建實例的構造函數,即Cat()
cat1.constructor == Cat //=> true cat2.constructor == Cat //=> true
對象的constructor屬性是用來標識對象類型的。但javascript提供了一個操作符instanceof來檢測對象的類型,上面例子中創建的對象cat1和cat2即是Object的實例也是Cat的實例
cat1 instanceof Object //=> true cat1 instanceof Cat //=> true cat2 instanceof Object //=> true cat2 instanceof Cat //=> true
以上可知,利用構造函數模式,我們可以將創建的對象標識為一種特定的類型。上面我們也提到過構造函數其實也是函數,它和普通函數的區別就在於調用方式的不同。任何函數,只要通過new運算符來調用,就可以當做構造函數,而任何函數,不通過new運算符調用,就和普通的函數調用沒有區別,看下面的實例:
//當做構造函數使用 var cat1 = new Cat("cc","white"); cat1.say(); //=> cc //當做普通函數調用 Cat("vv","block"); //普通函數運行,其this默認為window(ECMAScript3下) window.say(); //=> vv //在另一個對象作用域中調用 var o = new Object(); Cat.call(0,"ss","yellow"); o.say(); //=> yellow
以上三種使用方式,注意this的值
構造函數雖然解決了上面存在的一些問題,但它也有自己的缺點,就是每當創建一個對象時,其內部的所有屬性和方法都會重新創建一次,都會占有一定的內存,上面實例中創建的兩個貓咪對象cat1和cat2,它們的say()方法雖然相同,但卻不是同一個實例,占有不同的內存。
cat1.say == cat2.say; //=> false
但在實際使用中,每個對象中的方法實現的是同樣的功能,我們完全沒有必要創建多個Function實例,因此我們可以通過將函數定義到構造函數外面來解決這個問題。
function Cat(name,color){ this.name = name; this.color = color; this.say = sayName; } function sayName(){ alert(this.name); } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","black");
上例中,我們將say函數移到了外面,這樣cat1和cat2就可以共享在全局中定義的sayName函數了,但這樣做又存在一個新問題,就是定義在全局中的函數其實只是被某一類對象所調用,又使得全局作用域過於混亂,而對象也沒有封裝性可言了。這時,我們可以使用原型模式來解決。
4、原型模式
javascript中,我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象中的所有屬性和方法都會被構造函數創建的實例對象所繼承,即每個函數的prototype都是通過調用該構造函數而創建的實例對象的原型對象。我們看一個例子
function Person(){ } Person.prototype.name = "cc"; Person.prototype.age = "2"; Person.prototype.job = "software engineer"; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); person1.sayName(); //=> "cc" var person2 = new Person(); person2.sayName(); //=>"cc" person1.sayName == person2.sayName; //=> true
我們將Person的屬性都放在其原型對象中,其創建的實例對象person1和person2都包含一個內部屬性,該屬性指向Person的原型函數Person.prototype。我們用圖示表示(在這里偷懶,直接上高程的圖了):

上圖中展示了Person構造函數,Person的原型屬性以及Person的兩個實例對象之間的關系。其中,Person具有一個prototype屬性指向其原型對象,而Person的原型對象中又有一個constructor函數指向Person。其兩個實例對象的內部屬性[[Prototype]](目前還沒有標准的方式訪問)都指向了Person.prototype。從上圖我們可以看出,其實創建的實例對象和構造函數並沒有直接關系。
上面的例子中,雖然person1和person2都不具有屬性和方法,但卻可以調用sayName()方法。每當讀取對象的某個屬性時,都會執行一次搜索,首先搜索實例對象本身,如果沒有找到則繼續搜索對象指向的原型對象,如果有則返回,如果沒有則返回undefined。
上面的對象實例可以訪問原型中的屬性和方法,但卻不能重寫原型中的值。如果我們在實例中重寫一個與原型中相同名字的屬性,就會在實例中創建該屬性,並屏蔽原型中的同名屬性,但不會修改原型中的屬性。如下所示:
function Person(){ } Person.prototype.name = "cc"; Person.prototype.age = "2"; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "vv"; person1.name; //=> vv person2.name; //=> cc Person.prototype.name; //=> cc
前面實例中每次給原型添加屬性和方法都要輸入Person.prototype。因此我們可以使用對象字面量來重寫原型對象,如下所示:
function Person(){ } Person.prototype = { name : "cc", age : 2, job : "Software Engineer", sayName : function(){ alert(this.name); } }
上面的代碼定義的Person.prototype與上面定義的原型對象幾乎相同,但又一個例外,使用對象字面量重新定義原型對象(相當於重新定義了一個對象),其constructor屬性就不在指向Person了,而是指向Object構造函數。此時使用instanceof操作符還可以返回正確結果,但使用constructor屬性則無法確定對象的類型了。
var newPerson = new Person(); newPerson instanceof Object; //=> true newPerson instanceof Person; //=> true newPerson.constructor == Object; //=> true newPerson.constructor == Person; //=> false
如果我們需要constructor屬性,可以手動設置其值。
注意:我們可以隨時添加原型的屬性和方法,並可以在實例中立即反應過來,但我們一定要注意重寫原型對象的位置。調用構造函數創建對象會添加一個指向原型的指針,如果我們重寫對象的原型函數,就切斷了已創建實例和構造函數的關系。
function Person1(){ } Person1.prototype.name = "cc"; Person1.prototype.say = function(){ alert(this.name); } var friend1 = new Person1(); friend.say(); //=> cc function Person(){ } var friend = new Person(); Person.prototype = { constructor : Person, name : "cc", age : 2, job : "Software Engineer", sayName : function(){ alert(this.name); } } friend.sayName(); //=> error
原型模式的缺點:
1> 無法通過構造函數傳遞初始化參數
2> 引用類型的實例共享
我們看一個實例:
function Person(){ } Person.prototype = { constructor : Person, name : "cc", age : 2, job : "Software Engineer", friends : ["vv","dd"], sayName : function(){ alert(this.name); } } var person1 = new Person(); var person2 = new Person(); person1.friends.push("aa"); person1.friends; //=> vv,dd,aa person2.friends; //=> vv,dd,aa person1.friends == person2.friends; //=> true
以上代碼,當我們修改了person1.friends時,相應的person2.friends也會改變,因為它們指向同一個數組。但在實際中,我們通常希望實例擁有自己的獨立的屬性,因此提出了一個組合使用構造函數模式和原型模式的方法,這種混合模式,是目前使用最廣泛、認同度最高的一種創建自定義類型的方法。
function Cat(name,color){ this.name = name; this.color = color; this.firends = ["aa","bb"]; } Cat.prototype = { constructor : Cat, sayName : function(){ alert(this.name); } } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","block"); cat1.firends.push("dd"); cat1.firends; // => aa,bb,dd cat2.firends; //=> aa,bb cat1.firends == cat2.firends; //=> false cat1.sayName == cat2.sayName; //=> true
從以上實例可知,將我們希望實例對象獨立擁有的屬性放到構造函數中,將我們希望實力對象共享的屬性都放到原型中,有效的解決了上面存在的問題。
5、檢測對象類型的幾個方法
1> isPrototypeOf() 方法
目前我們在所有實現中都無法訪問到[[prototype]],但我們可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系,如果一個對象的[[prototype]]屬性指向調用isPrototypeOf()方法的對象,就返回true,否則返回false,如下:
Person.prototype.isPrototypeOf(person1); //=> true Person.prototype.isPrototyoeOf(person2); //=> true
以上,我們用Person的原型對象測試person1和person2,都返回true。
2> Object.getPrototypeOf() 方法 (ECMAScript5新定義)
這個方法返回[[prototype]]的值,如:
Object.getPrototypeOf(person1) == Person.prototype; //=> true Object.getPrototypeOf(person1).name; //=> "cc"
3> hasOwnProperty() 方法
該方法用於檢測一個屬性是否存在於一個實例中,而不是存在於原型中,如:
person1.name; //=> cc 來自於原型 person1.hasOwnProperty("name"); //=> false person1.name = "vv"; //=> 來自於實例 person1.hasOwnProperty("name"); //=> true
上面例子中,當person1的name屬性來自於原型時,hasOwnProperty()返回false,給person1重寫name屬性后,則返回true。
4> in運算符
有兩種方式使用in操作符:單獨使用或者再for-in循環中使用。
單獨使用時,in運算符可以用來檢測給定屬性是否能夠被對象所訪問,不管該屬性是存在於實例中還是原型中
person1.name; //=> cc //=>來自原型 "name" in person1; //=> true person1.name = "vv"; //=>來自實例中 "name" in person1; //=> true
上面例子中可見,無論對象的屬性來自原型還是來自實例,只要能被person1對象訪問就返回true
在for-in循環中,返回所有能夠被對象訪問的、可枚舉的屬性,其中即包括實例中的屬性,也包括原型中的屬性。屏蔽了原型中不可枚舉的屬性的實例屬性也會在for-in循環中返回,因為所有開發人員定義的屬性都是可枚舉的(IE8及更早版本下不會返回)。