JavaScript 物件深入學習總結

clearbug發表於2015-09-29

JavaScript中,除了五種原始型別(即數字,字串,布林值,null,undefined)之外的都是物件了,所以,不把物件學明白怎麼繼續往下學習呢?

一.概述

物件是一種複合值,它將很多值(原始值或其他物件)聚合在一起,可通過屬性名訪問這些值。而屬性名可以是包含空字串在內的任意字串。JavaScript物件也可以稱作一種資料結構,正如我們經常聽說的“雜湊(hash)”、“雜湊表(hashtable)”、“字典(dictionary)”、“關聯陣列(associative array)”。

JavaScript中物件可以分為三類:

①內建物件,例如陣列、函式、日期等;

②宿主物件,即JavaScript直譯器所嵌入的宿主環境(比如瀏覽器)定義的,例如HTMLElement等;

③自定義物件,即程式設計師用程式碼定義的;

物件的屬性可以分為兩類:

①自有屬性(own property):直接在物件中定義的屬性;

②繼承屬性(inherited property):在物件的原型物件中定義的屬性(關於原型物件下面會詳談);

二.物件的建立

既然學習物件,又怎能不懂如何建立物件呢?面試前端崗位的同學,可能都被問過這個基礎問題吧:

  建立JavaScript物件的兩種方法是什麼?(或者:說說建立JavaScript物件的方法?)

這個問題我就被問過兩次。“建立物件的兩種方法”這種說法網上有很多,但是據我所看書籍來說是有三種方法的!下面我們就來具體談談這三種方法:

1.物件直接量

物件直接量由若干名/值對組成的對映表,名/值對中間用冒號分隔,名/值對之間用逗號分隔,整個對映表用花括號括起來。屬性名可以是JavaScript識別符號也可以是字串直接量,也就是說下面兩種建立物件obj的寫法是完全一樣的:

var obj = {x: 1, y: 2};
var obj = {'x': 1, 'y':2};

2.通過new建立物件

new運算子後跟隨一個函式呼叫,即建構函式,建立並初始化一個新物件。例如:

1 var o = new Object();    //建立一個空物件,和{}一樣
2 var a = new Array();    //建立一個空陣列,和[]一樣
3 var d = new Date();    //建立一個表示當前時間的Date物件

關於建構函式相關的內容以後再說。

3.Object.create()

ECMAScript5定義了一個名為Object.create()的方法,它建立一個新物件,其中第一個引數是這個物件的原型物件(好像還沒解釋原型物件…下面馬上就說),第二個可選引數用以對物件的屬性進行進一步的描述,第二個引數下面再說(因為這第三種方法是ECMAScript5中定義的,所以以前大家才經常說建立物件的兩種方法的吧?個人覺得應該是這個原因)。這個方法使用很簡單:

1 var o1 = Object.create({x: 1, y: 2});    //物件o1繼承了屬性x和y
2 var o2 = Object.create(null);    //物件o2沒有原型

下面三種的完全一樣的:

1 var obj1 = {};
2 var obj2 = new Object();
3 var obj3 = Object.create(Object.prototype);

為了解釋為啥這三種方式是完全一樣的,我們先來解釋下JavaScript中的原型物件(哎,讓客官久等了!),記得一位大神說過:

Javascript是一種基於物件(object-based)的語言,你遇到的所有東西幾乎都是物件。但是,它又不是一種真正的物件導向程式設計(OOP)語言,因為它的語法中沒有class(類)。

物件導向的程式語言JavaScript,沒有類!!!那麼,它是怎麼實現繼承的呢?沒錯,就是通過原型物件。基本上每一個JavaScript物件(null除外)都和另一個物件相關聯,“另一個”物件就是所謂的原型物件(原型物件也可以簡稱為原型,並沒有想象的那麼複雜,它也只是一個物件而已)。每一個物件都從原型物件繼承屬性,並且一個物件的prototype屬性的值(這個屬性在物件建立時預設自動生成,並不需要顯示的自定義)就是這個物件的原型物件,即obj.prototype就是物件obj的原型物件。

