javascript是一種基於對象的語言,但它沒有類的概念,因此又和實際面向對象的語言有區別,面向對象是javascript中的難點之一。如今就我所理解的總結一下,便於之後複習:javascript
1、建立對象java
一、建立自定義對象最簡單的方式就是建立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); } }
以上兩種方式均可以用來建立單個對象,但這些方式具備明顯的缺點:在建立多個同一類型的對象的時候,會產生不少重複代碼,所以,人們開始使用工廠模式的一種變體來解決這個問題。測試
二、工廠模式建立對象this
利用工廠模式的思想,用一個函數來封裝建立對象的細節,並提供特定接口來實現建立對象,上面的例子能夠寫成以下:spa
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()能夠根據接受到的貓咪的姓名和花色來建立一個貓咪對象,咱們能夠重複調用這個函數,每次調用都會返回一個包含兩個屬性和一個方法的對象,該對象表面了貓咪的姓名和花色,並能夠說出貓咪的姓名。prototype
利用工廠模式建立對象能夠很好的解決上面的定義大量重複代碼的問題,但咱們卻沒法真正區分對象的類型,咱們只能知道建立的對象是Object,而沒法識別cat1和cat2都是貓咪對象。爲解決這個問題,javascript提出了一種構造函數模式。指針
三、構造函數模式code
咱們知道在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函數了,但這樣作又存在一個新問題,就是定義在全局中的函數其實只是被某一類對象所調用,又使得全局做用域過於混亂,而對象也沒有封裝性可言了。這時,咱們可使用原型模式來解決。
四、原型模式
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
從以上實例可知,將咱們但願實例對象獨立擁有的屬性放到構造函數中,將咱們但願實力對象共享的屬性都放到原型中,有效的解決了上面存在的問題。
五、檢測對象類型的幾個方法
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及更早版本下不會返回)。