JavaScript 對象

2023-03-20 15:57 更新

概述

生成方法

對象(object)是 JavaScript 語言的核心概念,也是最重要的數(shù)據(jù)類型。

什么是對象?簡單說,對象就是一組“鍵值對”(key-value)的集合,是一種無序的復合數(shù)據(jù)集合。

var obj = {
  foo: 'Hello',
  bar: 'World'
};

上面代碼中,大括號就定義了一個對象,它被賦值給變量obj,所以變量obj就指向一個對象。該對象內(nèi)部包含兩個鍵值對(又稱為兩個“成員”),第一個鍵值對是foo: 'Hello',其中foo是“鍵名”(成員的名稱),字符串Hello是“鍵值”(成員的值)。鍵名與鍵值之間用冒號分隔。第二個鍵值對是bar: 'World',bar是鍵名,World是鍵值。兩個鍵值對之間用逗號分隔。

鍵名

對象的所有鍵名都是字符串(ES6 又引入了 Symbol 值也可以作為鍵名),所以加不加引號都可以。上面的代碼也可以寫成下面這樣。

var obj = {
  'foo': 'Hello',
  'bar': 'World'
};

如果鍵名是數(shù)值,會被自動轉(zhuǎn)為字符串。

var obj = {
  1: 'a',
  3.2: 'b',
  1e2: true,
  1e-2: true,
  .234: true,
  0xFF: true
};

obj
// Object {
//   1: "a",
//   3.2: "b",
//   100: true,
//   0.01: true,
//   0.234: true,
//   255: true
// }

obj['100'] // true

上面代碼中,對象obj的所有鍵名雖然看上去像數(shù)值,實際上都被自動轉(zhuǎn)成了字符串。

如果鍵名不符合標識名的條件(比如第一個字符為數(shù)字,或者含有空格或運算符),且也不是數(shù)字,則必須加上引號,否則會報錯。

// 報錯
var obj = {
  1p: 'Hello World'
};

// 不報錯
var obj = {
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

上面對象的三個鍵名,都不符合標識名的條件,所以必須加上引號。

對象的每一個鍵名又稱為“屬性”(property),它的“鍵值”可以是任何數(shù)據(jù)類型。如果一個屬性的值為函數(shù),通常把這個屬性稱為“方法”,它可以像函數(shù)那樣調(diào)用。

var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2

上面代碼中,對象obj的屬性p,就指向一個函數(shù)。

如果屬性的值還是一個對象,就形成了鏈式引用。

var o1 = {};
var o2 = { bar: 'hello' };

o1.foo = o2;
o1.foo.bar // "hello"

上面代碼中,對象o1的屬性foo指向?qū)ο?code>o2,就可以鏈式引用o2的屬性。

對象的屬性之間用逗號分隔,最后一個屬性后面可以加逗號(trailing comma),也可以不加。

var obj = {
  p: 123,
  m: function () { ... },
}

上面的代碼中,m屬性后面的那個逗號,有沒有都可以。

屬性可以動態(tài)創(chuàng)建,不必在對象聲明時就指定。

var obj = {};
obj.foo = 123;
obj.foo // 123

上面代碼中,直接對obj對象的foo屬性賦值,結(jié)果就在運行時創(chuàng)建了foo屬性。

對象的引用

如果不同的變量名指向同一個對象,那么它們都是這個對象的引用,也就是說指向同一個內(nèi)存地址。修改其中一個變量,會影響到其他所有變量。

var o1 = {};
var o2 = o1;

o1.a = 1;
o2.a // 1

o2.b = 2;
o1.b // 2

上面代碼中,o1o2指向同一個對象,因此為其中任何一個變量添加屬性,另一個變量都可以讀寫該屬性。

此時,如果取消某一個變量對于原對象的引用,不會影響到另一個變量。

var o1 = {};
var o2 = o1;

o1 = 1;
o2 // {}

上面代碼中,o1o2指向同一個對象,然后o1的值變?yōu)?,這時不會對o2產(chǎn)生影響,o2還是指向原來的那個對象。