原型物件先說到這,回到上面的問題,有了對原型物件的認識,下面就是不需要過多解釋的JavaScript語言規定了:

①所有通過物件直接量建立的物件的原型物件就是Object.prototype物件;

②通過關鍵字new和建構函式建立的物件的原型物件就是建構函式prototype屬性的值,所以通過建構函式Object建立的物件的原型就是Object.prototype了;

現在也補充了第三種建立物件的方法Object.create()第一個引數的含義。

三.屬性的查詢和設定

學會了如何建立物件還不夠啊,因為物件只有擁有一些屬性才能真正起到作用滴!那麼,就繼續往下學習物件的屬性吧!

可以通過點(.)或方括號([])運算子來獲取和設定屬性的值。對於點(.)來說,右側必須是一個以屬性名命名的識別符號(注意:JavaScript語言的識別符號有自己的合法規則,並不同於帶引號的字串);對於方括號([])來說,方括號內必須是一個字串表示式(字串變數當然也可以嘍,其他可以轉換成字串的值比如數字什麼的也是都可以滴),這個字串就是屬性的名字。正如下面例子:

var obj = {x: 1, y: 2};
obj.x = 5;
obj['y'] = 6

概述中說過,JavaScript物件具有”自有屬性“,也有“繼承屬性”。當查詢物件obj的屬性x時,首先會查詢物件obj自有屬性中是否有x,如果沒有,就會查詢物件obj的原型物件obj.prototype是否有屬性x,如果沒有,就會進而查詢物件obj.prototype的原型物件obj.prototype.prototype是否有屬性x,就這樣直到找到x或者查詢到的原型物件是undefined的物件為止。可以看到,一個物件上面繼承了很多原型物件,這些原型物件就構成了一個”鏈“,這也就是我們平時所說的“原型鏈”,這種繼承也就是JavaScript中“原型式繼承”(prototypal inheritance)。

物件o查詢某一屬性時正如上面所說會沿著原型鏈一步步查詢,但是其設定某一屬性的值時,只會修改自有屬性(如果物件沒有這個屬性,那就會新增這個屬性並賦值),並不會修改原型鏈上其他物件的屬性。

四.存取器屬性getter和setter

上面我們所說的都是很普通的物件屬性,這種屬性稱做“資料屬性”(data property),資料屬性只有一個簡單的值。然而在ECMAScript 5中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter,有getter和setter定義的屬性稱做“存取器屬性”(accessor property)。

當程式查詢存取器屬性的值時,JavaScript呼叫getter方法(無引數)。這個方法的返回值就是屬性存取表示式的值。當程式設定一個存取器屬性的值時,JavaScript呼叫setter方法,將賦值表示式右側的值當做引數傳入setter。如果屬性同時具有getter和setter方法,那麼它就是一個讀/寫屬性;如果它只有getter方法,那麼它就是一個只讀屬性,給只讀屬性賦值不會報錯,但是並不能成功;如果它只有setter方法,那麼它是一個只寫屬性,讀取只寫屬性總是返回undefined。看個實際的例子:

var p = {
    x: 1.0,
    y: 2.0,
    get r(){ return Math.sqrt(this.x*this.x + this.y*this.y); };
    set r(newvalue){
        var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
        var ratio = newvalue/oldvalue;
        this.x *= ratio;
        this.y *= ratio;
    },
    get theta(){ return Math.atan2(this.y, this.x); },
    print: function(){ console.log('x:'+this.x+', y:'+this.y); }
};

正如例子所寫,存取器屬性定義一個或兩個和屬性同名的函式,這個函式定義並沒有使用function關鍵字,而是使用get和set,也沒有使用冒號將屬性名和函式體分隔開。對比一下,下面的print屬性是一個函式方法。注意:這裡的getter和setter裡this關鍵字的用法,JavaScript把這些函式當做物件的方法來呼叫,也就是說,在函式體內的this指向這個物件。下面看下例項執行結果:

正如控制檯的輸出,r、theta同x,y一樣只是一個值屬性,print是一個方法屬性。

ECMAScript 5增加的這種存取器,雖然比普通屬性更為複雜了,但是也使得操作物件屬性鍵值對更加嚴謹了。

五.刪除屬性

程式猿擼碼一般都是實現增、刪、改、查功能,前面已經說了增、改、查,下面就說說刪除吧!

delete運算子可以刪除物件的屬性,它的運算元應該是一個屬性訪問表示式。但是,delete只是斷開屬性和宿主物件的聯絡,而不會去操作屬性中的屬性:

var a = {p:{x:1}};
var b = a.p;
delete a.p;

執行這段程式碼後b.x的值依然是1,由於已刪除屬性的引用依然存在,所以有時這種不嚴謹的程式碼會造成記憶體洩露,所以在銷燬物件的時候,要遍歷屬性中的屬性,依次刪除。

delete表示式返回true的情況:

①刪除成功或沒有任何副作用(比如刪除不存在的屬性)時;

②如果delete後不是一個屬性訪問表示式;

var obj = {x: 1,get r(){return 5;},set r(newvalue){this.x = newvalue;}};
delete obj.x;    //刪除物件obj的屬性x,返回true
delete obj.x;    //刪除不存在的屬性,返回true
delete obj.r;    //刪除物件obj的屬性r,返回true
delete obj.toString;    //沒有任何副作用(toString是繼承來的,並不能刪除),返回true
delete 1;    //數字1不是屬性訪問表示式,返回true

delete表示式返回false的情況:

①刪除可配置性(可配置性是屬性的一種特性,下面會談到)為false的屬性時;

delete Object.prototype;    //返回false,prototype屬性是不可配置的
//通過var宣告的變數或function宣告的函式是全域性物件的不可配置屬性
var x = 1;
delete this.x;    //返回false
function f() {}
delete this.f;    //返回false

六.屬性的特性

上面已經說到了屬性的可配置性特性,因為下面要說的檢測屬性和列舉屬性還要用到屬性的特性這些概念,所以現在就先具體說說屬性的特性吧!

除了包含名字和值之外,屬性還包含一些標識它們可寫、可列舉、可配置的三種特性。在ECMAScript 3中無法設定這些特性,所有通過ECMAScript 3的程式建立的屬性都是可寫的、可列舉的和可配置的,且無法對這些特性做修改。ECMAScript 5中提供了查詢和設定這些屬性特性的API。這些API對於庫的開發者非常有用,因為:

①可以通過這些API給原型物件新增方法,並將它們設定成不可列舉的,這讓它們更像內建方法;

②可以通過這些API給物件定義不能修改或刪除的屬性,藉此“鎖定”這個物件;

在這裡我們將存取器屬性的getter和setter方法看成是屬性的特性。按照這個邏輯,我們也可以把屬性的值同樣看做屬性的特性。因此,可以認為屬性包含一個名字和4個特性。資料屬性的4個特性分別是它的值(value)、可寫性(writable)、可列舉性(enumerable)和可配置性(configurable)。存取器屬性不具有值特性和可寫性它們的可寫性是由setter方法是否存在與否決定的。因此存取器屬性的4個特性是讀取(get)、寫入(set)、可列舉性和可配置性。

為了實現屬性特性的查詢和設定操作,ECMAScript 5中定義了一個名為“屬性描述符”(property descriptor)的物件,這個物件代表那4個特性。描述符物件的屬性和它們所描述的屬性特性是同名的。因此,資料屬性的描述符物件的屬性有value、writable、enumerable和configurable。存取器屬性的描述符物件則用get屬性和set屬性代替value和writable。其中writable、enumerable和configurable都是布林值,當然,get屬性和set屬性是函式值。通過呼叫Object.getOwnPropertyDescriptor()可以獲得某個物件特定屬性的屬性描述符:

從函式名字就可以看出,Object.getOwnPropertyDescriptor()只能得到自有屬性的描述符,對於繼承屬性和不存在的屬性它都返回undefined。要想獲得繼承屬性的特性,需要遍歷原型鏈(不會遍歷原型鏈?不要急,下面會說到的)。

要想設定屬性的特性,或者想讓新建屬性具有某種特性,則需要呼叫Object.definePeoperty(),傳入需要修改的物件、要建立或修改的屬性的名稱以及屬性描述符物件:

可以看到:

①傳入Object.defineProperty()的屬性描述符物件不必包含所有4個特性;

②可寫性控制著對屬性值的修改;

③可列舉性控制著屬性是否可列舉(列舉屬性,下面會說的);

④可配置性控制著對其他特性(包括前面說過的屬性是否可以刪除)的修改;

如果要同時修改或建立多個屬性,則需要使用Object.defineProperties()。第一個引數是要修改的物件,第二個引數是一個對映表,它包含要新建或修改的屬性的名稱,以及它們的屬性描述符,例如:

var p = Object.defineProperties({},{
    x: {value: 1, writable: true, enumerable: true, configurable: true},
    y: {value: 2, writable: true, enumerable: true, configurable: true},
    r: {get: function(){return 88;}, set: function(newvalue){this.x =newvalue;},enumerable: true, configurable: true},
    greet: {value: function(){console.log('hello,world');}, writable: true, enumerable: true, configurable: true}
});

相信你也已經從例項中看出:Object.defineProperty()和Object.defineProperties()都返回修改後的物件。

前面我們說getter和setter存取器屬性時使用物件直接量語法給新物件定義存取器屬性,但並不能查詢屬性的getter和setter方法或給已有的物件新增新的存取器屬性。在ECMAScript 5中,就可以通過Object.getOwnPropertyDescriptor()和Object.defineProperty()來完成這些工作啦!但在ECMAScript 5之前,大多數瀏覽器(IE除外啦)已經支援物件直接量語法中的get和set寫法了。所以這些瀏覽器還提供了非標準的老式API用來查詢和設定getter和setter。這些API有4個方法組成,所有物件都擁有這些方法。__lookupGetter__()和__lookupSetter__()用以返回一個命名屬性的getter和setter方法。__defineGetter__()和__defineSetter__()用以定義getter和setter。這四個方法都是以兩條下劃線做字首,兩條下劃線做字尾,以表明它們是非標準方法。下面是它們用法:

七.檢測屬性

JavaScript物件可以看做屬性的集合,那麼我們有時就需要判斷某個屬性是否存在於某個物件中,這就是接下來要說的檢測屬性。

檢測一個物件的屬性也有三種方法,下面就來詳細說說它們的作用及區別!

1.in運算子

in運算子左側是屬性名(字串),右側是物件。如果物件的自有屬性或繼承屬性中包含這個屬性則返回true,否則返回false。

為了試驗,我們先給物件Object.prototype新增一個可列舉屬性m,一個不可列舉屬性n;然後,給物件obj定義兩個可列舉屬性x,一個不可列舉屬性y,並且物件obj是通過物件直接量形式建立的,繼承了Object.prototype。下面看例項:

從執行結果可以看出:in運算子左側是屬性名(字串),右側是物件。如果物件的自有屬性或繼承屬性(不論這些屬性是否可列舉)中包含這個屬性則返回true,否則返回false。

2.hasOwnProperty()

物件的hasOwnProperty()方法用來檢測給定的名字是否是物件的自有屬性(不論這些屬性是否可列舉),對於繼承屬性它將返回false。下面看例項:

3.propertyIsEnumerable()

