JavaScript物件類型詳解
《JavaScript進階程式設計》已經學習到了第四章,不過因為第五章講的都是各種物件類型,所以在進行第五章的學習之前,先深入瞭解一下對象是有好處的。 JavaScript Objects in Detail 關於物件類型的方方面面在這篇文章裡都寫得很清楚了,本著不重複造輪子的原則,我這裡也不打算再重新寫一篇了,更何況,我這新手寫出來的文章肯定也跟人家的沒得比。 鑒於很多朋友可能對英文不是高度興趣,所以這裡準備把文章翻譯過來。不過提前聲明,本人沒有什麼翻譯經驗,翻譯出來的文章可能水平欠佳。如果英文比較好的話,建議直接看原文。畢竟是新的嘗試,各位高手如果有啥建議或者意見可以在評論提出,但請勿無故亂噴。 JavaScript物件類型詳解JavaScrtip有六種資料類型,一種複雜的資料類型(參考型別),即Object物件類型,還有五種簡單的資料類型(原始類型):Number、String、Boolean、Undefined和Null。其中,最核心的類型就是物件類型了。同時要注意,簡單類型都是不可變的,而物件類型是可變的。 什麼是對象一個對象是一組單一資料型別(有時是引用資料類型)的無序列表,被儲存為一系列的名-值對(name-value pairs)。這個列表中的每一項被稱為 屬性(如果是函數則被稱為 方法)。 下面是一個簡單的對象: var myFirstObject = { firstName: "Richard", favoriteAuthor: "Conrad" };可以把對象考慮成一個列表,列表中的每一項(屬性或方法)都以名-值對的方式儲存。上面例子中,對象的屬性名稱就是firstName和favortieAuthor,相應的,對象的屬性值為Richard和Conrad。 屬性名稱可以是字串或者數字,但是如果以數字作為屬性名稱,則必須以方括弧(方括弧記法)來獲得這個數字屬性名稱對應的屬性值。稍後有方括弧記法的更詳細解釋。下面是一個方括弧記法的例子: var ageGroup = {30: "Children", 100:"Very Old"}; console.log(ageGroup.30) // 報錯 // 訪問30這個屬性的正確方法 console.log(ageGroup["30"]); // Children //最好避免使用數字作為屬性名稱作為一個JavaScript程式員,你會經常使用到對象資料類型。一般用它來儲存資料,或者建立自訂的方法或函數。 引用資料類型和未經處理資料類型參考型別與原始類型最主要的一個不同點就是參考型別是按引用儲存的,它不會像原始類型一樣,將值直接儲存在變數中。比如: // 原始類型資料是按值儲存的 var person = "Kobe"; var anotherPerson = person; // anotherPerson = the value of person person = "Bryant"; // person的值改變了 console.log(anotherPerson); // Kobe console.log(person); // Bryan可以注意到,即使我們將person的值改為"Bryant",對anthoerPerson也會不有絲毫影響,它仍然儲存了原本person賦給它的值。 將原始類型的按值儲存跟參考型別的按引用儲存進行一下比較: var person = {name: "Kobe"}; var anotherPerson = person; person.name = "Bryant"; console.log(anotherPerson.name); // Bryant console.log(person.name); // Bryant在這個例子中,我們將person對象複製給了anthoerPerson,但是由於person對象中儲存的是引用而不是真正的值。所以當我們將person.name改變為"Bryant"的時候,anotherPerson變數也反應出了這個變化,因為它並沒有將person中的所有屬性都複製一份儲存起來,而是直接儲存了對象的引用。 對象屬性的特性(Attributes)註:Attribute一般也是翻譯為屬性,但是為了跟Propertie(也翻譯為屬性)進行區分,這裡將其翻譯為特性,這也是諮詢過別人的,應該無傷大雅 每個對象屬性不止儲存了自身的名-值對,它同時還包含了三個特性,這三個特性預設被設定為true。 Configurable Attribute: 指定這個對象屬性是否可以被刪除或修改。Enumerable:指定這個對象屬性在for-in迴圈中是否可以被取得。Writable:指定這個對象屬性是否可以被修改。在EMACScript 5中有一些新的特性,這裡不做詳細講解。 建立對象建立對象有兩種比較常用的方法: 對象字面量這是建立對象最常用,也是最簡單的方式,直接使用字面量進行建立: // Null 物件var myBooks = {};// 使用字面量建立的包含4個屬性的對象var mango = { color: "yellow", shape: "round", sweetness: 8, howSweetAmI: function () { console.log("Hmm Hmm Good"); }}物件建構函數第二種常用的方法是使用物件建構函數。建構函式是一種可以用來建立新對象的特殊函數,要使用new關鍵字來調用建構函式。 var mango = new Object ();mango.color = "yellow";mango.shape= "round";mango.sweetness = 8;mango.howSweetAmI = function () { console.log("Hmm Hmm Good");}雖然可以使用某些保留字或關鍵字,比如for作為對象屬性的名稱,不過這可不是一個明智的選擇。 對象的屬性可以包含任何資料類型,包括Number,Arrays,甚至是其它的Object。 對象建立的實踐模式對於建立只使用一次的用於儲存資料的簡單對象,上面的兩種方法就可以滿足需求。 但是,假設有一個程式用於展示水果和它的詳細資料。程式中的每個水果類型都有如下對象屬性:color, shape, sweetness, cost 和一個showName函數。要是每次建立一個新的水果對象時,都得敲一遍下面的代碼,那將是十分乏味和低效率的。 var mangoFruit = { color: "yellow", sweetness: 8, fruitName: "Mango", nativeToLand: ["South America", "Central America"], showName: function () { console.log("This is " + this.fruitName); }, nativeTo: function () { this.nativeToLand.forEach(function (eachCountry) { console.log("Grown in:" + eachCountry); }); } }如果你有10個水果,你就得添加10次相同的代碼。並且,如果想修改nativeTo函數,就得在10個不同的地方進行修改。再進一步推想,如果你在開發一個大型網站,你為上面的對象都一個一個添加了屬性。但是,你突然發現你建立對象的方式不是很理想,你想要進行修改,這時又該怎麼辦。 為瞭解決這些重複性的問題,軟體工程師們發明了各種模式(對於重複問題和常見任務的解決方案),使用開發程式更有效率和合理化。 下面是兩種建立對象的常用模式: 構造方法模式 function Fruit (theColor, theSweetness, theFruitName, theNativeToLand) { this.color = theColor; this.sweetness = theSweetness; this.fruitName = theFruitName; this.nativeToLand = theNativeToLand; this.showName = function () { console.log("This is a " + this.fruitName); } this.nativeTo = function () { this.nativeToLand.forEach(function (eachCountry) { console.log("Grown in:" + eachCountry); }); }}使用這種模式,很容易就可以建立出各式各樣的水果來。像這樣: var mangoFruit = new Fruit ("Yellow", 8, "Mango", ["South America", "Central America", "West Africa"]);mangoFruit.showName(); // This is a Mango.mangoFruit.nativeTo();//Grown in:South America// Grown in:Central America// Grown in:West Africa var pineappleFruit = new Fruit ("Brown", 5, "Pineapple", ["United States"]);pineappleFruit.showName(); // This is a Pineapple.如果你要改變屬性或方法,你只需要在一個地方進行修改就可以了。這個模式通過一個Fruit函數的繼承,封裝了所有水果的功能和特性。 注意: 可繼承的屬性需要定義在對象的prototype對象屬性上。比如 someObject.prototype.firstName = "rich";屬於自身的屬性要直接定義在對象的上。比如: // 首先,建立一個對象var aMango = new Fruit ();// 接著,直接在對象上定義mongoSpice方法// 因為我們直接在對象身上定義了mangoSpice屬性,所以它是aMango自身的屬性,不是一個可繼承的屬性aMango.mangoSpice = “some value”;要訪問一個對象的屬性,使用object.property,如: console.log(aMango.mangoSpice); // "some value"要調用一個對象的方法,使用object.method(),如: // 首先,增加一個方法aMango.printStuff = function() { return "Printing"; } // 現在,可以調用printStuff方法aMango.printStuff(); 原型模式 function Fruit () {} Fruit.prototype.color = "Yellow";Fruit.prototype.sweetness = 7;Fruit.prototype.fruitName = "Generic Fruit";Fruit.prototype.nativeToLand = "USA"; Fruit.prototype.showName = function () { console.log("This is a " + this.fruitName);} Fruit.prototype.nativeTo = function () { console.log("Grown in:" + this.nativeToLand);}下面是在原型模式中調用Fruit()建構函式的方法: var mangoFruit = new Fruit ();mangoFruit.showName(); //mangoFruit.nativeTo();// This is a Generic Fruit// Grown in:USA擴充閱讀如果需要瞭解這兩種模式的更詳細的解釋,可以閱讀《JavaScript進階程式設計》的第六章,其中詳細討論了這兩種方法的優缺點。書中還討論了除這兩個外的其它模式。 如何訪問對象中的屬性訪問對象屬性的兩種主要方法是點記法(dot notation)和中括弧記法(bracket notation)。 點記法 // 這是我們前面例子中一直使用的訪問屬性的方法var book = {title: "Ways to Go", pages: 280, bookMark1:"Page 20"};// 使用點記法訪問book對象的title和pages屬性:console.log ( book.title); // Ways to Goconsole.log ( book.pages); // 280中括弧記法 // 使用方括弧啟示訪問book對象的屬性:console.log ( book["title"]); //Ways to Goconsole.log ( book["pages"]); // 280//如果屬性名稱儲存在一個變數當中,也可以這樣:var bookTitle = "title";console.log ( book[bookTitle]); // Ways to Goconsole.log (book["bookMark" + 1]); // Page 20訪問一個對象中不存在的屬性會得到一個undefined。 自身屬性和繼承屬性對象擁有自身屬性和繼承屬性。自身屬性是直接定義在對象上的屬性,而繼承屬性是從Object的Prototype繼承的屬性。 為了確寫一個對象是否擁有某個屬性(不管是自身屬性還是繼承屬性),可以使用in操作符: // 建立一個有schoolName屬性的對象 var school = {schoolName:"MIT"}; // 列印出true,因為對象擁有schoolName這個屬性 console.log("schoolName" in school); // true // 列印出false,因為我們既沒有定義schoolType屬性,也沒有從Object的Prototype中繼承schoolType屬性 console.log("schoolType" in school); // false // 列印出true, 因為從Object的Prototype中繼承了toString方法 console.log("toString" in school); // truehasOwnProperty為了確定一個對象是否擁有一個特定的自身屬性,可以使用hasOwnPrototype方法。這個方法十分有用,因為我們經常需要枚舉一個對象的所有自身屬性,而不是繼承屬性。 // 建立一個擁有schoolName屬性的對象 var school = {schoolName:"MIT"}; // 列印出true,因為schooName是school的自身屬性 console.log(school.hasOwnProperty ("schoolName")); // true // 列印出false,因為toString是從Object的Prototype中繼承下來的,並且school的自身屬性 console.log(school.hasOwnProperty ("toString")); // false 訪問和枚舉對象中的屬性為了訪問對象中可以枚舉的屬性(自身或者繼承的),可以使用for-in迴圈或普通的迴圈方式。 // 建立擁有3個屬性的school對象: schoolName, schoolAccredited, and schoolLocation. var school = {schoolName:"MIT", schoolAccredited: true, schoolLocation:"Massachusetts"}; //使用for-in迴圈擷取對象中的屬性 for (var eachItem in school) { console.log(eachItem); // Prints schoolName, schoolAccredited, schoolLocation }訪問繼承的屬性從Object的Prototype中繼承的屬性不可枚舉的,所以在for-in迴圈中不會訪問到這些屬性。然而,如果是可枚舉的繼承屬性,它們也是能夠從for-in迴圈中訪問到的。 比如: //使用for-in逐一查看school對象中的屬性 for (var eachItem in school) { console.log(eachItem); // Prints schoolName, schoolAccredited, schoolLocation } // 註:以下這段說明是原文的說明 /* SIDE NOTE: As Wilson (an astute reader) correctly pointed out in the comments below, the educationLevel property is not actually inherited by objects that use the HigherLearning constructor; instead, the educationLevel property is created as a new property on each object that uses the HigherLearning constructor. The reason the property is not inherited is because we use of the "this" keyword to define the property. */ // Create a new HigherLearning function that the school object will inherit from. function HigherLearning () { this.educationLevel = "University"; } // Implement inheritance with the HigherLearning constructor var school = new HigherLearning (); school.schoolName = "MIT"; school.schoolAccredited = true; school.schoolLocation = "Massachusetts"; //Use of the for/in loop to access the properties in the school object for (var eachItem in school) { console.log(eachItem); // Prints educationLevel, schoolName, schoolAccredited, and schoolLocation }刪除對象中的屬性可以使用delete操作符來刪除對象中的屬性。我們不能刪除繼承的屬性,同時也不能刪除Configurable特性被設定為false的對象屬性。要刪除繼承的屬性,必須從Prototype對象中刪除(也就是定義這些屬性的地方)。並且,我們也不能刪除全域對象中的屬性。 刪除成功的時候,delete操作符會返回true。令人意外的是,當要刪除的屬性不存在,或者不能被刪除(即不是自身的屬性或者Configurable特性被設定為false)時, delete操作符也會返回true。 以下是樣本: var christmasList = {mike:"Book", jason:"sweater" } delete christmasList.mike; // deletes the mike property for (var people in christmasList) { console.log(people); } // Prints only jason // The mike property was deleted delete christmasList.toString; // 返回 true, 但是因為toString是繼承的屬性,所以它不會被刪除 // 因為toString沒有被刪除,所以這裡還能夠正常使用 christmasList.toString(); //"[object Object]" // 如果一個屬性是對象執行個體的自身屬性,則我們可以刪除它。 // 比如我們可以從之前例子中定義的school對象中刪除educationLevel屬性, // 因為educationLevel是定義在那個執行個體中的:我們在HigherLearning函數中定義educationLevel時使用了"this"關鍵字。 //我們並沒有在HigherLearning函數的prototype對象在定義educationLevel屬性。 console.log(school.hasOwnProperty("educationLevel")); // true // educationLevel是一個school對象的一個自身屬性,所以 我們可以刪除它 delete school.educationLevel; // true // educationLevel屬性已經從school執行個體中刪除了 console.log(school.educationLevel); // undefined // 但是educationLevel屬性仍然存在於HigherLearning函數中 var newSchool = new HigherLearning (); console.log(newSchool.educationLevel); // University // 如果我們在HigherLearning函數prototype中定義了一個屬性, 比如這個educationLevel2屬性: HigherLearning.prototype.educationLevel2 = "University 2"; // 這個educationLevel2屬性不屬性HigherLearning執行個體的自身屬性 // educationLevel2屬性不是school執行個體的自身屬性 console.log(school.hasOwnProperty("educationLevel2")); false console.log(school.educationLevel2); // University 2 // 嘗試刪除繼承的educationLevel2屬性 delete school.educationLevel2; // true (正如前面所提到的,這個運算式會返回true) // 繼承的educationLevel2屬性沒有被刪除 console.log(school.educationLevel2); University 2序列化和還原序列化對象為了在HTTP中傳遞對象或者將對象轉化成字串,我們必須將對象序列化(將其轉化為字串)。我們可以使用JSON.stringify來序列化對象。要注意的是,在ECMAScript 5之前的版本,我們要使用json2庫來獲得JSON.stringify函數。在ECMAScript 5中,這個函數已經成為標準函數。 為了將還原序列化對象(即,將字串轉化成對象),可以使用JSON.parse函數來完成。同樣,在第5版之前要從json2庫中擷取這個函數,在第5版中已經加入這個標準函數。 範例程式碼: var christmasList = {mike:"Book", jason:"sweater", chelsea:"iPad" } JSON.stringify (christmasList); // Prints this string: // "{"mike":"Book","jason":"sweater","chels":"iPad"}" // To print a stringified object with formatting, add "null" and "4" as parameters: JSON.stringify (christmasList, null, 4); // "{ // "mike": "Book", // "jason": "sweater", // "chels": "iPad" // }" // JSON.parse Examples // The following is a JSON string, so we cannot access the properties with dot notation (like christmasListStr.mike) var christmasListStr = '{"mike":"Book","jason":"sweater","chels":"iPad"}'; // Let’s convert it to an object var christmasListObj = JSON.parse (christmasListStr); // Now that it is an object, we use dot notation console.log(christmasListObj.mike); // Book