深入淺出 JavaScript 對象 v0.5

JavaScript 沒有類的概念,因此它的對象與基於類的語言中的對象有所不同。筆者主要參考《JS 高級程序設計》、《JS 權威指南》和《JS 精粹》

 

本文由淺入深的講解瞭對象的概念,特性,和使用,由於筆者水平的確有限,有些觀點也是邊理解,邊查證,邊分享。

 

希望大傢都能感受到分享的樂趣,祝我們共同進步,請大傢不吝交流。

 

目錄

對象是什麼?

對象有什麼特性?

對象有什麼用?

如何創建對象?

對象直接量

工廠方法創建對象

通過 new 創建對象

對象屬性的查詢與設置(檢索與更新)

刪除對象的屬性

反射

枚舉屬性

對象的三個屬性

序列化對象

寫在後面的話

對象是什麼?

ECMA-262(JS的標準)把對象定義為“無序屬性的集合,其屬性包含基本值、對象和函數”。

 

JavaScript中的對象是可變的鍵控集合(keyed collections)(註意,是可變的喲)。

 

對象是屬性和方法的容器(貌似在任何一門支持對象的語言中都是),每個屬性都擁有名字和值。

 

屬性的名字(key)可以包括空字符串在內的任意字符串,屬性值可以是除undefined值之外的任何值。

 

對象有什麼特性?

動態性:可以新增也可以刪除屬性,但常用來模擬靜態對象和靜態類型語言中的結構體,有時還用做字符串的集合(忽略名值對的值)。

無序性:在枚舉對象的屬性時,屬性是沒有順序的。

無類型:對象適合用於匯集和管理數據。對象可以包含其他對象,所以它們很容易表示樹形結構或圖形結構。

原型鏈特征:允許對象繼承另一個對象的屬性。正確地使用它能減少對象初始化時消耗的時間和內存。

對象有什麼用?

對象最常用的用法就是創建、設置、查找、刪除、檢測和枚舉它的屬性。另外還有一些高級操作。

 

如何創建對象?

可以通過對象直接量、工廠方法、new 關鍵字來創建對象。(另外《權威指南》 上說Object.create() 函數也可以創建自定義對象)

 

對象直接量

對象直接量是由若幹名值對組成的映射表,名值對中間用冒號隔開,名值對之間用逗號分隔,整個映射表用花括號括起來。

 

屬性名可以是JavaScript 標識符或者字符串直接量(包括空字符串),屬性的值可以是任意類型的JavaScript 表達式,表達式的值就是這個屬性的值。

 

對象直接量是一個表達式,該表達式每次運算時都創建並初始化一個新的對象,也都會計算它每個屬性的值(註意在循環體中使用對象直接量)。

 

復制代碼

var tony = {

    name: 'tony',

    age: 23,

    contacts: {

        tel: '12345566',

        QQ: '55555' 

    }

};

復制代碼

其中 ECMAScript 5 中 字面量最後一個名值對後面的逗號將被忽略,但是IE中會報錯。

 

工廠方法創建對象

用函數來封裝以特定接口創建對象的細節。

 

復制代碼

function createPerson (name, age, job) {

    var o = new Object ();

    o.name = name;

    o.age = age;

    o.job = job;

    o.sayHello = function () {

        alert("hello, I'm "+this.name);

    };

    return o;

}

 

var tony = createPerson('tony', 23, 'engneer');

復制代碼

工廠方法雖然解決瞭創建多個相似對象的問題,但是卻沒有解決對象識別的問題,不知道一個對象屬於那種類型,沒有對象的類型信息。

 

通過 new 創建對象

new 運算符 創建並初始化一個對象,關鍵字 new 後面跟一個函數調用(該函數被稱為構造函數,構造函數用來初始化一個新對象)。

JavaScript語言核心中的原始類型都包含內置構造函數。

 

var o = new Object();    //  創建一個空對象 {} 還有 Array() Date() RegExp()

除瞭內置構造函數外,還可以自定義構造函數,例如:

 

復制代碼

function Person /*首字母大寫主要是區別普通函數*/ (name, age, job) {

    this.name = name;

    this.age = age;

    this.job = job;

    this.sayHello = function () {

        alert("Hello  I'm "+ this.name);

    };

}

var tony = new Person ('tony', 23, 'engneer');

var googny = new Person ('googny', 23, 'manager');

googny.sayHello == tony.sayHello; // false

復制代碼

要創建 Person 的新實例,必須使用 new 操作符,這種方式調用構造函數實際上經歷四個步驟:

 

創建一個新對象;

將構造函數的作用域賦給新對象(this 就指向這個新對象)

執行構造函數中的代碼

