第六章 物件導向的程式設計 JavaScript進階程式設計

來源:互聯網
上載者:User

ECMA-262把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。

對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值。

對象:散列表(一組名值對,其中值可以是資料或函數)

每個對象都是基於一個參考型別建立的。

6.1 建立對象

建立一個Object的執行個體,為它添加屬性和方法:

6_1_my.html

<script>var person = new Object();person.name = "Nicholas";person.age = 29;person.job = "Software Engineer";person.sayName = function(){console.log(this.name);}person.sayName();</script>

運行結果:

Nicholas

缺點:使用同一個介面建立很多個物件,會產生大量的重複代碼。

6.1.1 原廠模式                 (與寄生建構函式模式 穩妥建構函式模式一樣)

原廠模式抽象了建立具體對象的過程。

<script>function createPerson(name,age,job){var o = new Object();o.name = name;o.age = age;o.job = job;o.sayName = function(){console.log(this.name);};return o;}var person1 = createPerson("Nicholas",29,"Software Engineer");var person2 = createPerson("Greg",27,"Doctor");person1.sayName(); //Nicholasperson2.sayName(); //"Greg"</script>

運行結果:

NicholasGreg

原廠模式雖然解決了建立多個相似對象的問題,卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)

6.1.2 建構函式模式

ECMAScript中的建構函式可用來建立特定類型的對象。像Object和Array這樣的原生建構函式,在運行時會自動出現在執行環境中。

<script>function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);};}var person1 = new Person("Nicholar",29,"Software Engineer");var person2 = new Person("Greg",27,"Doctor");person1.sayName(); //"Nicholas"person2.sayName(); //"Greg"//對象的constructor屬性最初是用來標識物件類型的。但是提到檢測物件類型,還是instanceof操作符//要更可靠一些console.log(person1.constructor == Person); //trueconsole.log(person2.constructor == Person); //trueconsole.log(person1 instanceof Object); //trueconsole.log(person1 instanceof Person); //trueconsole.log(person2 instanceof Object); //trueconsole.log(person2 instanceof Person); //true</script>

特點:

  • 沒有顯示地建立對象;
  • 直接將屬性和方法賦給了this對象;
  • 沒有return語句。

建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。

調用建構函式建立對象會經曆以下的步驟:

  • 建立一個新對象;
  • 將建構函式的範圍賦給對象(因此this就指向了這個新對象);
  • 執行建構函式中的代碼(為這個新對象添加屬性);
  • 返回對象。

建立自訂的建構函式意味著將來可以將它的執行個體標識為一種特定的類型。

以這種方式定義的建構函式是定義在Global對象(在瀏覽器中是window對象)中的。因此除非另有說明,instanceof操作符和constructor屬性始終會假設是在全域範圍中查詢建構函式。

1.將建構函式當作函數

建構函式與其他函數的唯一區別,就在於調用它們的方式不同。建構函式也是函數,不存在定義建構函式的特殊文法。任何函數,只要通過new操作符來調用,那它就可以作為建構函式;而任何函數,如果不通過new操作符來調用,那它跟普通函數也不會有什麼兩樣。

<script>function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);};}//當作建構函式使用var person = new Person("Nicholas",29,"Software Engineer");person.sayName(); //"Nicholas"//作為普通函數調用Person("Greg",27,"Doctor");//添加到windowwindow.sayName();//"Greg"//在另一個對象的範圍中調用var o = new Object();Person.call(o,"Kristen",25,"Nurse");o.sayName();//"Kristen"</script>

在全域範圍中調用一個函數時,this對象總是指向Global對象(在瀏覽器中就是window對象)

2.建構函式的問題

使用建構函式的主要問題,就是每個方法都要在每個執行個體上重新建立一遍。

在前面的例子中,person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個Function的執行個體。ECMAScript中的函數是對象,因此每定義一個函數,也就是執行個體化了一個對象。

<script>function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = new Function("console.log(this.name)");//與聲明函數在邏輯上是等價的}var person1 = new Person("Nicholar",29,"Software Engineer");var person2 = new Person("Greg",27,"Doctor");console.log(person1.sayName == person2.sayName); //false</script>

建立兩個完成同樣任務的Function執行個體的確沒有必要;況且有this對象在,根本不用在執行代碼前就把函數綁定到特定對象上面。因此,通過把函數定義轉移到建構函式外部來解決這個問題:

<script>function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = sayName;}function sayName(){console.log(this.name);}var person1 = new Person("Nicholar",29,"Software Engineer");var person2 = new Person("Greg",27,"Doctor");person1.sayName(); //"Nicholas"person2.sayName(); //"Greg"console.log(person1 instanceof Object); //trueconsole.log(person1 instanceof Person); //trueconsole.log(person2 instanceof Object); //trueconsole.log(person2 instanceof Person); //trueconsole.log(person1.constructor == Person); //trueconsole.log(person2.constructor == Person); //trueconsole.log(person1.sayName == person2.sayName);true</script>

