JavaScript 基礎入門04
JavaScript 對象
介紹
對象(object)
是js中的核心概念,也是最重要的數據類型。
那么什么是對象呢?
簡單的說,對象就是一組鍵值對
(key-value)的集合,是一種無序的復合數據集合。
var obj = {
name:"張三",
age:20
};
在上面的示例代碼中,大括號定義了一個對象
,並且將這個對象賦值給obj
這個變量,So,此時我們的變量obj
也就指向了一個對象。
在這個對象內部包含有兩個鍵值對(當然,也可以將他們稱為對象的兩個成員),第一個鍵值對是name:"張三"
,name
是鍵名,也就是成員的名稱,字符串張三
是鍵值,也就是成員的值。
在鍵名
和鍵值
之間使用冒號來進行分割。
第二個鍵值對是age:20
,鍵名是age
,鍵值是20
。
關於鍵名
一定要知道的一點是,在js中,對象的所有的鍵名
都是字符串(ES6中又引入了Symbol可以作為鍵名),所以對象的鍵名加不加引號都可以,例如上面的demo,也可以寫成下面的樣子:
var obj = {
"name":"張三",
"age":20
};
即使對象的鍵名是數值,也會被自動轉換成字符串。
需要注意的是,如果對象的鍵名不合符標識名的條件(比如第一個字符為數字,或者含有空格或運算符),且也不是數字,則必須加上引號,否則會報錯。
// 報錯
var obj = {
1p: 'Hello World'
};
// 不報錯
var obj = {
'1p': 'Hello World',
'h w': 'Hello World',
'p+q': 'Hello World'
};
上面對象的三個鍵名,都不符合標識名的條件,所以必須加上引號。
對象的每一個鍵名又稱為“屬性”(property),它的“鍵值”可以是任何數據類型。如果一個屬性的值為函數,通常把這個屬性稱為“方法”,它可以像函數那樣調用。
var obj = {
p: function (x) {
return 2 * x;
}
};
obj.p(1) // 2
上面代碼中,對象obj的屬性p,就指向一個函數。
如果屬性的值還是一個對象,就形成了鏈式引用。
var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"
上面代碼中,對象o1的屬性foo指向對象o2,就可以鏈式引用o2的屬性。
對象的屬性之間用逗號分隔,最后一個屬性后面可以加逗號(trailing comma),也可以不加。
var obj = {
p: 123,
m: function () { ... },
}
上面的代碼中,m屬性后面的那個逗號,有沒有都可以。
屬性可以動態創建,不必在對象聲明時就指定。
var obj = {};
obj.foo = 123;
obj.foo // 123
上面代碼中,直接對obj對象的foo屬性賦值,結果就在運行時創建了foo屬性。
對象的引用
如果不同的變量名指向同一個對象,那么它們就是這個對象的引用,簡單點說就是指向同一個內存地址。修改其中的一個變量,會影響到其他所有的變量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
上面代碼中,o1和o2指向同一個對象,因此為其中任何一個變量添加屬性,另一個變量都可以讀寫該屬性。
此時,如果取消某一個變量對於原對象的引用,不會影響到另一個變量。
var obj1 = {a:"hello,world"};
var obj2 = obj1;
obj1 = 1;
console.log(obj2.a); // hello,world
在上面的代碼中,obj1和obj2原本指向同一個對象,后期obj1指向了數字1,這時obj1的變化不會影響到obj2,obj2還指向原來的那個對象。
需要注意的是,這種引用的問題僅僅局限於對象類型,如果兩個變量指向同一個原始類型的值。那么,變量這時都是值的拷貝。
var x = 1;
var y = x;
x = 2;
y // 1
上面的代碼中,當x的值發生變化后,y的值並不變,這就表示y和x並不是指向同一個內存地址.
語句和表達式需要注意的地方
我們在上面說過,想要創建一個對象,需要通過大括號的形式。但是如果行首就出現了大括號,那么此時它到底是表達式還是語句呢?
{
name:"張三"
}
上面的代碼碰到了解析引擎,可能會出現兩種含義:
- 這段代碼中是一個表達式,表示的是一個包含於
name:"張三"
鍵值對的一個對象。 - 這表示了一個代碼區塊,里面有一個標簽
name
,指向了表達式張三
.
JS的解析引擎為了避免引發歧義,設定為,如果遇到這種代碼段,無法確定是對象還是代碼塊,一律解釋為代碼塊。
{
console.log(123);//輸出結果為123
}
上面的語句就會被解釋為代碼塊,因為只有處於代碼塊的環境下才能夠執行。
如果需要將大括號及里面的內容解釋為對象,最好在大括號的前面加上圓括號。因為圓括號的里面,只能是表達式,所以確保大括號只能解釋為對象。
// 下面的代碼正確
({
name:"張三"
})
// 下面的代碼錯誤
({
console.log(123);
})
對象屬性常見的操作
讀取屬性
在js的對象中,讀取屬性有兩種方法,一種是使用點運算符
,另外一種是使用方括號運算符
。
var obj = {
p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"
需要注意的是,當我們使用方括號運算符讀取鍵名的時候,鍵名必須放在引號里面,否則會被當做變量處理。
var foo = 'bar';
var obj = {
foo: 1,
bar: 2
};
obj.foo // 1
obj[foo] // 2
上面代碼中,引用對象obj的foo屬性時,如果使用點運算符,foo就是字符串;如果使用方括號運算符,但是不使用引號,那么foo就是一個變量,指向字符串bar。
方括號運算符內部還可以使用表達式。
obj['hello' + ' world']
obj[3 + 3]
數字鍵可以不加引號,因為會自動轉成字符串。
var obj = {
0.7: 'Hello World'
};
obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"
上面代碼中,對象obj的數字鍵0.7,加不加引號都可以,因為會被自動轉為字符串。
注意,數值鍵名不能使用點運算符(因為會被當成小數點),只能使用方括號運算符。
var obj = {
123: 'hello world'
};
obj.123 // 報錯
obj[123] // "hello world"
上面代碼的第一個表達式,對數值鍵名123使用點運算符,結果報錯。第二個表達式使用方括號運算符,結果就是正確的。
屬性的賦值
在上面我們說過的點運算符和方括號運算符不僅可以讀取屬性也可以對屬性進行賦值。
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
命令來刪除對象的屬性,刪除成功后返回true
。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
上面代碼中,delete命令刪除對象obj的p屬性。刪除后,再讀取p屬性就會返回undefined,而且Object.keys方法的返回值也不再包括該屬性。
注意,刪除一個不存在的屬性,delete不報錯,而且返回true。
var obj = {};
delete obj.p // true
上面代碼中,對象obj並沒有p屬性,但是delete命令照樣返回true。因此,不能根據delete命令的結果,認定某個屬性是存在的。
只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false
另外,需要注意的是,delete命令只能刪除對象本身的屬性,無法刪除繼承的屬性。
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
上面代碼中,toString是對象obj繼承的屬性,雖然delete命令返回true,但該屬性並沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值。
判斷屬性是否存在 in
在JS中我們可以通過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
來循環遍歷。
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循環有兩個使用注意點。
它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
它不僅遍歷對象自身的屬性,還遍歷繼承的屬性。
舉例來說,對象都繼承了toString屬性,但是for...in循環不會遍歷到這個屬性。
var obj = {};
// toString 屬性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
console.log(p);
} // 沒有任何輸出
上面代碼中,對象obj繼承了toString屬性,該屬性不會被for...in循環遍歷到,因為它默認是“不可遍歷”的。
如果繼承的屬性是可遍歷的,那么就會被for...in循環遍歷到。但是,一般情況下,都是只想遍歷對象自身的屬性,所以使用for...in的時候,應該結合使用hasOwnProperty方法,在循環內部判斷一下,某個屬性是否為對象自身的屬性。
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區塊內部有變量的賦值操作,必須是當前對象已經存在的屬性,否則會創造一個當前作用域的全局變量。
var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}
obj.p1 // undefined
p1 // 4
上面代碼中,對象obj並沒有p1屬性,對p1賦值等於創造了一個全局變量p1。正確的寫法應該是,先定義對象obj的屬性p1,然后在with區塊內操作它。
這是因為with區塊沒有改變作用域,它的內部依然是當前作用域。這造成了with語句的一個很大的弊病,就是綁定對象不明確。
with (obj) {
console.log(x);
}
單純從上面的代碼塊,根本無法判斷x到底是全局變量,還是對象obj的一個屬性。這非常不利於代碼的除錯和模塊化,編譯器也無法對這段代碼進行優化,只能留到運行時判斷,這就拖慢了運行速度。因此,建議不要使用with語句,可以考慮用一個臨時變量代替with。
with(obj1.obj2.obj3) {
console.log(p1 + p2);
}
// 可以寫成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);
JSON
JSON 格式(JavaScript Object Notation 的縮寫)是一種用於數據交換的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁瑣笨重的 XML 格式。
特點
相比 XML 格式,JSON 格式有兩個顯著的優點:書寫簡單,一目了然;符合 JavaScript 原生語法,可以由解釋引擎直接處理,不用另外添加解析代碼。所以,JSON 迅速被接受,已經成為各大網站交換數據的標准格式,並被寫入標准。
每個 JSON 對象就是一個值,可能是一個數組或對象,也可能是一個原始類型的值。總之,只能是一個值,不能是兩個或更多的值。
語法規則
- 復合類型的值只能是數組或對象,不能是函數、正則表達式對象、日期對象。
- 原始類型的值只有四種:字符串、數值(必須以十進制表示)、布爾值和null(不能使用NaN, Infinity, -Infinity和undefined)。
- 字符串必須使用雙引號表示,不能使用單引號。
- 對象的鍵名必須放在雙引號里面。
- 數組或對象最后一個成員的后面,不能加逗號。
JSON合法示例
下面都是一些合法的JSON對象
["one", "two", "three"]
{ "one": 1, "two": 2, "three": 3 }
{"names": ["張三", "李四"] }
[ { "name": "張三"}, {"name": "李四"} ]
下面是一些不正確的JSON
{ name: "張三", 'age': 32 } // 屬性名必須使用雙引號
[32, 64, 128, 0xFFF] // 不能使用十六進制值
{ "name": "張三", "age": undefined } // 不能使用 undefined
{ "name": "張三",
"birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
"getName": function () {
return this.name;
}
} // 屬性值不能使用函數和日期對象
注意,null、空數組和空對象都是合法的 JSON 值。
JSON對象
JSON對象是 JavaScript 的原生對象,用來處理 JSON 格式數據。它有兩個靜態方法:JSON.stringify()
和JSON.parse()
。
JSON.stringify()
1、基本使用方式:
JSON.stringify方法用於將一個值轉為 JSON 字符串。該字符串符合 JSON 格式,並且可以被JSON.parse方法還原。
JSON.stringify('abc') // ""abc""
JSON.stringify(1) // "1"
JSON.stringify(false) // "false"
JSON.stringify([]) // "[]"
JSON.stringify({}) // "{}"
JSON.stringify([1, "false", false])
// '[1,"false",false]'
JSON.stringify({ name: "張三" })
// '{"name":"張三"}'
上面代碼將各種類型的值,轉成 JSON 字符串。
注意,對於原始類型的字符串,轉換結果會帶雙引號。
JSON.stringify('foo') === "foo" // false
JSON.stringify('foo') === "\"foo\"" // true
上面代碼中,字符串foo,被轉成了""foo""。這是因為將來還原的時候,內層雙引號可以讓 JavaScript 引擎知道,這是一個字符串,而不是其他類型的值。
JSON.stringify(false) // "false"
JSON.stringify('false') // "\"false\""
上面代碼中,如果不是內層的雙引號,將來還原的時候,引擎就無法知道原始值是布爾值還是字符串。
如果對象的屬性是undefined、函數或 XML 對象,該屬性會被JSON.stringify過濾。
var obj = {
a: undefined,
b: function () {}
};
JSON.stringify(obj) // "{}"
上面代碼中,對象obj的a屬性是undefined,而b屬性是一個函數,結果都被JSON.stringify過濾。
如果數組的成員是undefined、函數或 XML 對象,則這些值被轉成null。
var arr = [undefined, function () {}];
JSON.stringify(arr) // "[null,null]"
上面代碼中,數組arr的成員是undefined和函數,它們都被轉成了null。
正則對象會被轉成空對象。
JSON.stringify(/foo/) // "{}"
JSON.stringify方法會忽略對象的不可遍歷的屬性。
var obj = {};
Object.defineProperties(obj, {
'foo': {
value: 1,
enumerable: true
},
'bar': {
value: 2,
enumerable: false
}
});
JSON.stringify(obj); // "{"foo":1}"
上面代碼中,bar是obj對象的不可遍歷屬性,JSON.stringify方法會忽略這個屬性。
2、第二個參數說明
JSON.stringify方法還可以接受一個數組,作為第二個參數,指定需要轉成字符串的屬性。
var obj = {
'prop1': 'value1',
'prop2': 'value2',
'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
JSON.stringify(obj, selectedProperties)
// "{"prop1":"value1","prop2":"value2"}"
上面代碼中,JSON.stringify方法的第二個參數指定,只轉prop1和prop2兩個屬性。
這個類似白名單的數組,只對對象的屬性有效,對數組無效。
JSON.stringify(['a', 'b'], ['0'])
// "["a","b"]"
JSON.stringify({0: 'a', 1: 'b'}, ['0'])
// "{"0":"a"}"
上面代碼中,第二個參數指定 JSON 格式只轉0號屬性,實際上對數組是無效的,只對對象有效。
第二個參數還可以是一個函數,用來更改JSON.stringify的返回值。
function f(key, value) {
if (typeof value === "number") {
value = 2 * value;
}
return value;
}
JSON.stringify({ a: 1, b: 2 }, f)
// '{"a": 2,"b": 4}'
上面代碼中的f函數,接受兩個參數,分別是被轉換的對象的鍵名和鍵值。如果鍵值是數值,就將它乘以2,否則就原樣返回。
注意,這個處理函數是遞歸處理所有的鍵。
var o = {a: {b: 1}};
function f(key, value) {
console.log("["+ key +"]:" + value);
return value;
}
JSON.stringify(o, f)
// []:[object Object]
// [a]:[object Object]
// [b]:1
// '{"a":{"b":1}}'
上面代碼中,對象o一共會被f函數處理三次,最后那行是JSON.stringify的輸出。第一次鍵名為空,鍵值是整個對象o;第二次鍵名為a,鍵值是{b: 1};第三次鍵名為b,鍵值為1。
遞歸處理中,每一次處理的對象,都是前一次返回的值。
var o = {a: 1};
function f(key, value) {
if (typeof value === 'object') {
return {b: 2};
}
return value * 2;
}
JSON.stringify(o, f)
// "{"b": 4}"
上面代碼中,f函數修改了對象o,接着JSON.stringify方法就遞歸處理修改后的對象o。
如果處理函數返回undefined或沒有返回值,則該屬性會被忽略。
function f(key, value) {
if (typeof(value) === "string") {
return undefined;
}
return value;
}
JSON.stringify({ a: "abc", b: 123 }, f)
// '{"b": 123}'
上面代碼中,a屬性經過處理后,返回undefined,於是該屬性被忽略了。
3、第三個參數
JSON.stringify還可以接受第三個參數,用於增加返回的 JSON 字符串的可讀性。如果是數字,表示每個屬性前面添加的空格(最多不超過10個);如果是字符串(不超過10個字符),則該字符串會添加在每行前面。
JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
"p1": 1,
"p2": 2
}"
*/
JSON.stringify({ p1:1, p2:2 }, null, '|-');
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
4、參數對象的toJSON方法
如果參數對象有自定義的toJSON方法,那么JSON.stringify會使用這個方法的返回值作為參數,而忽略原對象的其他屬性。
下面是一個普通的對象。
var user = {
firstName: '三',
lastName: '張',
get fullName(){
return this.lastName + this.firstName;
}
};
JSON.stringify(user)
// "{"firstName":"三","lastName":"張","fullName":"張三"}"
現在,為這個對象加上toJSON方法。
var user = {
firstName: '三',
lastName: '張',
get fullName(){
return this.lastName + this.firstName;
},
toJSON: function () {
return {
name: this.lastName + this.firstName
};
}
};
JSON.stringify(user)
// "{"name":"張三"}"
上面代碼中,JSON.stringify發現參數對象有toJSON方法,就直接使用這個方法的返回值作為參數,而忽略原對象的其他參數。
Date對象就有一個自己的toJSON方法。
var date = new Date('2015-01-01');
date.toJSON() // "2015-01-01T00:00:00.000Z"
JSON.stringify(date) // ""2015-01-01T00:00:00.000Z""
上面代碼中,JSON.stringify發現處理的是Date對象實例,就會調用這個實例對象的toJSON方法,將該方法的返回值作為參數。
toJSON方法的一個應用是,將正則對象自動轉為字符串。因為JSON.stringify默認不能轉換正則對象,但是設置了toJSON方法以后,就可以轉換正則對象了。
var obj = {
reg: /foo/
};
// 不設置 toJSON 方法時
JSON.stringify(obj) // "{"reg":{}}"
// 設置 toJSON 方法時
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/) // ""/foo/""
上面代碼在正則對象的原型上面部署了toJSON()方法,將其指向toString()方法,因此轉換成 JSON 格式時,正則對象就先調用toJSON()方法轉為字符串,然后再被JSON.stringify()方法處理。
JSON.parse()
JSON.parse方法用於將 JSON 字符串轉換成對應的值。
JSON.parse('{}') // {}
JSON.parse('true') // true
JSON.parse('"foo"') // "foo"
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('null') // null
var o = JSON.parse('{"name": "張三"}');
o.name // 張三
如果傳入的字符串不是有效的 JSON 格式,JSON.parse方法將報錯。
JSON.parse("'String'") // illegal single quotes
// SyntaxError: Unexpected token ILLEGAL
上面代碼中,雙引號字符串中是一個單引號字符串,因為單引號字符串不符合 JSON 格式,所以報錯。
為了處理解析錯誤,可以將JSON.parse方法放在try...catch代碼塊中。
try {
JSON.parse("'String'");
} catch(e) {
console.log('parsing error');
}
JSON.parse方法可以接受一個處理函數,作為第二個參數,用法與JSON.stringify方法類似。
function f(key, value) {
if (key === 'a') {
return value + 10;
}
return value;
}
JSON.parse('{"a": 1, "b": 2}', f)
// {a: 11, b: 2}
上面代碼中,JSON.parse的第二個參數是一個函數,如果鍵名是a,該函數會將鍵值加上10。
可以在下面這個網址進行JSON驗證:http://www.bejson.com/