返回新對象

OK,上面的例子的確可以創建並初始化新的對象,有心人可能會註意到我最後三行代碼,googny.sayHello 和 tony.sayHello 不相等。

 

也就是說該函數在每個對象中都有一份拷貝,熟悉類C語言的童鞋都知道,類中的方法方法隻有一份拷貝,各個對象共享這份拷貝,調用過程中將 this 隱式的傳遞給該方法。

 

這種情況可以在 JavaScript 中實現嗎? 是的,可以

 

復制代碼

var sayHello = function () {

        alert("Hello  I'm "+ this.name);

 };

 

function Person /*首字母大寫主要是區別普通函數*/ (name, age, job) {

    this.name = name;

    this.age = age;

    this.job = job;

    this.sayHello = sayHello;

}

var tony = new Person ('tony', 23, 'engneer');

var googny = new Person ('googny', 23, 'manager');

googny.sayHello == tony.sayHello; // true

復制代碼

從返回結果可以看出,兩個對象共享一段代碼,但是這樣看起來封裝性不是特別的好,用起來也憋手蹩腳的,《悟透 JavaScript》 中用“不優雅”來形容。

 

JavaScript 的設計者也覺得上面的兩種做法是浪費“糧食”(內存),而且醜陋,於是“原型” 應運而生。

 

什麼是原型?

 

我們創建的每個函數都有一個 prototype (原型) 屬性, 這個屬性是一個指針,指向一個對象,而這個對象的用途就是用來包含可以有特定類型的所有實例共享的屬性和方法。

 

使用原型對象的好處就是可以讓所有實例共享它所包含的屬性和方法。

 

復制代碼

function Person /*首字母大寫主要是區別普通函數*/ (name, age, job) {

    this.name = name;

    this.age = age;

    this.job = job;

}

 

Person.prototype.sayHello = function () {

    alert("Hello  I'm "+ this.name);

};

var tony = new Person ('tony', 23, 'engneer');

var googny = new Person ('googny', 23, 'manager');

googny.sayHello == tony.sayHello; // true

復制代碼

