[TOC]
## 概述
(1)定義
對象(object)是JavaScript的核心概念,也是最重要的數據類型。JavaScript的所有數據都可以被視為對象。
簡單說,所謂對象,就是一種無序的數據集合,由若干個“鍵值對”(key-value)構成。
~~~
var o = {
p: "Hello World"
};
~~~
上面代碼中,大括號就定義了一個對象,它被賦值給變量o。這個對象內部包含一個鍵值對(又稱為“成員”),p是“鍵名”(成員的名稱),字符串“Hello World”是“鍵值”(成員的值)。鍵名與鍵值之間用冒號分隔。如果對象內部包含多個鍵值對,每個鍵值對之間用逗號分隔。
(2)鍵名
鍵名加不加引號都可以,上面的代碼也可以寫成下面這樣。
~~~
var o = {
"p": "Hello World"
};
~~~
但是,如果鍵名不符合標識名的條件(比如包含數字、字母、下劃線以外的字符,或者第一個字符為數字),也不是正整數,則必須加上引號。
~~~
var o = {
"1p": "Hello World",
"h w": "Hello World",
"p+q": "Hello World"
};
~~~
上面對象的三個鍵名,都不符合標識名的條件,所以必須加上引號。由于對象是鍵值對的封裝,所以可以把對象看成是一個容器,里面封裝了多個成員,上面的對象就包含了三個成員。
(3)屬性
對象的每一個“鍵名”又稱為“屬性”(property),它的“鍵值”可以是任何數據類型。如果一個屬性的值為函數,通常把這個屬性稱為“方法”,它可以像函數那樣調用。
~~~
var o = {
p: function(x) {return 2*x;}
};
o.p(1)
// 2
~~~
上面的對象就有一個方法p,它就是一個函數。
對象的屬性之間用逗號分隔,ECMAScript 5規定最后一個屬性后面可以加逗號(trailing comma),也可以不加。
~~~
var o = {
p: 123,
m: function () { ... },
}
~~~
上面的代碼中m屬性后面的那個逗號,有或沒有都不算錯。但是,ECMAScript 3不允許添加逗號,所以如果要兼容老式瀏覽器(比如IE 8),那就不能加這個逗號。
### 生成方法
對象的生成方法,通常有三種方法。除了像上面那樣直接使用大括號生成({}),還可以用new命令生成一個Object對象的實例,或者使用Object.create方法生成。
~~~
var o1 = {};
var o2 = new Object();
var o3 = Object.create(null);
~~~
上面三行語句是等價的。一般來說,第一種采用大括號的寫法比較簡潔,第二種采用構造函數的寫法清晰地表示了意圖,第三種寫法一般用在需要對象繼承的場合。關于第二種寫法,詳見《標準庫》一章的Object對象一節,第三種寫法詳見《面向對象編程》一章。
### 讀寫屬性
(1)讀取屬性
讀取對象的屬性,有兩種方法,一種是使用點運算符,還有一種是使用方括號運算符。
~~~
var o = {
p: "Hello World"
};
o.p // "Hello World"
o["p"] // "Hello World"
~~~
上面代碼分別采用點運算符和方括號運算符,讀取屬性p。
請注意,如果使用方括號運算符,鍵名必須放在引號里面,否則會被當作變量處理。但是,數字鍵可以不加引號,因為會被當作字符串處理。
~~~
var o = {
0.7: "Hello World"
};
o.["0.7"] // "Hello World"
o[0.7] // "Hello World"
~~~
方括號運算符內部可以使用表達式。
~~~
o['hello' + ' world']
o[3+3]
~~~
(2)檢查變量是否聲明
如果讀取一個不存在的鍵,會返回undefined,而不是報錯。可以利用這一點,來檢查一個變量是否被聲明。
~~~
// 檢查a變量是否被聲明
if(a) {...} // 報錯
if(window.a) {...} // 不報錯
if(window['a']) {...} // 不報錯
~~~
上面的后二種寫法之所以不報錯,是因為在瀏覽器環境,所有全局變量都是window對象的屬性。window.a的含義就是讀取window對象的a屬性,如果該屬性不存在,就返回undefined,并不會報錯。
需要注意的是,后二種寫法有漏洞,如果a屬性是一個空字符串(或其他對應的布爾值為false的情況),則無法起到檢查變量是否聲明的作用。正確的寫法是使用in運算符。
~~~
if('a' in window) {
...
}
~~~
(3)寫入屬性
點運算符和方括號運算符,不僅可以用來讀取值,還可以用來賦值。
~~~
o.p = "abc";
o["p"] = "abc";
~~~
上面代碼分別使用點運算符和方括號運算符,對屬性p賦值。
JavaScript允許屬性的“后綁定”,也就是說,你可以在任意時刻新增屬性,沒必要在定義對象的時候,就定義好屬性。
~~~
var o = { p:1 };
// 等價于
var o = {};
o.p = 1;
~~~
(4)查看所有屬性
查看一個對象本身的所有屬性,可以使用Object.keys方法。
~~~
var o = {
key1: 1,
key2: 2
};
Object.keys(o);
// ["key1", "key2"]
~~~
### 屬性的刪除
刪除一個屬性,需要使用delete命令。
~~~
var o = { p:1 };
Object.keys(o) // ["p"]
delete o.p // true
o.p // undefined
Object.keys(o) // []
~~~
上面代碼表示,一旦使用delete命令刪除某個屬性,再讀取該屬性就會返回undefined,而且Object.keys方法返回的該對象的所有屬性中,也將不再包括該屬性。
麻煩的是,如果刪除一個不存在的屬性,delete不報錯,而且返回true。
~~~
var o = {};
delete o.p // true
~~~
上面代碼表示,delete命令只能用來保證某個屬性的值為undefined,而無法保證該屬性是否真的存在。
只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。
~~~
var o = Object.defineProperty({}, "p", {
value: 123,
configurable: false
});
o.p // 123
delete o.p // false
~~~
上面代碼之中,o對象的p屬性是不能刪除的,所以delete命令返回false(關于Object.defineProperty方法的介紹,請看《標準庫》一章的Object對象章節)。
另外,需要注意的是,delete命令只能刪除對象本身的屬性,不能刪除繼承的屬性(關于繼承參見《面向對象編程》一節)。delete命令也不能刪除var命令聲明的變量,只能用來刪除屬性。
### 對象的引用
如果不同的變量名指向同一個對象,那么它們都是這個對象的引用,也就是說指向同一個內存地址。修改其中一個變量,會影響到其他所有變量。
~~~
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
~~~
上面代碼之中,o1和o2指向同一個對象,因此為其中任何一個變量添加屬性,另一個變量都可以讀寫該屬性。
但是,這種引用只局限于對象,對于原始類型的數據則是傳值引用,也就是說,都是值的拷貝。
~~~
var x = 1;
var y = x;
x = 2;
y // 1
~~~
上面的代碼中,當x的值發生變化后,y的值并不變,這就表示y和x并不是指向同一個內存地址。
### in運算符
in運算符用于檢查對象是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。
~~~
var o = { p: 1 };
'p' in o // true
~~~
該運算符對數組也適用。
~~~
var a = ["hello", "world"];
0 in a // true
1 in a // true
2 in a // false
'0' in a // true
'1' in a // true
'2' in a // false
~~~
上面代碼表示,數字鍵0和1都在數組之中。由于數組是一種特殊對象,而對象的鍵名都是字符串,所以字符串的”0“和”1“,也是數組的鍵名。
在JavaScript語言中,所有全局變量都是頂層對象(瀏覽器的頂層對象就是window對象)的屬性,因此可以用in運算符判斷,一個全局變量是否存在。
~~~
// 假設變量x未定義
// 寫法一:報錯
if (x){ return 1; }
// 寫法二:不正確
if (window.x){ return 1; }
// 寫法三:正確
if ('x' in window) { return 1; }
~~~
上面三種寫法之中,如果x不存在,第一種寫法會報錯;如果x的值對應布爾值false(比如x等于空字符串),第二種寫法無法得到正確結果;只有第三種寫法,才能正確判斷變量x是否存在。
in運算符的一個問題是,它不能識別對象繼承的屬性。
~~~
var o = new Object();
o.hasOwnProperty('toString') // false
'toString' in o // true
~~~
上面代碼中,toString方法不是對象o自身的屬性,而是繼承的屬性,hasOwnProperty方法可以說明這一點。但是,in運算符不能識別,對繼承的屬性也返回true。
### for...in循環
for...in循環用來遍歷一個對象的全部屬性。
~~~
var o = {a:1, b:2, c:3};
for (i in o){
console.log(o[i]);
}
// 1
// 2
// 3
~~~
注意,for...in循環遍歷的是對象所有可enumberable的屬性,其中不僅包括定義在對象本身的屬性,還包括對象繼承的屬性。
~~~
// 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循環會遍歷實例自身的屬性(name),
// 以及繼承的屬性(describe)
for (var key in person) {
console.log(key);
}
// name
// describe
~~~
上面代碼中,name是對象本身的屬性,describe是對象繼承的屬性,for-in循環的遍歷會包括這兩者。
如果只想遍歷對象本身的屬性,可以使用hasOwnProperty方法,在循環內部做一個判斷。
~~~
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
~~~
為了避免這一點,可以新建一個繼承null的對象。由于null沒有任何屬性,所以新對象也就不會有繼承的屬性了。
## 類似數組的對象
在JavaScript中,有些對象被稱為“類似數組的對象”(array-like object)。意思是,它們看上去很像數組,可以使用length屬性,但是它們并不是數組,所以無法使用一些數組的方法。
下面就是一個類似數組的對象。
~~~
var a = {
0:'a',
1:'b',
2:'c',
length:3
};
a[0] // 'a'
a[2] // 'c'
a.length // 3
~~~
上面代碼的變量a是一個對象,但是看上去跟數組很像。所以只要有數字鍵和length屬性,就是一個類似數組的對象。當然,變量a無法使用數組特有的一些方法,比如pop和push方法。而且,length屬性不是動態值,不會隨著成員的變化而變化。
~~~
a[3] = 'd';
a.length // 3
~~~
上面代碼為對象a添加了一個數字鍵,但是length屬性沒變。這就說明了a不是數組。
典型的類似數組的對象是函數的arguments對象,以及大多數DOM元素集,還有字符串。
~~~
// arguments對象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false
// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false
~~~
通過函數的call方法,可以用slice方法將類似數組的對象,變成真正的數組。
~~~
var arr = Array.prototype.slice.call(arguments);
~~~
遍歷類似數組的對象,可以采用for循環,也可以采用數組的forEach方法。
~~~
// for循環
function logArgs() {
for (var i=0; i<arguments.length; i++) {
console.log(i+'. '+arguments[i]);
}
}
// forEach方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i+'. '+elem);
});
}
~~~
## with語句
with語句的格式如下:
~~~
with (object)
statement
~~~
它的作用是操作同一個對象的多個屬性時,提供一些書寫的方便。
~~~
// 例一
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區塊內部的變量,必須是當前對象已經存在的屬性,否則會創造一個當前作用域的全局變量。這是因為with區塊沒有改變作用域,它的內部依然是當前作用域。
~~~
var o = {};
with (o){
x = "abc";
}
o.x
// undefined
x
// "abc"
~~~
上面代碼中,對象o沒有屬性x,所以with區塊內部對x的操作,等于創造了一個全局變量x。正確的寫法應該是,先定義對象o的屬性x,然后在with區塊內操作它。
~~~
var o = {};
o.x = 1;
with (o){
x = 2;
}
o.x
// 2
~~~
這是with語句的一個很大的弊病,就是綁定對象不明確。
~~~
with (o) {
console.log(x);
}
~~~
單純從上面的代碼塊,根本無法判斷x到底是全局變量,還是o對象的一個屬性。這非常不利于代碼的除錯和模塊化,編譯器也無法對這段代碼進行優化,只能留到運行時判斷,這就拖慢了運行速度。因此,建議不要使用with語句,可以考慮用一個臨時變量代替with。
~~~
with(o1.o2.o3) {
console.log(p1 + p2);
}
// 可以寫成
var temp = o1.o2.o3;
console.log(temp.p1 + temp.p2);
~~~
with語句少數有用場合之一,就是替換模板變量。
~~~
var str = 'Hello <%= name %>!';
~~~
上面代碼是一個模板字符串,為了替換其中的變量name,可以先將其分解成三部分`'Hello ', name, '!'`,然后進行模板變量替換。
~~~
var o = {
name: 'Alice'
};
var p = [];
var tmpl = '';
with(o){
p.push('Hello ', name, '!');
};
p.join('') // "Hello Alice!"
~~~
上面代碼中,with區塊內部,模板變量name可以被對象o的屬性替換,而p依然是全局變量。事實上,這就是很多模板引擎的實現原理。
## 參考鏈接
* Dr. Axel Rauschmayer,[Object properties in JavaScript](http://www.2ality.com/2012/10/javascript-properties.html)
* Lakshan Perera,?[Revisiting JavaScript Objects](http://www.laktek.com/2012/12/29/revisiting-javascript-objects/)
* Angus Croll,?[The Secret Life of JavaScript Primitives](http://javascriptweblog.wordpress.com/2010/09/27/the-secret-life-of-javascript-primitives/)i
* Dr. Axel Rauschmayer,?[JavaScript’s with statement and why it’s deprecated](http://www.2ality.com/2011/06/with-statement.html)
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架