但是,這種引用只局限于對象,如果兩個變量指向同一個原始類型的值。那么,變量這時都是值的拷貝。

var x = 1;
var y = x;

x = 2;
y // 1

上面的代碼中,當x的值發(fā)生變化后,y的值并不變,這就表示yx并不是指向同一個內(nèi)存地址。

表達式還是語句?

對象采用大括號表示,這導致了一個問題:如果行首是一個大括號,它到底是表達式還是語句?

{ foo: 123 }

JavaScript 引擎讀到上面這行代碼,會發(fā)現(xiàn)可能有兩種含義。第一種可能是,這是一個表達式,表示一個包含foo屬性的對象;第二種可能是,這是一個語句,表示一個代碼區(qū)塊,里面有一個標簽foo,指向表達式123。

為了避免這種歧義,JavaScript 引擎的做法是,如果遇到這種情況,無法確定是對象還是代碼塊,一律解釋為代碼塊。

{ console.log(123) } // 123

上面的語句是一個代碼塊,而且只有解釋為代碼塊,才能執(zhí)行。

如果要解釋為對象,最好在大括號前加上圓括號。因為圓括號的里面,只能是表達式,所以確保大括號只能解釋為對象。

({ foo: 123 }) // 正確
({ console.log(123) }) // 報錯

這種差異在eval語句(作用是對字符串求值)中反映得最明顯。

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

上面代碼中,如果沒有圓括號,eval將其理解為一個代碼塊;加上圓括號以后,就理解成一個對象。

屬性的操作

屬性的讀取

讀取對象的屬性,有兩種方法,一種是使用點運算符,還有一種是使用方括號運算符。

var obj = {
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

上面代碼分別采用點運算符和方括號運算符,讀取屬性p。

請注意,如果使用方括號運算符,鍵名必須放在引號里面,否則會被當作變量處理。

var foo = 'bar';

var obj = {
  foo: 1,
  bar: 2
};

obj.foo  // 1
obj[foo]  // 2

上面代碼中,引用對象objfoo屬性時,如果使用點運算符,foo就是字符串;如果使用方括號運算符,但是不使用引號,那么foo就是一個變量,指向字符串bar。

方括號運算符內(nèi)部還可以使用表達式。

obj['hello' + ' world']
obj[3 + 3]

數(shù)字鍵可以不加引號,因為會自動轉(zhuǎn)成字符串。

var obj = {
  0.7: 'Hello World'
};

obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

上面代碼中,對象obj的數(shù)字鍵0.7,加不加引號都可以,因為會被自動轉(zhuǎn)為字符串。

注意,數(shù)值鍵名不能使用點運算符(因為會被當成小數(shù)點),只能使用方括號運算符。

var obj = {
  123: 'hello world'
};

obj.123 // 報錯
obj[123] // "hello world"

上面代碼的第一個表達式,對數(shù)值鍵名123使用點運算符,結(jié)果報錯。第二個表達式使用方括號運算符,結(jié)果就是正確的。

屬性的賦值

點運算符和方括號運算符,不僅可以用來讀取值,還可以用來賦值。

var obj = {};

obj.foo = 'Hello';
obj['bar'] = 'World';

上面代碼中,分別使用點運算符和方括號運算符,對屬性賦值。

JavaScript 允許屬性的“后綁定”,也就是說,你可以在任意時刻新增屬性,沒必要在定義對象的時候,就定義好屬性。

var obj = { p: 1 };

// 等價于

var obj = {};
obj.p = 1;

屬性的查看

查看一個對象本身的所有屬性,可以使用Object.keys方法。

var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']

屬性的刪除:delete 命令

delete命令用于刪除對象的屬性,刪除成功后返回true。

var obj = { p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

上面代碼中,delete命令刪除對象objp屬性。刪除后,再讀取p屬性就會返回undefined,而且Object.keys方法的返回值也不再包括該屬性。

注意,刪除一個不存在的屬性,delete不報錯,而且返回true。

var obj = {};
delete obj.p // true

上面代碼中,對象obj并沒有p屬性,但是delete命令照樣返回true。因此,不能根據(jù)delete命令的結(jié)果,認定某個屬性是存在的。

只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

上面代碼之中,對象objp屬性是不能刪除的,所以delete命令返回false(關(guān)于Object.defineProperty方法的介紹,請看《標準庫》的 Object 對象一章)。

另外,需要注意的是,delete命令只能刪除對象本身的屬性,無法刪除繼承的屬性(關(guān)于繼承參見《面向?qū)ο缶幊獭氛鹿?jié))。

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