新問題:在全域範圍中定義的函數實際上只能被某個對象調用,這讓全域範圍有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那麼就要定義很多個全域函數,於是我們這個自訂的參考型別就絲毫沒有任何封裝性可言了。

6.1.3 原型模式

每個函數都有一個prototype(原型)屬性,這個屬性是一個對象,它的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。如果按照字元意思來理解,那麼prototype就是通過調用建構函式而建立的那個對象的原型對象。使用原型的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。換句話說,不必在建構函式中定義對象資訊,而是可以將這些資訊直接添加到原型對象中。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();person1.sayName(); //"Nicholas"var person2 = new Person();person2.sayName(); //"Nicholas"console.log(person1.sayName == person2.sayName); //true</script>
1.理解原型

無論什麼時候,只要建立了一個新函數,就會根據一組特定的規則為該函數建立一個prototype屬性。在預設的情況下,所有prototype屬性都會自動獲得一個constructor(建構函式)屬性,這個屬性包含一個指向prototype屬性所在函數的指標。

建立了自訂的建構函式之後,其原型屬性預設只會取得constructor屬性;至於其他的方法,則都是從Object繼承而來的。當調用建構函式建立一個新執行個體後,該執行個體的內部將包含一個指標(內部屬性),指向建構函式的原型屬性。在很多實現中,這個內部屬性的名字是__proto__,而且通過指令碼可以訪問到(在Firefox,Safari、Chrome和Flash的ActionScript中,都可以通過指令碼訪問__proto__);而在其他實現中,這個屬性對指令碼則是完全不可見的。不過,要明確的真正重要的一點,就是這個串連存在於執行個體與建構函式的原型屬性之間,而不是存在於執行個體與建構函式之間。

雖然在某些實現中無法訪問到內部的__proto__屬性,但在所有實現中都可以通過isPrototypeOf()方法來確定對象之間是否存在這種關係。從本質上講,如果對象的__proto__指向調用isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就返回true。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();var person2 = new Person();console.log(Person.prototype.isPrototypeOf(person1));//trueconsole.log(Person.prototype.isPrototypeOf(person2));//true</script>

這裡,我們用原型對象的isPrototypeOf()方法測試了person1和person2。因為它們內部都有一個指向Person.prototype的指標,因此都返回了true。

每當代碼讀取某個對象的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從對象執行個體本身開始。如果在執行個體中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜尋指標指向的原型對象,在原型對象中尋找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值。這正是多個對象執行個體共用原型所儲存的屬性和方法的基本原理。

原型最初只包含constructor屬性,而該屬性也是共用的,因此可以通過對象執行個體訪問。

雖然可以通過對象執行個體訪問儲存在原型中的值,但卻不能通過對象執行個體重寫原型中的值。如果我們在執行個體中添加了一個屬性,而該屬性與執行個體原型中的一個屬性同名,那我們就在執行個體中建立該屬性,該屬性將會屏蔽原型中的那個屬性。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name = "Greg";console.log(person1.name); //“Greg”---來自執行個體console.log(person2.name); //"Nicholas"————來自原型</script>

當為對象執行個體添加一個屬性時,這個屬性就會屏蔽原型對象中儲存的同名屬性;換句話說,添加這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設定為null,也只會在執行個體中設定這個屬性,而不會恢複其指向原型的串連。不過,使用delete操作符則可以完全刪除執行個體屬性,從而讓我們能夠重新訪問原型中的屬性。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name = "Greg";console.log(person1.name); //“Greg”---來自執行個體console.log(person2.name); //"Nicholas"————來自原型delete person1.name;console.log(person1.name); //"Nicholas"————來自原型</script>

使用hasOwnProperty()方法可以檢測一個屬性是存在於執行個體中,還是存在於原型中。這個方法(從Object繼承來的)只在給定屬性存在於對象執行個體中時,才會返回true。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();var person2 = new Person();console.log(person1.hasOwnProperty("name"));//falseperson1.name = "Greg";console.log(person1.name); //"Greg"——————來自執行個體console.log(person1.hasOwnProperty("name")); //trueconsole.log(person2.name); //"Nicholas"————來自原型console.log(person2.hasOwnProperty("name"));//falsedelete person1.name;console.log(person1.name); //"Nicholas"————來自原型console.log(person1.hasOwnProperty("name"));//falsea</script>

關係如所示:

2.原型與in操作符

有兩種方式使用in操作符:單獨使用和在for-in迴圈中使用。在單獨使用時,in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在於執行個體中還是原型中。

