《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中有一些新的特性,這里不做詳細講解。
創建對象
創建對象有兩種比較常用的方法:
對象字面量
這是創建對象最常用,也是最簡單的方式,直接使用字面量進行創建:// 空對象?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 Go?console.log ( book.pages); // 280
中括號記法
// 使用方括號啟示訪問book對象的屬性:console.log ( book["title"]); //Ways to Go?console.log ( book["pages"]); // 280???//如果屬性名儲存在一個變量當中,也可以這樣:??var bookTitle = "title";console.log ( book[bookTitle]); // Ways to Go?console.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); // true
hasOwnProperty
為了確定一個對象是否擁有一個特定的自身屬性,可以使用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
更多關于JavaScript對象的討論和解釋,以及ECMAScript第5版增加的內容,可以參考《JavaScript權威指南(第6版)》第六章。
后記
第一次翻譯文章,真心覺得要把翻譯做好也不是那么簡單的,很多簡單的句子看著很明白,結果真正想翻譯出來的時候,卻是死活想不出合適的表達方式。通篇文章都是根據我自己的理解,然后通過意譯出來的,沒有逐句進行翻譯。所以,如果有哪些地方理解有偏差,或者翻譯不當的地方,請盡量指出,我會盡快改正。畢竟翻譯這往篇文章也是想跟大家分享,我不希望因為自己理解的錯誤,導致對大家產生誤導。
就醬,收工。