在默認情況下,所有原型對象都會自動獲取一個constructor(構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。

 

很繞是吧,看一張清爽無比的高清大圖

 

 

 

//    為瞭說明Prototype 中 constructor 指向Person函數

var some = new Person.prototype.constructor('somebody',25,'boss');

console.log(some);  

當然,上述小代碼隻是為瞭說明問題,該屬性主要是用來判斷對象的類型。

 

tony.constructor == Person;    //constructor先從對象的屬性中找,沒有的話,再從tony 的原型中找。

tony instanceof Person;

組合使用構造函數和原型模式來創建新對象

 

復制代碼

function Person (name, age, job) {

    this.name = name;

    this.age = age;

    this.job = job;

}

 

Person.prototype = {

    constructor : Person,  //以對象形式給prototype 賦值,要想有類型判斷,則必須指定constructor

    sayHello : function () {

        alert("hello,  my name is "+this.name);

    }

};

 

var tony = new Person ('tony', 23, 'student');

 

tony instanceof Person;

復制代碼

該方式是目前使用最廣泛,認同度最高的一種創建自定義類型的方法。

 

對象屬性的查詢與設置(檢索與更新)

要檢索對象裡包含的值,可以采用在[]後綴中括住一個字符串表達式的方法。如果字符串表達式是一個字符串字面量,而且它是一個合法的JavaScript 標識符而不是保留字,那麼也可以用 . 表示法代替。 

 

復制代碼

var tony = {

    "first-name" : 'tony tong',

    departure : 'xidian university',

    contacts : {

        tel : '123456767',

        QQ : '55555'

    }

};

 

tony ['first-name'] ;    // '-'標識符中不合法喲

tony.contacts.tel ; 

復制代碼

如果你嘗試查詢一個並不存在的成員屬性的值,將返回 undefined

 

|| 運算符可以用來填充默認值:

 

var middle = tony["middle-name"] || "(none)"

嘗試從undefined 的成員屬性中取值會導致TypeError 異常,這時可以通過 && 運算符來避免錯誤。

 

tony.age;     // undefined 

tony.age.birth    // TypeError 異常

tony.age && tony.age.birth    //undefined

對象裡的值可以通過賦值語句來更新,如果屬性名已經存在於對象裡,那麼這幾個屬性的值就會被替換。

 

如果之前對象中沒有那個屬性,那麼該屬性就被擴充到對象中。

 

刪除對象的屬性

delete 運算符可以用來刪除對象的屬性。如果對象包含該屬性,那麼該屬性就會被移除,它不會觸及原型鏈中的任何對象。

 

刪除對象的屬性可能會讓來自原型鏈中的屬性顯現出來。

 

復制代碼

function Person (name, age, job) {

    this.name = name;

    this.age = age;

    this.job = job;

    this.sayHello = function () {

        alert("hello, I'm a person!");

    };

}

 

Person.prototype = {

    constructor : Person,  

    sayHello : function () {

        alert("hello,  my name is "+this.name);

    }

};

 

var tony = new Person ('tony', 23, 'student');

 

tony.sayHello();    // hello, I'm a person!

delete tony.sayHello;

tony.sayHello();    // hello, my name is tony

復制代碼

反射

反射指的是程序在運行時能夠獲取自身的信息。例如一個對象能夠在運行時知道自己有哪些方法和屬性。

 

檢查對象並確定對象有什麼屬性是很容易的事兒,隻要試著去檢索該屬性並驗證取得的值。

 

typeof 操作符對確定屬性的類型很有幫助;

 

typeof tony.sayHello    // 'function'

typeof tony.contacts    // 'object'

使用 hasOwnProperty 方法來檢查對象是否擁有該屬性, 如果對象擁有獨有的屬性, 它將返回true, 該方法不會檢查原型鏈

 

枚舉屬性

for in 語句用來遍歷一個對象中的所有屬性名。該枚舉過程將會列出所有的屬性—— 包括函數和可能不關心的原型中的屬性——所以有必要過濾掉那些不想要的屬性,

 

最為常用的過濾器是 hasOwnProperty 方法,以及使用typeof 來排除值為函數的屬性

 

var property;

for (property in tony) {

    if ( tony.hasOwnProperty(property) && typeof tony[property] != 'function') {

        console.log(property);

    }

}

屬性名出現的順序是不確定的。

 

對象的三個屬性

每一個對象都有與之相關的原型(prototype)、類(class)、和可擴展性(extensible attribute)

 

原型屬性是一個指向原型對象的指針,主要用來在同一類對象中共享屬性和方法,還可以用來繼承屬性。

 

對象的類屬性是一個字符串,用以表示對象的類型信息,隻有一種間接的方法來查詢它。

 

默認的toString()方法,返回如下格式的字符串:

 

[object class]

因此要想獲得對象的類,調用對象的toString 方法,然後提取字符串 從第8個字符到倒數第二個字符之間的字符串。

 

不過讓人棘手的是,很多對象的toString() 方法都被重寫瞭,為瞭能調用正確的toString() 版本,必須使用函數的間接調用模式(參考深入淺出 JavaScript 函數 中的間接調用模式)

 

例如:

 

function classOf (o) {

    if (o == null)  return 'Null';

    if (o == undefined) return 'Undefined';

    return Object.prototype.toString.call(o).slice(8,-1);

}

該函數可以接受任何類型的參數。

 

對象的可擴展性用以表示對象是否可以給對象添加新屬性。所有內置對象和自定義對象都是顯式可擴展性的。

 

宿主對象的可擴展性是由JavaScript 引擎定義的,在ECMAScript 5 中,所有的內置對象和自定義對象默認都是可擴展的,除非將它們轉換為不可擴展的。

 

通過將對象傳入 Object.esExtensible(),來判斷該對象是否是可擴展的。

 

如果想將對象轉換成不可擴展的,則需要調用Object.preventExtensions(),將待轉換的對象作為參數傳進去。(此過程是不可逆的)

 

如果想將對象所有的自有屬性設置為不可配置的,也就是說不能增加新屬性,刪除、設置它的已有屬性,則需要調用Object.seal()。不過它已有的可寫屬性依然可以設置。

 

Object.freeze() 將對象所有屬性設置為隻讀的。 Object.isSealed() 檢測是否封閉,Object.isFrozen() 來檢測對象是否被凍結。

 

序列化對象

對象序列化,是指將對象的狀態轉換為字符串,也可將字符串還原成對象。

 

ECMAScript 5 提供瞭內置函數JSON.stringify() 和 JSON.prase()用來序列化和反序列化JavaScript 對象。

 

復制代碼

var o = {

    x: 1,

    y: {

        z: [false, null, ""]

    }

};    //定義一個測試對象

 

var s = JSON.stringify (o);    //s 是一個字符串

console.log(s);

var p = JSON.parse(s);    //p是o的深拷貝

console.log(p);

復制代碼

寫在後面的話

其實關於JavaScript 對象的話題遠遠不止這些,比如一些比較高級的話題:對象屬性的特性,對象屬性getter和setter等

 

有興趣的讀者可以找相關資料查閱。

 

謝謝大傢,看著覺的說得過去的話,點個推薦吧。 

 

 

發佈留言