上面代碼中,toString是對象obj繼承的屬性,雖然delete命令返回true,但該屬性并沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值。

屬性是否存在:in 運算符

in運算符用于檢查對象是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。它的左邊是一個字符串,表示屬性名,右邊是一個對象。

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

in運算符的一個問題是,它不能識別哪些屬性是對象自身的,哪些屬性是繼承的。就像上面代碼中,對象obj本身并沒有toString屬性,但是in運算符會返回true,因為這個屬性是繼承的。

這時,可以使用對象的hasOwnProperty方法判斷一下,是否為對象自身的屬性。

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}

屬性的遍歷:for...in 循環(huán)

for...in循環(huán)用來遍歷一個對象的全部屬性。

var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
  console.log('鍵名:', i);
  console.log('鍵值:', obj[i]);
}
// 鍵名: a
// 鍵值: 1
// 鍵名: b
// 鍵值: 2
// 鍵名: c
// 鍵值: 3

for...in循環(huán)有兩個使用注意點。

  • 它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
  • 它不僅遍歷對象自身的屬性,還遍歷繼承的屬性。

舉例來說,對象都繼承了toString屬性,但是for...in循環(huán)不會遍歷到這個屬性。

var obj = {};

// toString 屬性是存在的
obj.toString // toString() { [native code] }

for (var p in obj) {
  console.log(p);
} // 沒有任何輸出

上面代碼中,對象obj繼承了toString屬性,該屬性不會被for...in循環(huán)遍歷到,因為它默認是“不可遍歷”的。關(guān)于對象屬性的可遍歷性,參見《標準庫》章節(jié)中 Object 一章的介紹。

如果繼承的屬性是可遍歷的,那么就會被for...in循環(huán)遍歷到。但是,一般情況下,都是只想遍歷對象自身的屬性,所以使用for...in的時候,應該結(jié)合使用hasOwnProperty方法,在循環(huán)內(nèi)部判斷一下,某個屬性是否為對象自身的屬性。

var person = { name: '老張' };

for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

with 語句

with語句的格式如下:

with (對象) {
  語句;
}

它的作用是操作同一個對象的多個屬性時,提供一些書寫的方便。

// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
  console.log(href);
  console.log(title);
  console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);

注意,如果with區(qū)塊內(nèi)部有變量的賦值操作,必須是當前對象已經(jīng)存在的屬性,否則會創(chuàng)造一個當前作用域的全局變量。

var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}

obj.p1 // undefined
p1 // 4

上面代碼中,對象obj并沒有p1屬性,對p1賦值等于創(chuàng)造了一個全局變量p1。正確的寫法應該是,先定義對象obj的屬性p1,然后在with區(qū)塊內(nèi)操作它。

這是因為with區(qū)塊沒有改變作用域,它的內(nèi)部依然是當前作用域。這造成了with語句的一個很大的弊病,就是綁定對象不明確。

with (obj) {
  console.log(x);
}

單純從上面的代碼塊,根本無法判斷x到底是全局變量,還是對象obj的一個屬性。這非常不利于代碼的除錯和模塊化,編譯器也無法對這段代碼進行優(yōu)化,只能留到運行時判斷,這就拖慢了運行速度。因此,建議不要使用with語句,可以考慮用一個臨時變量代替with。

with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}

// 可以寫成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

參考鏈接


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號