JavaScript 對象

2018-07-24 11:49 更新

目錄

概述

生成方法

對象(object)是JavaScript的核心概念,也是最重要的數(shù)據(jù)類型。JavaScript的所有數(shù)據(jù)都可以被視為對象。

簡單說,所謂對象,就是一種無序的數(shù)據(jù)集合,由若干個“鍵值對”(key-value)構(gòu)成。

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

上面代碼中,大括號就定義了一個對象,它被賦值給變量o。這個對象內(nèi)部包含一個鍵值對(又稱為“成員”),p是“鍵名”(成員的名稱),字符串Hello World是“鍵值”(成員的值)。鍵名與鍵值之間用冒號分隔。如果對象內(nèi)部包含多個鍵值對,每個鍵值對之間用逗號分隔。

var o = {
  p1: 'Hello',
  p2: 'World'
};

對象的生成方法,通常有三種方法。除了像上面那樣直接使用大括號生成({}),還可以用new命令生成一個Object對象的實(shí)例,或者使用Object.create方法生成。

var o1 = {};
var o2 = new Object();
var o3 = Object.create(Object.prototype);

上面三行語句是等價的。一般來說,第一種采用大括號的寫法比較簡潔,第二種采用構(gòu)造函數(shù)的寫法清晰地表示了意圖,第三種寫法一般用在需要對象繼承的場合。關(guān)于第二種寫法,詳見《標(biāo)準(zhǔn)庫》一章的《Object 對象》一節(jié),第三種寫法詳見《面向?qū)ο缶幊獭芬徽隆?/p>

鍵名

對象的所有鍵名都是字符串,所以加不加引號都可以。上面的代碼也可以寫成下面這樣。

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

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

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

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

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

var o = {
  '1p': "Hello World",
  'h w': "Hello World",
  'p+q': "Hello World"
};

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

注意,JavaScript的保留字可以不加引號當(dāng)作鍵名。

var obj = {
  for: 1,
  class: 2
};

屬性

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

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

o.p(1)
// 2

上面的對象就有一個方法p,它就是一個函數(shù)。

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

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

上面的代碼中m屬性后面的那個逗號,有或沒有都不算錯。

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

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