propertyIsEnumerable()是hasOwnProperty()的增強版,只有檢測到是自有屬性且這個屬性可列舉性為true時它才返回true。還是例項:

八.列舉屬性

相對於檢測屬性,我們更常用的是列舉屬性。列舉屬性我們通常使用for/in迴圈,它可以在迴圈體中遍歷物件中所有可列舉的自有屬性和繼承屬性,把屬性名稱賦值給迴圈變數。繼續上例項:

我原來認為for/in迴圈跟in運算子有莫大關係的,現在看來它們的規則並不相同啊!當然,如果這裡不想遍歷出繼承的屬性,那就在for/in迴圈中加一層hasOwnProperty()判斷:

for(prop in obj){
    if(obj.hasOwnProperty(prop)){
        console.log(prop);
    }
}

除了for/in迴圈之外,ECMAScript 5還定義了兩個可以列舉屬性名稱的函式:

①Object.getOwnpropertyNames(),它返回物件的所有自有屬性的名稱,不論是否可列舉;

②Object.keys(),它返回物件物件中可列舉的自有屬性的名稱;

還是例項:

九.物件的三個特殊屬性

每個物件都有與之相關的原型(prototype)、類(class)和可擴充套件性(extensible attribute)。這三個就是物件的特殊屬性(它們也只是物件的屬性而已,並沒有想象的複雜哦)。

1.原型屬性

正如前面所說,物件的原型屬性是用來繼承屬性的(有點繞…),這個屬性如此重要,以至於我們經常把“o的原型屬性”直接叫做“o的原型”。原型屬性是在例項建立之初就設定好的(也就是說,這個屬性的值是JavaScript預設自動設定的,後面我們會說如何自己手動設定),前面也提到:

①通過物件直接量建立的物件使用Object.prototype作為它們的原型;

②通過new+建構函式建立的物件使用建構函式的prototype屬性作為它們的原型;

③通過Object.create()建立的物件使用第一個引數(如果這個引數為null,則物件原型屬性值為undefined;如果這個引數為undefined,則會報錯:Uncaught TypeError: Object prototype may only be an Object or null: undefined)作為它們的原型;

那麼,如何查詢一個物件的原型屬性呢?在ECMAScript 5中,將物件作為引數傳入Object.getPrototypeOf()可以查詢它的原型,例如:

但是在ECMAScript 3中,沒有Object.getPrototypeOf()函式,但經常使用表示式obj.constructor.prototype來檢測一個物件的原型,因為每個物件都有一個constructor屬性表示這個物件的建構函式:

①通過物件直接量建立的物件的constructor屬性指向建構函式Object();

②通過new+建構函式建立的物件的constructor屬性指向建構函式;

③通過Object.create()建立的物件的constructor屬性指向與其原型物件的constructor屬性指向相同;

要檢測一個物件是否是另一個物件的原型(或處於原型鏈中),可以使用isPrototypeOf()方法。例如:

還有一個非標準但眾多瀏覽器都已實現的物件的屬性__proto__(同樣是兩個下劃線開始和結束,以表明其為非標準),用以直接查詢/設定物件的原型。

2.類屬性

物件的類屬性(class attribute)是一個字串,用以表示物件的型別資訊。ECMAScript 3 和ECMAScript 5 都未提供設定這個屬性的方法,並只有一種間接的方法可以查詢它。預設的toString()方法(繼承自Object.prototype)返回了這種格式的字串:[object class] 。因此,要想獲得物件的類,可以呼叫物件的toString()方法,然後提取已返回字串的第8到倒數第二個位置之間的字元。不過,很多物件繼承的toString()方法重寫了(比如:Array、Date等),為了能呼叫正確的toString()版本,必須間接地呼叫Function.call()方法。下面程式碼可以返回傳遞給它的任意物件的類:

function classof(obj){
    if(o === null){
        return 'Null';
    }
    if(o === undefined){
        return 'Undefined';
    }
    return Object.prototype.toString.call(o).slice(8, -1);
}