<script>function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person1 = new Person();var person2 = new Person();console.log(person1.hasOwnProperty("name"));//falseconsole.log("name" in person1); //trueperson1.name = "Greg";console.log(person1.name); //"Greg"——————來自執行個體console.log(person1.hasOwnProperty("name")); //trueconsole.log("name" in person1); //trueconsole.log(person2.name); //"Nicholas"————來自原型console.log(person2.hasOwnProperty("name"));//falseconsole.log("name" in person2); //truedelete person1.name;console.log(person1.name); //"Nicholas"————來自原型console.log(person1.hasOwnProperty("name"));//falseaconsole.log("name" in person1); //true</script>

同時使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在於對象中,還是存在於原型中。

<script>function hasPrototypeProperty(object,name){return !object.hasOwnProperty(name) && (name in object)}function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){console.log(this.name);};var person = new Person();console.log(hasPrototypeProperty(person,"name"));person.name = "Greg";console.log(hasPrototypeProperty(person,"name"));</script>

在使用for-in迴圈時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於執行個體中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即設定了[[DontEnum]]標記的屬性)的執行個體屬性(即重寫的屬性)也會在for-in迴圈中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的——只有IE例外。

IE的JScript實現中存在一個bug,即屏蔽不可枚舉屬性的執行個體屬性不會出現在for-in迴圈中。