上面代碼中,直接對obj對象的foo屬性賦值,結(jié)果就在運(yùn)行時創(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還是指向原來的那個對象。

但是,這種引用只局限于對象,對于原始類型的數(shù)據(jù)則是傳值引用,也就是說,都是值的拷貝。

var x = 1;
var y = x;

x = 2;
y // 1

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

表達(dá)式還是語句?

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

{ foo: 123 }

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

為了避免這種歧義,JavaScript規(guī)定,如果行首是大括號,一律解釋為語句(即代碼塊)。如果要解釋為表達(dá)式(即對象),必須在大括號前加上圓括號。

({ foo: 123})

這種差異在eval語句中反映得最明顯。

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

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

屬性的操作

讀取屬性

讀取對象的屬性,有兩種方法,一種是使用點(diǎn)運(yùn)算符,還有一種是使用方括號運(yùn)算符。

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

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

上面代碼分別采用點(diǎn)運(yùn)算符和方括號運(yùn)算符,讀取屬性p。

請注意,如果使用方括號運(yùn)算符,鍵名必須放在引號里面,否則會被當(dāng)作變量處理。但是,數(shù)字鍵可以不加引號,因為會被當(dāng)作字符串處理。

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

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

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

o['hello' + ' world']
o[3 + 3]

數(shù)值鍵名不能使用點(diǎn)運(yùn)算符(因為會被當(dāng)成小數(shù)點(diǎn)),只能使用方括號運(yùn)算符。

obj.0xFF
// SyntaxError: Unexpected token
obj[0xFF]
// true

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

檢查變量是否聲明

如果讀取一個不存在的鍵,會返回undefined,而不是報錯。可以利用這一點(diǎn),來檢查一個全局變量是否被聲明。

// 檢查a變量是否被聲明
if (a) {...} // 報錯

if (window.a) {...} // 不報錯
if (window['a']) {...} // 不報錯

上面的后二種寫法之所以不報錯,是因為在瀏覽器環(huán)境,所有全局變量都是window對象的屬性。window.a的含義就是讀取window對象的a屬性,如果該屬性不存在,就返回undefined,并不會報錯。

需要注意的是,后二種寫法有漏洞,如果a屬性是一個空字符串(或其他對應(yīng)的布爾值為false的情況),則無法起到檢查變量是否聲明的作用。正確的做法是可以采用下面的寫法。

if ('a' in window) {
  // 變量 a 聲明過
} else {
  // 變量 a 未聲明
}

屬性的賦值

點(diǎn)運(yùn)算符和方括號運(yùn)算符,不僅可以用來讀取值,還可以用來賦值。

o.p = 'abc';
o['p'] = 'abc';

上面代碼分別使用點(diǎn)運(yùn)算符和方括號運(yùn)算符,對屬性p賦值。

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

var o = { p: 1 };

// 等價于

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

查看所有屬性

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

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

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

delete命令

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

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

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

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

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

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

上面代碼中,o對象并沒有p屬性,但是delete命令照樣返回true。因此,不能根據(jù)delete命令的結(jié)果,認(rèn)定某個屬性是存在的,只能保證讀取這個屬性肯定得到undefined。

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

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

o.p // 123
delete o.p // false

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

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

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

上面代碼中,toString是對象o繼承的屬性,雖然delete命令返回true,但該屬性并沒有被刪除,依然存在。

最后,delete命令不能刪除var命令聲明的變量,只能用來刪除屬性。

var p = 1;
delete p // false
delete window.p // false

上面命令中,pvar命令聲明的變量,delete命令無法刪除它,返回false。因為var聲明的全局變量都是頂層對象的屬性,而且默認(rèn)不得刪除。

in運(yùn)算符

in運(yùn)算符用于檢查對象是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。

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

在JavaScript語言中,所有全局變量都是頂層對象(瀏覽器的頂層對象就是window對象)的屬性,因此可以用in運(yùn)算符判斷,一個全局變量是否存在。

// 假設(shè)變量x未定義

// 寫法一:報錯
if (x) { return 1; }

// 寫法二:不正確
if (window.x) { return 1; }

// 寫法三:正確
if ('x' in window) { return 1; }

上面三種寫法之中,如果x不存在,第一種寫法會報錯;如果x的值對應(yīng)布爾值false(比如x等于空字符串),第二種寫法無法得到正確結(jié)果;只有第三種寫法,才能正確判斷變量x是否存在。

in運(yùn)算符的一個問題是,它不能識別對象繼承的屬性。

var o = new Object();
o.hasOwnProperty('toString') // false

'toString' in o // true

上面代碼中,toString方法不是對象o自身的屬性,而是繼承的屬性,hasOwnProperty方法可以說明這一點(diǎn)。但是,in運(yùn)算符不能識別,對繼承的屬性也返回true。

for…in循環(huán)

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

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

for (var i in o) {
  console.log(o[i]);
}
// 1
// 2
// 3

下面是一個使用for...in循環(huán),提取對象屬性的例子。

var obj = {
  x: 1,
  y: 2
};
var props = [];
var i = 0;

for (props[i++] in obj);

props // ['x', 'y']

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

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

請看下面的例子。

// name 是 Person 本身的屬性
function Person(name) {
  this.name = name;
}

// describe是Person.prototype的屬性
Person.prototype.describe = function () {
  return 'Name: '+this.name;
};

var person = new Person('Jane');

// for...in循環(huán)會遍歷實(shí)例自身的屬性(name),
// 以及繼承的屬性(describe)
for (var key in person) {
  console.log(key);
}
// name
// describe

上面代碼中,name是對象本身的屬性,describe是對象繼承的屬性,for...in循環(huán)的遍歷會包括這兩者。

如果只想遍歷對象本身的屬性,可以使用hasOwnProperty方法,在循環(huán)內(nèi)部判斷一下是不是自身的屬性。

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

對象person其實(shí)還有其他繼承的屬性,比如toString。

person.toString()
// "[object Object]"

這個toString屬性不會被for...in循環(huán)遍歷到,因為它默認(rèn)設(shè)置為“不可遍歷”,詳見《標(biāo)準(zhǔn)庫》一章的Object對象部分。

一般情況下,都是只想遍歷對象自身的屬性,所以不推薦使用for...in循環(huán)。

with語句

with語句的格式如下:

with (object) {
  statements;
}

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

// 例一
with (o) {
  p1 = 1;
  p2 = 2;
}
// 等同于
o.p1 = 1;
o.p2 = 2;

// 例二
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)部的變量,必須是當(dāng)前對象已經(jīng)存在的屬性,否則會創(chuàng)造一個當(dāng)前作用域的全局變量。這是因為with區(qū)塊沒有改變作用域,它的內(nèi)部依然是當(dāng)前作用域。

var o = {};

with (o) {
  x = "abc";
}

o.x // undefined
x // "abc"

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

var o = {};
o.x = 1;

with (o) {
  x = 2;
}

o.x // 2

這是with語句的一個很大的弊病,就是綁定對象不明確。

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

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

with(o1.o2.o3) {
  console.log(p1 + p2);
}

// 可以寫成

var temp = o1.o2.o3;
console.log(temp.p1 + temp.p2);

with語句少數(shù)有用場合之一,就是替換模板變量。

var str = 'Hello <%= name %>!';

上面代碼是一個模板字符串。假定有一個parser函數(shù),可以將這個字符串解析成下面的樣子。

parser(str)
// '"Hello ", name, "!"'

那么,就可以利用with語句,進(jìn)行模板變量替換。

var str = 'Hello <%= name %>!';

var o = {
  name: 'Alice'
};

function tmpl(str, obj) {
  str = 'var p = [];' +
    'with (obj) {p.push(' + parser(str) + ')};' +
    'return p;'
  var r = (new Function('obj', str))(obj);
  return r.join('');
}

tmpl(str, o)
// "Hello Alice!"

上面代碼的核心邏輯是下面的部分。

var o = {
  name: 'Alice'
};

var p = [];

with (o) {
  p.push('Hello ', name, '!');
};

p.join('') // "Hello Alice!"

上面代碼中,with區(qū)塊內(nèi)部,模板變量name可以被對象o的屬性替換,而p依然是全局變量。這就是很多模板引擎的實(shí)現(xiàn)原理。

參考鏈接

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號