鍵值聚合體的物件

July 22, 2022

在 JavaScript 中,物件是 Object 的實例。你可以如下建立一個新的物件:

var obj = new Object();

Object 實例

實際上很少人這麼撰寫了,使用物件實字(Object literal)語法就可以建立一個物件:

var obj = {};

兩者的作用相同,物件實字寫法顯然較有效率。在 JavaScript 中,每個物件都可以是獨一無二,不一定是由其建構式來規範,這稱為物件個性化(Object individuation),你可以隨時為物件新增特性(Properties),也可以隨時用 delete 運算子來刪除特性。例如:

> var obj = {};
undefined
> obj.x = 10;
10
> obj.x;
10
> delete obj.x;
true
> obj.x;
undefined
> 

若刪除成功,delete 會傳回 true,並非所有特性都可被 delete 刪除,不可組態的特性無法被刪除(之後文件會看到如何定義物件特性是否為可組態),舉例來說,Array 實例有個 length 特性,你無法刪除它,這會引發 TypeError

> var arr = [];
undefined
> arr.length;
0
> delete arr.length;
TypeError: Cannot delete property 'length' of [object Array]
	at repl:1:1
	at ContextifyScript.Script.runInThisContext (vm.js:50:33)
	at REPLServer.defaultEval (repl.js:240:29)
	at bound (domain.js:301:14)
	at REPLServer.runBound [as eval] (domain.js:314:12)
	at REPLServer.onLine (repl.js:441:10)
	at emitOne (events.js:121:20)
	at REPLServer.emit (events.js:211:7)
	at REPLServer.Interface._onLine (readline.js:282:10)
	at REPLServer.Interface._line (readline.js:631:8)
>

附帶一提的是,delete 作用在實字(Literal)上時會傳回 true,例如 delete 1

如果事先知道物件的特性,可以使用物件實字一併建立。例如:

> var obj = {
...     x : 10,
...     y: 20
... };
undefined
> obj.x;
10
> obj.y;
20
> 

想要知道物件上有哪些自定義特性,可以使用 for in 語法,逐一取出物件的特性名稱。例如:

> for(var prop in obj) {
...     console.log(prop);
...     console.log(typeof prop);
... }
x
string
y
string
undefined
> 'x' in obj;
true
> 

由以上也可以得知,每個特性名稱其實是字串型態,這也說明了,如果想用 in 測試某特性時,特性名稱必須以字串指定。

事實上,點運算子(.)只是存取物件特性的一種方式。你也可以使用 [] 運算子來存取特性。例如:

> var obj = {};
undefined
> obj['x'] = 10;
10
> obj.x;
10
> obj['x'];
10
> 

JavaScript 的物件本質上,其實是個特性與值的群集(Collection),要比喻的話,有點像是 Java 中的 Map 物件。如果你要使用 for in 取得物件上的特性與值,則可以如下:

> var obj = {
...     x : 10,
...     y : 20
... };
undefined
> for(var prop in obj) {
...     console.log(prop + ': ', obj[prop]);
... }
x:  10
y:  20
undefined
> 

使用 [] 運算子的場合之一,就是當你的特性會包括空白、. 字元等時。例如:

> var obj = {
...     'openhome.cc': 'OpenHome',
... };
undefined
> obj.openhome.cc;
TypeError: Cannot read property 'cc' of undefined
	at repl:1:13
	at REPLServer.self.eval (repl.js:110:21)
	at repl.js:249:20
	at REPLServer.self.eval (repl.js:122:7)
	at Interface.<anonymous> (repl.js:239:12)
	at Interface.EventEmitter.emit (events.js:95:17)
	at Interface._onLine (readline.js:202:10)
	at Interface._line (readline.js:531:8)
	at Interface._ttyWrite (readline.js:760:14)
	at ReadStream.onkeypress (readline.js:99:10)
> obj['openhome.cc'];
'OpenHome'
> delete obj['openhome.cc'];
true
> 'openhome.cc' in obj;
false
> 

特性偵測

除了使用 in 測試物件上是否存在特性之外,由於物件上不存在某個特性時,你試圖存取時會傳回 undefined,而 undefined 若在判斷是否成立時會被當作 false,所以就有了特性偵測的作法:

> var obj = {};
undefined
> obj.x ? 'has x' : 'has no x';
'has no x'
> obj.x = 10;
10
> obj.x ? 'has x' : 'has no x';
'has x'
> 

特性偵測也可以與 || 一同結合,用在合併物件特性。例如:

function doSome(option) {    
	return {
		x : option.x || 1,
		y : option.y || 2,
		z : option.z || 3
	};
}

function log(obj) {
	for(var p in obj) {
		console.log(p + ': ' + obj[p]);
	}
}

var processed = doSome({
	x : 10,
	y : 20
});

log(processed);

在上例中,doSome 傳回物件的 xyz 特性預設值分別是 1、2、3,如果 option 上有提供對應的特性,則以 option 提供的為主,這經常用在函式上有太多參數及預設值要提供的場合,之後說明函式時還會看到,不清楚 || 運算為何會有這種結果的話,請看〈不只是加減乘除的運算子〉中的說明。執行結果會顯示:

x: 10
y: 20
z: 3

valueOf、toString

JavaScript 是個弱型別語言,在需要將物件轉為數值的場合,會呼叫 valueOf 方法。例如:

> var obj = {
...     valueOf : function() {
.....       return 100;
.....   }
... };
undefined
> 100 + obj;
200
> obj + 200;
300
> obj > 100;
false
> obj >= 100;
true
> 

在需要將物件轉換為字串的場合,則會呼叫 toString 方法。例如:

> var caterpillar = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     toString : function() {
.....         return '[name: ' + this.name + ', url: ' + this.url + ']';
.....     }
... };
undefined
> 'My info: ' + caterpillar;
'My info: [name: Justin Lin, url: openhome.cc]'
> 

在上例中,若透過 caterpillar 呼叫了 toStringthis 就是參考至 caterpillar 所參考的物件,之後還會詳細說明 this 是什麼。

相等性比較

在 JavaScript 中,=== 用在物件比較時,是比較參考的對象是否為同一物件,而不是物件實際內含值(== 得考慮型態轉換後的結果),如果你要比較兩個物件實際上是否為同一物件,必須自行定義專屬方法,這個方法名稱並沒有規範。例如,也許定義個 equals 方法:

> var man1 = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     equals : function(other) {
.....         return (this.name === other.name) &&  (this.url === other.url);
.....     }
... };
undefined
> var man2 = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     equals : function(other) {
.....         return (this.name === other.name) &&  (this.url === other.url);
.....     }
... };
undefined
> man1 === man2;
false
> man1.equals(man2);
true
> 

在上例中,兩個物件的 equals 參考的函式定義重複了。如果你懂在 JavaScript 中函式是物件的觀念,則可以修改如下:

function equals(other) {
	return (this.name === other.name) &&  (this.url === other.url);
}

var man1 = {
	name : 'Justin Lin',
	url  : 'openhome.cc',
	equals : equals
};

var man2 = {
	name : 'Justin Lin',
	url  : 'openhome.cc',
	equals : equals
};

var man3 = {
	name : 'Justin Lin',
	url  : 'openhome.cc',
	equals : equals
};

console.log(man1.equals(man2));  // true
console.log(man1.equals(man3));  // true

實際上,如果你知道如何使用函式定義建構式,並瞭解運用原型鏈(Prototype chain)實現繼承,上頭的需求可以改為以下的方式:

function Man(name, url) {
	this.name = name;
	this.url = url;
}

Man.prototype.equals = function(other) {
	return (this.name === other.name) &&  (this.url === other.url);
};

var man1 = new Man('Justin Lin', 'openhome.cc');
var man2 = new Man('Justin Lin', 'openhome.cc');
var man3 = new Man('Justin Lin', 'openhome.cc');

console.log(man1.equals(man2));
console.log(man1.equals(man3));

至於為何可以這麼做,會留在之後說明函式時再來討論。

在嚴格模式中,如果想刪除的特性並非可組態的(Configurable)(之後文件會看到如何定義物件特性是否為可組態),會發生 TypeError。舉例來說,Array 實例有個 length 特性,在嚴格模式下你不能對它做 delete,否則會引發 TypeError

分享到 LinkedIn 分享到 Facebook 分享到 Twitter