classof()函式可以傳入任何型別的引數。下面是使用例項:

總結:從執行結果可以看出通過三種方式建立的物件的類屬性都是’Object’。

3.可擴充套件性

物件的可擴充套件性用以表示是否可以給物件新增新屬性。所有內建物件和自定義物件都是顯示可擴充套件的(除非將它們轉換為不可擴充套件),宿主物件的可擴充套件性是由JavaScript引擎定義的。ECMAScript 5中定義了用來查詢和設定物件可擴充套件性的函式:

①(查詢)通過將物件傳入Object.isExtensible(),來判斷該物件是否是可擴充套件的。

②(設定)如果想將物件轉換為不可擴充套件,需要呼叫Object.preventExtensions(),將待轉換的物件作為引數傳進去。注意:

a.一旦將物件轉換為不可擴充套件的,就無法再將其轉換回可擴充套件的了;

b.preventExtensions()隻影響到物件本身的可擴充套件性,如果給一個不可擴充套件的物件的原型新增屬性,這個不可擴充套件的物件同樣會繼承這些新屬性;

進一步,Object.seal()和Object.preventExtensions()類似,除了能將物件設定為不可擴充套件的,還可以將物件的所有自有屬性都設定為不可配置的。對於那些已經封閉(sealed)起來的物件是不能解封的。可以使用Object.isSealed()來檢測物件是否封閉。

更進一步,Object.freeze()將更嚴格地鎖定物件——“凍結”(frozen)。除了將物件設定為不可擴充套件和將其屬性設定為不可配置之外,還可以將它自有的所有資料屬性設定為只讀(若物件的存取器屬性有setter方法,存取器屬性將不受影響,仍可通過給屬性賦值呼叫它們)。使用Object.isFrozen()來檢測物件是否總結。

總結:Object.preventExtensions()、Object.seal()和Object.freeze()都返回傳入的物件,也就是說,可以通過巢狀的方式呼叫它們:

var obj = Object.seal(Object.create(Object.freeze({x:1}),{y:{value: 2, writable: true}));

這條語句中使用Object.create()函式傳入了兩個引數,即第一個引數是建立出的物件的原型物件,第二個引數是在建立物件是直接給其定義的屬性,並且附帶定義了屬性的特性。

十.物件的序列化

前面說完了物件的屬性以及物件屬性的特性,東西還是蠻多的,不知道你是否已看暈。不過,下面就是比較輕鬆的話題了!

物件序列化(serialization)是指將物件的狀態轉換為字串,也可以將字串還原為物件。ECMAScript 5提供了內建函式JSON.stringify()和JSON.parse()用來序列化和還原物件。這些方法都使用JSON作為資料交換格式,JSON的全稱是“JavaScript Object Notation”——JavaScript物件表示法,它的語法和JavaScript物件與陣列直接量的語法非常相近:

其中,最後的jsonObj是obj的深拷貝(關於什麼是深拷貝,什麼是淺拷貝,可以參考:http://www.zhihu.com/question/23031215,第二個答案)。

JSON的語法是JavaScript的子集,它並不能表示JavaScript裡的所有值。支援物件、陣列、字串、無窮大數字、true、false和null,並且它們可以序列化和還原。注意:

①NaN、Infinity和-Infinity序列化的結果是null;

②JSON.stringify()只能序列化物件可列舉的自有屬性;

③日期物件序列化的結果是ISO格式的日期字串(參照Date.toJSON()函式),但JSON.parse()依然保留它們的字串形態,而不能將它們還原為原始日期物件;

④函式、RegExp、Error物件和undefined值不能序列化和還原;

當然,JSON.stringify()和JSON.parse()都可以接受第二個可選引數,通過傳入需要序列化或還原的屬性列表來定製自定義的序列化或還原操作,這個我們以後再詳談。

相關文章