<script>var o = {toString : function(){return "My Object";}}for(var prop in o){if(prop == "toString"){//注釋掉if,只會彈一次alert("Found toString"); //在IE中不會顯示(IE9已經修複)}}</script>

3.更簡單的原型文法

<script>function Person(){}Person.prototype = {name = "Nicholas";age = 29;job = "Software Engineer";sayName = function(){console.log(this.name);}}</script>

在上面的代碼中,我們將Person.prototype設定為等於一個以對象字面量形式建立的新對象。最終結果相同,但有一個例外:constructor屬性不再指向Person了。每建立一個函數,就會同時建立它prototype對象,這個對象也會自動獲得cosntructor屬性。而我們在這裡使用的文法,本質上完全重寫了預設的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性(指向Object建構函式),不再指向Person函數。此時,儘管instanceof操作符還能返回正確的結果,但通過constructor已經無法確定對象的類型了。

<script>function Person(){}Person.prototype = {name : "Nicholas",age : 29,job : "Software Engineer",sayName : function(){console.log(this.name);}}var person = new Person();console.log(person instanceof Object); //trueconsole.log(person instanceof Person); //trueconsole.log(person.constructor == Person); //falseconsole.log(person.constructor == Object); //true</script>

修正constructor的指向:

<script>function Person(){}Person.prototype = { constructor : Person,name : "Nicholas",age : 29,job : "Software Engineer",sayName : function(){console.log(this.name);}}var person = new Person();console.log(person instanceof Object); //trueconsole.log(person instanceof Person); //trueconsole.log(person.constructor == Person); //falseconsole.log(person.constructor == Object); //true</script>

4.原型的動態性

由於在原型中尋找值的過程是一次搜尋,因此我們對原型對象所做的任何修改都能夠立即從執行個體上反映出來——既是是先建立了執行個體後修改原型也照樣如此。

<script>function Person(){}Person.prototype = { constructor : Person,name : "Nicholas",age : 29,job : "Software Engineer",sayName : function(){console.log(this.name);}}var person = new Person();Person.prototype.sayHi = function(){console.log("hi");}person.sayHi();//"hi"(沒有問題!)</script>

其原因可以歸結為執行個體與原型之間的鬆散串連關係。當我們調用person.sayHi()時,首先會在執行個體中搜尋名為sayHi的屬性,在沒找到的情況下,會繼續搜尋原型。因為執行個體與原型之間的串連只不過是一個指標,而非一個副本,因此就可以在原型中找到新的sayHi屬性並返回儲存在那裡的函數。

儘管可以隨時為原型添加屬性和方法,並且修改能夠立即在所有對象執行個體中放映出來,但如果是重寫整個原型對象,那麼情況就不一樣了。我們知道,調用建構函式時會為執行個體添加一個指向最初原型的__proto__指標,而把原型修改為另外一個對象就等於切斷了建構函式與最初原型之間的聯絡。請記住:執行個體中的指標僅指向原型,而不指向建構函式。

<script>function Person(){}var person = new Person();Person.prototype = { constructor : Person,name : "Nicholas",age : 29,job : "Software Engineer",sayName : function(){console.log(this.name);}}person.sayHi();//error</script>

5.原生對象的原型

原型模式的重要性不僅體現在建立自訂類型方面,就連所有原生的參考型別,都是採用這種模式建立的。所有的原生類型(Object、Array、String,等等)都在其建構函式的原型上定義了方法。

<script>alert(typeof Array.prototype.sort); //"function"alert(typeof String.prototype.substring); //"function"</script>

通過原生對象的原型,不僅可以取得所有預設方法的引用,而且也可以定義新方法。可以像修改自訂對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。

<script>String.prototype.startsWith = function(text){return this.indexOf(text) == 0;}var msg = "Hello world!";console.log(msg.startsWith("Hello")); //true</script>

6.原型對象的問題

它省略了為建構函式傳遞初始化參數這一環節,結果所有執行個體在預設情況下都將取得相同的屬性值。原型模型的最大問題是由其共用的本性所導致的。

<script>function Person(){}Person.prototype = { constructor : Person,name : "Nicholas",age : 29,job : "Software Engineer",friends : ["Shelby","Court"],sayName : function(){console.log(this.name);}}var person1 = new Person();var person2 = new Person();person1.friends.push("Van");console.log(person1.friends); //"Shelby,Court,Van"console.log(person2.friends); //"Shelby,Court,Van"console.log(person1.friends == person2.friends); //true</script>
6.1.4組合使用建構函式模式和原型模式

建構函式模式用於定義執行個體屬性,而原型模式用於定義方法和共用的屬性。結果,每個執行個體都會有自己的一份執行個體屬性的副本,但同時又共用著對方法的引用,最大限度地節省了記憶體。另外,這種混成模式還支援向建構函式傳遞參數。

<script>function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.friends = ["Shelby","Court"];}Person.prototype ={constructor : Person,sayName : function(){console.log(this.name);}}var person1 = new Person("Nicholas",29,"Software Engineer");var person2 = new Person("Greg",29,"Doctor");person1.friends.push("Van");console.log(person1.friends); //"Shelby,Count,Van"console.log(person2.friends); //"Shelby,Count"console.log(person1.friends === person2.friends); //falseconsole.log(person1.sayName === person2.sayName); //true</script>
6.1.5 動態原型模式

動態原型模式把所有資訊都封裝在了建構函式中,而通過在建構函式中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的特點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

<script>function Person(name,age,job){//屬性this.name = name;this.age = age;this.job = job;//方法if(typeof this.sayName != "function"){Person.prototype.sayName = function(){console.log(this.name);};}}var person = new Person("Nicholas",29,"Software Engineer");person.sayName();</script>

if語句檢查的可以是初始化之後應該存在的任何屬性或方法——不必用一大堆if語句檢查每個屬性和每個方法;只要檢查其中一個即可。

使用動態原型模式時,不能使用對象字面量重寫原型。如果在已經建立了執行個體的情況下重寫原型,那麼就會切斷現有執行個體與新原型之間的聯絡。

6.1.6 寄生建構函式模式         (與原廠模式一樣)

這種模式的基本思想是建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再返回新建立的對象;但從表面上看,這個函數又很像是典型的建構函式。

<script>function Person(name,age,job){var o = new Object();o.name = name;o.age = age;o.jbo = job;o.sayName = function(){console.log(this.name);};return o;}var person = new Person("Nicholas",29,"Software Engineer");person.sayName(); //"Nicholas"</script>

建構函式在不傳回值的情況下,預設會返回新對象執行個體。而通過在建構函式的末尾添加一個return語句,可以重寫調用建構函式時返回的值。

假設我們想建立一個具有額外方法的特殊數組。由於不能直接修改Array建構函式,因此可以使用這個模式:

<script>function SpecialArray(){//建立數組var values = new Array();//添加值values.push.apply(values,arguments);//添加方法values.toPipedString = function(){return this.join("|");};//返回數組return values;}var colors = new SpecialArray("red","blue","green");console.log(colors.toPipedString()); //"red|blue|green"</script>

首先,返回的對象與建構函式或者與建構函式的原型屬性之間沒有關係;也就是說,建構函式返回的對象與在建構函式外部建立的對象沒有什麼不同。為此,不能依賴instanceof操作符來確定物件類型。

6.17 穩妥建構函式模式         (與原廠模式一樣)

穩妥對象:沒有公用屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new),或者在防止資料被其他應用程式(如Mashup程式)改動時使用。

穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:一是新建立對象的執行個體方法不引用this;二是不使用new操作符調用建構函式。

<script>function Person(name,age,job){//建立要返回的對象var o = new Object();//可以在這裡定義私人變臉和函數this.name = name;this.age = age;this.jbo = job;//添加方法o.sayName = function(){console.log(name);};//返回對象return o;}var person = Person("Nicholas",29,"Software Engineer");person.sayName();  //"Nicholas"'</script>

與寄生建構函式模式類似,使用穩妥建構函式模式建立的對象與建構函式之間沒有什麼關係,因此instanceof操作符對這種對象也沒有意義。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.