對象能夠經過兩種形式定義:聲明(文字)形式和構造形式。算法
對象的文字語法大概是這樣:數組
var myObj = {
key: value,
// ...
};
複製代碼
構造形式大概是這樣:安全
var myObj = new Object();
myObj.key = value;
複製代碼
構造形式和文字形式生成的對象是同樣的。惟一的區別是,在文字聲明中你能夠添加多個鍵值對,可是在構造形式中你必須逐個添加屬性。數據結構
對象是JavaScript的基礎。在JavaScript中一共有六種主要類型(術語是「語言類型」):函數
注意,簡單基本類型(string,boolean,number,null和undefined)自己並非對象。 null有時會被看成一種對象類型,可是這其實只是語言自己的一個bug,即對null執行typeof null時會返回字符串"object"。實際上,null自己是基本類型。學習
JavaScript中有許多特殊的對象子類型,咱們能夠稱之爲複雜基本類型。優化
函數就是對象的一個子類型(從技術角度來講就是「可調用的對象」)。JavaScript中的函數是「一等公民」,由於它們本質上和普通的對象同樣(只是能夠調用),因此能夠像操做其餘對象同樣操做函數(好比看成另外一個函數的參數)。ui
數組也是對象的一種類型,具有一些額外的行爲。數組中內容的組織方式比通常的對象要稍微複雜一些。this
JavaScript中還有一些對象子類型,一般被稱爲內置對象。有些內置對象的名字看起來和簡單基礎類型同樣,不過實際上它們的關係更復雜。spa
這些內置對象實際上只是一些內置函數。這些內置函數能夠看成構造函數(由new產生的函數調用)來使用,從而能夠構造一個對應子類型的新對象。
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查sub-type對象
Object.prototype.toString.call(strObject); // [Object String]
複製代碼
原始值「I am a string」並非一個對象,它只是一個字面量,而且是一個不可變的值。若是要在這個字面量上執行一些操做,好比獲取長度、訪問其中某個字符等,那須要將其轉換爲String對象。
幸虧,在必要時語言會自動把字符串字面量轉換成一個String對象,也就是說你並不須要顯式建立一個對象。JavaScript社區中的大多數人都認爲能使用文字形式時就不要使用構造形式。
var strPrimitive = "I am a string";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
複製代碼
以上兩種方法,均可以直接在字符串字面量上訪問屬性或者方法,之因此能夠這樣,是由於引擎自動把字面量轉換成String對象,因此能夠訪問屬性和方法。
一樣的事也會發生在數值字面量上,若是使用相似42.359.toFixed(2)的方法,引擎會把42轉換成new Number(42)。對於布爾字面量來講也是如此。
null和undefined沒有對應的構造形式,它們只有文字形式。相反,Date只有構造,沒有文字形式。
對於Object、Array、Function和RegExp來講,不管使用文字形式仍是構造形式,它們都是對象,不是字面量。
Error對象不多在代碼中顯式建立,通常是在拋出異常時被自動建立。
對象的內容是由一些存儲在特定命名位置的(任意類型的)值組成的,咱們稱之爲屬性。
var myObject = {
a: 2
}
myObject.a; // 2
myObject['a']; // 2
複製代碼
.a語法一般被稱爲「屬性訪問」,["a"]語法一般被稱爲「鍵訪問」。實際上它們訪問的是同一個位置,而且會返回相同的值2,而且這兩個術語是能夠互換的。
這兩種語法的主要區別在於.操做符要求屬性名知足標識符的命名規範,而[""]語法能夠接受任意UTF-8/Unicode字符串做爲屬性名。
var myObject = {
a: 2
}
var idx;
if(wantA) {
idx = "a";
}
console.log(myObject[idx]); // 2
複製代碼
在對象中,屬性名永遠都是字符串。若是你使用string(字面量)之外的其餘值做爲屬性名,那它首先會被轉換爲一個字符串。即便是數字也不例外,雖然在數組下標中使用的的確是數字,可是在對象屬性名中數字會被轉換成字符串,因此小心不要搞混對象和數組中數字的用法:
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
複製代碼
ES6增長了可計算屬性名,能夠在文字形式中使用[]包裹一個表達式來看成屬性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
}
myObject["foobar"]; // hello
myObject["foobaz"]; // world
複製代碼
不管返回值是什麼類型,每次訪問對象的屬性就是屬性訪問。若是屬性訪問返回的是一個函數,那它並非一個「方法」。屬性訪問返回的函數和其餘函數沒有任何區別(除了可能發生的隱式綁定this)。
function foo() {
console.log("foo");
}
var someFoo = foo; // 對foo的變量引用
var myObject = {
someFoo: foo
}
foo; // function foo() {...}
someFoo; // function foo() {...}
myObject.someFoo; // function foo() {...}
複製代碼
someFoo和myObject.someFoo只是對於同一個函數的不一樣引用,並不能說明這個函數是特別的或者「屬於」某個對象。若是foo()定義時內部有一個this引用,那這兩個函數引用的惟一區別就是myObject.someFoo中的this會被隱式綁定到一個對象。
即便你在對象的文字形式中聲明一個函數表達式,這個函數也不會「屬於」這個對象--它們只是對於相同函數對象的多個引用。
var myObject = {
foo: function() {
console.log("foo");
}
}
var someFoo = myObject.foo;
someFoo; // function foo() {...}
myObject.foo; // function foo() {...}
複製代碼
數組也支持[]訪問形式,可是數組指望的是數值下標,也就是說值存儲的位置(一般被稱爲索引)是非負整數,好比說0和42:
var myArray = ['foo', 42, 'bar'];
myArray.length; // 3
myArray[0]; // 'foo'
myArray[2]; // 'bar'
複製代碼
數組也是對象,因此雖然每一個下標都是整數,你仍然能夠給數組添加屬性:
var myArray = ['foo', 42, 'bar'];
myArray.baz = 'baz';
myArray.length; // 3
myArray.baz; // 'baz'
複製代碼
能夠看到雖然添加了命名屬性(不管是經過.語法仍是[]語法),數組的length值並未發生變化。
數組和普通的對象都根據其對應的行爲和用途進行了優化,因此最好只用對象來存儲鍵值對,只用數組來存儲數值下標/值對。
注意:若是你視圖向數組添加一個屬性,可是屬性名「看起來」像一個數字,那它會變成一個數值下標(所以會修改數組的內容而不是添加一個屬性):
var myArray = ['foo', 42, 'bar'];
myArray['3'] = 'baz';
myArray.length; // 4
myArray[3]; // 'baz'
複製代碼
JavaScript初學者最多見的問題之一就是如何複製一個對象。
舉例來講,思考一下這個對象:
function anotherFunction() { /*...*/ }
var anotherObject = {
c: true
}
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是複本!
c: anotherArray, // 另外一個引用
d: anotherFunction
}
anotherArray.push(anotherObject, myObject);
複製代碼
如何準確地表示myObject的複製呢?
首先,咱們應該判斷它是淺拷貝仍是深拷貝。對於淺拷貝來講,複製出的新對象中a的值會複製舊對象中a的值,也就是2,可是新對象中b,c,d三個屬性其實只是三個引用,它們和舊對象中b,c,d引用的對象是同樣的。對於深拷貝來講,除了複製myObject之外還好複製anotherObject和anotherArray。這時問題來了,anotherArray引用了anotherObject和myObject,因此又須要複製myObject,這樣就會因爲循環引用致使死循環。
對於JSON安全(也就是說能夠被序列化爲一個JSON字符串而且能夠根據這個字符串解析出一個結構和值徹底同樣的對象)的對象來講,有一種巧妙的深拷貝方法:
var newObj = JSON.parse(JSON.stringify(someObj));
複製代碼
固然,這種方法須要保證JSON安全的,因此只適用於部分狀況。
ES6定義了Object.assign(..)方法來實現淺拷貝。Object.assign(..)方法的第一個參數是目標對象,以後還能夠跟一個或多個源對象。它會遍歷一個或多個源對象的全部可枚舉的自由鍵並把它們賦值(使用=操做符賦值)到目標對象,最後返回目標對象,就像這樣:
var newObj = Object.assign({}, myObject);
newObj.a; //2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
複製代碼
在ES5以前,JavaScript語言自己並無提供能夠直接檢測屬性特性的方法,好比判斷屬性是否可讀。
可是從ES5開始,全部的屬性都具有了屬性描述符。
思考下面的代碼:
var myObject = {
a: 2
}
Object.getOwnPropertyDescriptor(myObject, 'a');
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
複製代碼
如你所見,這個普通的對象屬性對應的屬性描述符可不只僅只是一個2。它還包好另外三個特性:writable(可寫)、enumerable(可枚舉)、和configurable(可配置)。
在建立普通屬性時屬性描述符會使用默認值,咱們也可使用Object.defineProperty(..)來添加一個新屬性或者修改一個已有屬性(若是它是configurable)並對特性進行設置。
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
configurable: true,
enumerable: true
})
myObject.a; // 2
複製代碼
咱們使用defineProperty(..)給myObject添加了一個普通的屬性並顯式指定了一些特性。然而,通常來講你不會使用這種方式,除非你想修改屬性描述符。
writable決定是否能夠修改屬性的值。
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: false, // 不可寫
configurable: true,
enumerable: true
})
myObject.a = 3;
myObjec.a; // 2
複製代碼
若是在嚴格模式下,這種方法會出錯:
'use strict'
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: false, // 不可寫
configurable: true,
enumerable: true
})
myObject.a = 3; // TypeError
複製代碼
只要屬性是可配置的,就可使用defineProperty(..)方法來修改屬性描述符:
var myObject = {
a: 3
}
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject,'a', {
value: 4,
writable: true,
configurable: false, // 不可配置
enumerable: true
})
myObject.a; //4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject,'a', {
value: 6,
writable: true,
configurable: true,
enumerable: true
}); // TypeError
複製代碼
最後一個defineProperty(..)會產生一個TypeError錯誤,無論是否是處於嚴格模式,嘗試修改一個不可配置的屬性描述符都會出錯,注意:把configurable修改爲false是單向操做,沒法撤銷!
有一個小小的例外,即使屬性是configurable: false,
咱們仍是能夠把writable狀態由true改成false,可是沒法由false改成true。
複製代碼
除了沒法修改,configurable: false還會禁止刪除這個屬性:
var myObject = {
a: 2
}
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
configurable: false,
enumerable: true
})
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
複製代碼
這個描述符控制的是屬性是否會出如今對象的屬性枚舉中,好比說for..in循環。若是把enumerable設置成false,這個屬性就不會出如今枚舉中,雖然仍然能夠正常訪問它,相對地,設置成true就會讓它出如今枚舉中。
有時候你會但願屬性或者對象是不可改變(不管是有意仍是無心)的,在ES5中能夠經過不少種方法來實現。
很重要的一點是,全部的方法建立的都是淺不變性,也就是說,它們只會影響目標對象和它的直接屬性。若是目標對象引用了其餘對象(數組、對象、函數等),其餘對象的內容不受影響,仍然是可變的:
myImmutableObject.foo; // [1,2,3];
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]
複製代碼
假設代碼中的myImmutableObject已經被建立並且是不可變的,可是爲了保護它的內容myImmutableObject.foo,你還須要使用下面的方法讓foo也不可變。
結合writable:false和configurable:false就能夠建立一個真正的常量屬性(不可修改、重定義或者刪除):
var myObject = {};
Object.defineProperty(myObject,"FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
})
複製代碼
若是你想禁止一個對象添加新屬性而且保留已有屬性,可使用Object.preventExtensions(..):
var myObject = {
a: 2
}
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
複製代碼
Object.seal(..)會建立一個「密封」的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..)並把全部現有屬性標記爲configurable:false。
因此,密封以後不只不能添加新屬性,也不能從新配置或者刪除任何現有屬性(雖然能夠修改屬性的值)。
Object.freeze(..)會建立一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(..)並把全部「數據訪問」屬性標記爲writable:false,這樣就沒法修改它們的值。
這個方法是你能夠應用在對象上的級別最高的不可變性,它會禁止對於對象自己及其任意直接屬性的修改(不過就像咱們以前說過的,這個對象引用的其餘對象是不受影響的)。
你能夠「深度凍結」一個對象,具體方法爲,首先在這個對象上調用Object.freeze(..),而後遍歷它引用的全部對象並在這些對象上調用Object.freeze(..)。可是必定要當心,由於這樣可能會在無心中凍結其餘(共享)對象。
屬性訪問在實現時有一個微妙卻很是重要的細節,思考下面的代碼:
var myObject = {
a: 2
}
myObject.a; // 2
複製代碼
myObject.a是一次屬性訪問,可是這條語句並不只僅是在myObject中查找名字爲a的屬性,雖然看起來好像是這樣。
在語言規範中,myObject.a在myObject其實是實現了[[Get]]操做。對象默認的內置的[[Get]]操做首先在對象中查找是否有名稱相同的屬性,若是找到就會返回這個屬性的值。
然而,若是沒有找到名稱相同的屬性,按照[[Get]]算法的定義會執行另一種很是重要的行爲。遍歷可能存在的[[prototype]]鏈,也就是原型鏈。
若是不管如何都沒有找到名稱相同的屬性,那麼[[Get]]操做會返回值undefined:
var myObject = {
a: 2
}
myObject.b; // undefined
複製代碼
注意,這種方法和訪問變量時是不同的。若是你引用了一個檔期詞法做用域中不存在的變量,並不會像對象屬性同樣返回undefined,而是會拋出一個ReferenceError異常:
var myObject = {
a: undefined
}
myObject.a; // undefined
myObject.b; // undefined
複製代碼
從返回值的角度來講,這兩個引用沒有什麼區別--它們都返回了undefined。然而,實際上底層的[[Get]]操做對myObject.b進行了更復雜的處理。
既然有能夠獲取屬性值的[[Get]]操做,就必定有對應的[[Put]]操做。
[[put]]被觸發時,實際的行爲取決於許多因素,包括對象中是否已經存在這個屬性(這是最重要的因素)。
在ES5中可使用getter和setter部分改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個對象上。getter是一個隱藏函數,會在獲取屬性值時調用。setter也是一個隱藏函數,會在設置屬性值時調用。
var myObject = {
// 給a定義一個getter
get a() {
return 2;
}
}
Object.defineProperty(myObject,'b',{
// 給b設置一個getter
get: function() { return this.a *2 },
// 確保b會出如今對象的屬性列表中
enumerable: true
})
myObject.a; // 2
myObject.b; // 4
複製代碼
無論是對象文字語法中的get a() {...},仍是defineProperty(..)中的顯式定義,兩者都會在對象中建立一個不包含值的屬性,對於這個屬性的訪問會自動調用一個隱藏函數,它的返回值會被看成屬性訪問的返回值:
var myObject = {
// 給a定義一個getter
get a() {
return 2;
}
}
myObject.a = 3;
myObject.a; //2
複製代碼
一般來講,getter和setter是成對出現的(只定義一個的話一般會產生意料以外的行爲):
var myObject = {
// 給a定義一個getter
get a() {
return this._a_;
},
set a(val) {
this._a_ = val * 2
}
}
myObject.a = 2;
myObject.a; // 4
複製代碼
前面咱們說過,如myObject.a的屬性訪問返回值多是undefined,可是這個值有多是屬性中存儲的undefined,也多是由於屬性不存在因此返回undefined。那麼如何區分這兩種狀況呢?
var myObject = {
a: 2
}
console.log('a' in myObject); // true
console.log('b' in myObject); // false
myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
複製代碼
in操做符會檢查屬性名是否在對象及其[[Prototype]]原型鏈中。相比之下,hasOwnProperty(..)只會檢查屬性是否在myObject對象中,不會檢查[[Prototype]鏈。
全部的普通對象均可以經過Object.prototype的委託來訪問hasOwnPeoperty(..),可是有的對象可能沒有鏈接到Object.prototype,(經過Object.create(null)來建立)。在這種狀況下,形如myObject.hasOwnProperty(..)就會失敗。
這時可使用一種更增強硬的方法來進行判斷:Object.prototype.hasOwnProperty.call(myObject,'a'),它借用基礎的hasOwnProperty(..)方法並把它顯式綁定到myObject上。
var myObject = {};
Object.defineProperty(myObject,'a',{
value: 2,
// 讓a像普通屬性同樣能夠枚舉
enumerable: true
})
Object.defineProperty(myObject,'b', {
value: 3,
// 讓b不可枚舉
enumerable: false
})
myObject.b; // 3
('b' in myObject); // true
myObject.hasOwnProperty('b'); // true
for(var k in myObject) {
console.log(k,myObject[k])
}
// 'a' 2
複製代碼
能夠看到,myObject.b確實存在而且有訪問值,可是卻不會出如今for..in循環中(儘管能夠經過in操做符來判斷是否存在)。緣由是「可枚舉」就至關於「能夠出如今對象屬性的遍歷中」。
也能夠經過另外一種方式來區分屬性是否可枚舉:
var myObject = { };
Object.defineProperty(myObject,'a',{
enumerable: true,
value: 2
})
Object.defineProperty(myObject,'b',{
enumerable: false,
value: 3
})
myObject.propertyIsEnumerable('a'); // true
myObject.propertyIsEnumerable('b'); // false
Object.keys(myObject); // ['a']
Object.getOwnPropertyNames(myObject); // ['a','b']
複製代碼
propertyIsEnumerable(..)會檢查給定的屬性名是否直接存在於對象中(而不是原型鏈上)而且知足enumerable:true。
Object.keys(..)會返回一個數組,包含全部可枚舉屬性,Object.getOwnPropertyNames(..)會返回一個數組,包含全部屬性,不管它們是否可枚舉。
in和hasOwnProperty(..)的區別在因而否查找[[Prototype]]鏈,然而,Object.keys(..)和Object.getOwnPropertyNames(..)都只會查找對象直接包含的屬性。
for..in循環能夠用來遍歷對象的可枚舉屬性列表(包括[[Prototype]]鏈)。可是如何遍歷屬性的值呢?
對於數值索引的數組來講,可使用標準的for循環來遍歷值:
var myArray = [1,2,3];
for(var i = 0; i < myArray.length; i++){
console.log(myArray[i]);
}
// 1 2 3
複製代碼
這實際上並非在遍歷值,而是遍歷下標來指向值,如myArray[i]。
ES5中增長了一些數組的輔助迭代器,包括forEach(..)、every(..)和some(..)。每種輔助迭代器均可以接受一個回調函數並把它應用到數組的每一個元素上,惟一的區別就是它們對於回調函數返回值的處理方式不一樣。
那麼如何直接遍歷值而不是數組下標(或者對象屬性)呢?幸虧,ES6增長了一種用來遍歷數組的for..of循環語法(若是對象自己定義了迭代器的話也能夠遍歷對象):
var myArray = [1,2,3];
for(var v of myArray) {
console.log(v);
}
// 1 2 3
複製代碼
for..of循環首先會向被訪問對象請求一個迭代器對象,而後經過調用迭代器對象的next()方法來遍歷全部返回值。
JavaScript中的對象有字面形式(好比var a = {..})和構造形式(好比var a = new Array(..))。字面形式更經常使用,不過有時候構造形式能夠提供更多選項。
許多人都覺得「JavaScript中萬物都是對象」,這是錯誤的。對象是6個(或者是7個,取決於你的觀點)基礎類型之一。對象有包括function在內的子類型,不一樣子類型具備不一樣的行爲,好比內部標籤[Object Array]表示這是對象的子類型數組。
對象就是鍵值對的集合。能夠經過.propName或者['propName']語法來獲取屬性值。訪問屬性時,引擎實際上會調用內部的默認[[Get]]操做(在設置屬性值時是[[Put]]),[[Get]]操做會檢查對象自己是否包含這個屬性,若是沒找到的話,還會查找[[Prototype]]鏈。
屬性的特性能夠經過屬性描述符來控制,好比writable和configurable。此外,可使用Object.preventExtensions(..)、Object.seal(..)和Object.freeze(..)來設置對象(及其屬性)的不可變性級別。
屬性不必定包含值--它們多是具有getter/setter的「訪問描述符」。此外,屬性能夠是枚舉或者不可枚舉的,這決定了它們是否會出如今for..in循環中。
你可使用ES6的for..of語法來遍歷數據結構(數組、對象,等等)中的值,for..of會尋找內置或者自定義的@@iterator對象並調用它的next()方法來遍歷數據值。
感受很久沒有更新了,一直斷斷續續的,也是由於前段時間工做有點忙,沒辦法平衡工做和學習的時間,如今終於能夠習慣一些,因此開始堅持把這一個系列的書好好看完。