原文:You-Dont-Know-JSjavascript
對象能夠經過兩種形式定義:聲明(文字)形式和構造形式java
聲明(文字)形式:git
var myObj = {
key: value
// ...
};
複製代碼
構造形式:github
var myObj = new Object();
myObj.key = value;
複製代碼
這裏書上說 JavaScript 有六種主要類型,ES6 引入了一種新的原始數據類型Symbol
,表示獨一無二的值。它是 JavaScript 語言的第七種數據類型,前六種是:undefined
、null
、布爾值(Boolean)、字符串(String)、數值(Number)、對象(Object)。數組
關於 js 的類型 https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive數據結構
null
自己是基本類型:null 有時會被看成一種對象類型,可是這其實只是語言自己的一個 bug,即對 null 執行 typeof null 時會返回字符串 "object"學習
原理是這樣的,不一樣的對象在底層都表示爲二進制,在 JavaScript 中二進制前三位都爲 0 的話會被判 斷爲 object 類型,null 的二進制表示是全 0,天然前三位也是 0,因此執行 typeof 時會返回「object」。測試
JavaScript 中還有一些對象子類型,一般被稱爲內置對象
在 JavaScript 中,它們實際上只是一些內置函數。這些內置函數能夠看成構造函數 (由 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 對象。
Object.prototype.toString…
的用法有個小技巧:https://gist.github.com/Yunkou/67d5da9d05b922479d771d8bcde3308d 判斷 js 類型核心的代碼:
Object.prototype.toString.call(obj).slice(8, -1); 複製代碼
基本類型值 "I am a string"
不是一個對象,它是一個不可變的基本字面值。爲了對它進行操做,好比檢查它的長度,訪問它的各個獨立字符內容等等,都須要一個 String
對象。
幸運的是,在必要的時候語言會自動地將 "string"
基本類型強制轉換爲 String
對象類型,這意味着你幾乎從不須要明確地建立對象。JS 社區的絕大部分人都 強烈推薦 儘量地使用字面形式的值,而非使用構造的對象形式。
考慮下面的代碼:
var strPrimitive = "I am a string";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
複製代碼
在這兩個例子中,咱們在字符串的基本類型上調用屬性和方法,引擎會自動地將它強制轉換爲 String
對象,因此這些屬性/方法的訪問能夠工做。
null
和 undefined
沒有對象包裝的形式,僅有它們的基本類型值。相比之下,Date
的值 僅能夠 由它們的構造對象形式建立,由於它們沒有對應的字面形式。
咱們須要使用 . 或 [ ] 操做符。
兩種語法的主要區別在於,.
操做符後面須要一個 標識符(Identifier)
兼容的屬性名,而 [".."]
語法基本能夠接收任何兼容 UTF-8/unicode 的字符串做爲屬性名。舉個例子,爲了引用一個名爲「Super-Fun!」的屬性,你不得不使用 ["Super-Fun!"]
語法訪問,由於 Super-Fun!
不是一個合法的 Identifier
屬性名。 [".."]
語法能夠傳變量。
在對象中,屬性名 老是 字符串。若是你使用 string
之外的(基本)類型值,它會首先被轉換爲字符串。這甚至包括在數組中經常使用於索引的數字,因此要當心不要將對象和數組使用的數字搞混了。
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
複製代碼
若是你試圖在一個數組上添加屬性,可是屬性名 看起來 像一個數字,那麼最終它會成爲一個數字索引(也就是改變了數組的內容):
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
複製代碼
在 ES5 以前,JavaScript 語言沒有給出直接的方法,讓你的代碼能夠考察或描述屬性性質間的區別,好比屬性是否爲只讀。
在 ES5 中,全部的屬性都用 屬性描述符(Property Descriptors) 來描述。
考慮這段代碼:
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
複製代碼
正如你所見,咱們普通的對象屬性 a
的屬性描述符(稱爲「數據描述符」,由於它僅持有一個數據值)的內容要比 value
爲 2
多得多。它還包含另外三個性質:
writable
enumerable
configurable
當咱們建立一個普通屬性時,能夠看到屬性描述符的各類性質的默認值,同時咱們能夠用 Object.defineProperty(..)
來添加新屬性,或使用指望的性質來修改既存的屬性(若是它是 configurable
的!)。
writable
控制着你改變屬性值的能力。
考慮這段代碼:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
});
myObject.a = 3;
myObject.a; // 2
複製代碼
如你所見,咱們對 value
的修改悄無聲息地失敗了。若是咱們在 strict mode
下進行嘗試,會獲得一個錯誤:
"use strict";
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
});
myObject.a = 3; // TypeError
複製代碼
這個 TypeError
告訴咱們,咱們不能改變一個不可寫屬性。
注意: 咱們一下子就會討論 getters/setters,可是簡單地說,你能夠觀察到 writable:false
意味着值不可改變,和你定義一個空的 setter 是有些等價的。實際上,你的空 setter 在被調用時須要扔出一個 TypeError
,來和 writable:false
保持一致。
只要屬性當前是可配置的,咱們就可使用相同的 defineProperty(..)
工具,修改它的描述符定義。
var myObject = {
a: 2
};
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,這與 strict mode
無關,若是你試圖改變一個不可配置屬性的描述符定義,就會發生 TypeError。要當心:如你所看到的,將 configurable
設置爲 false
是 一個單向操做,不可撤銷!
注意: 這裏有一個須要注意的微小例外:即使屬性已是 configurable:false
,writable
老是能夠沒有錯誤地從 true
改變爲 false
,但若是已是 false
的話不能變回 true
。
configurable:false
阻止的另一個事情是使用 delete
操做符移除既存屬性的能力。
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
複製代碼
如你所見,最後的 delete
調用(無聲地)失敗了,由於咱們將 a
屬性設置成了不可配置。
delete
僅用於直接從目標對象移除該對象的(能夠被移除的)屬性。若是一個對象的屬性是某個其餘對象/函數的最後一個現存的引用,而你 delete
了它,那麼這就移除了這個引用,因而如今那個沒有被任何地方所引用的對象/函數就能夠被做爲垃圾回收。可是,將 delete
當作一個像其餘語言(如 C/C++)中那樣的釋放內存工具是 不 恰當的。delete
僅僅是一個對象屬性移除操做 —— 沒有更多別的含義。
咱們將要在這裏提到的最後一個描述符性質是 enumerable
(還有另外兩個,咱們將在一下子討論 getter/setters 時談到)。
它的名稱可能已經使它的功能很明顯了,這個性質控制着一個屬性是否能在特定的對象-屬性枚舉操做中出現,好比 for..in
循環。設置爲 false
將會阻止它出如今這樣的枚舉中,即便它依然徹底是能夠訪問的。設置爲 true
會使它出現。
全部普通的用戶定義屬性都默認是可 enumerable
的,正如你一般但願的那樣。但若是你有一個特殊的屬性,你想讓它對枚舉隱藏,就將它設置爲 enumerable:false
。
咱們能夠查詢一個對象是否擁有特定的屬性,而 沒必要 取得那個屬性的值:
var myObject = {
a: 2
};
"a" in myObject; // true
"b" in myObject; // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
複製代碼
in
操做符會檢查屬性是否存在於對象 中,或者是否存在於 [[Prototype]]
鏈對象遍歷的更高層中(詳見第五章)。相比之下,hasOwnProperty(..)
僅僅 檢查 myObject
是否擁有屬性,但 不會 查詢 [[Prototype]]
鏈。咱們會在第五章詳細講解 [[Prototype]]
時,回來討論這個兩個操做重要的不一樣。
經過委託到 Object.prototype
,全部的普通對象均可以訪問 hasOwnProperty(..)
(詳見第五章)。可是建立一個不連接到 Object.prototype
的對象也是可能的(經過 Object.create(null)
—— 詳見第五章)。這種狀況下,像 myObject.hasOwnProperty(..)
這樣的方法調用將會失敗。
在這種場景下,一個進行這種檢查的更健壯的方式是 Object.prototype.hasOwnProperty.call(myObject,"a")
,它借用基本的 hasOwnProperty(..)
方法並且使用 明確的 this 綁定(詳見第二章)來對咱們的 myObject
實施這個方法。
注意: in
操做符看起來像是要檢查一個值在容器中的存在性,可是它實際上檢查的是屬性名的存在性。在使用數組時注意這個區別十分重要,由於咱們會有很強的衝動來進行 4 in [2, 4, 6]
這樣的檢查,可是這老是不像咱們想象的那樣工做。
先前,在學習 enumerable
屬性描述符性質時,咱們簡單地解釋了"可枚舉性(enumerability)"的含義。如今,讓咱們來更加詳細地從新講解它。
var myObject = {};
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚舉,如通常狀況
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚舉
{ enumerable: false, value: 3 }
);
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
操做符的存在性檢查經過了)。這是由於 「enumerable」 基本上意味着「若是對象的屬性被迭代時會被包含在內」。
注意: 將 for..in
循環實施在數組上可能會給出意外的結果,由於枚舉一個數組將不只包含全部的數字下標,還包含全部的可枚舉屬性。因此一個好主意是:將 for..in
循環 僅 用於對象,而爲存儲在數組中的值使用傳統的 for
循環並用數字索引迭代。
另外一個能夠區分可枚舉和不可枚舉屬性的方法是:
var myObject = {};
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚舉,如通常狀況
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `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(..)
都 只 考察直接給定的對象。
(當下)沒有與 in
操做符的查詢方式(在整個 [[Prototype]]
鏈上遍歷全部的屬性,如咱們在第五章解釋的)等價的、內建的方法能夠獲得一個 全部屬性 的列表。你能夠近似地模擬一個這樣的工具:遞歸地遍歷一個對象的 [[Prototype]]
鏈,在每一層都從 Object.keys(..)
中取得一個列表——僅包含可枚舉屬性。
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(..)
。這些幫助方法的每個都接收一個回調函數,這個函數將施用於數組中的每個元素,僅在如何響應回調的返回值上有所不一樣。
forEach(..)
將會迭代數組中全部的值,而且忽略回調的返回值。every(..)
會一直迭代到最後,或者 當回調返回一個 false
(或「falsy」)值,而 some(..)
會一直迭代到最後,或者 當回調返回一個 true
(或「truthy」)值。
這些在 every(..)
和 some(..)
內部的特殊返回值有些像普通 for
循環中的 break
語句,它們能夠在迭代執行到末尾以前將它結束掉。
若是你使用 for..in
循環在一個對象上進行迭代,你也只能間接地獲得值,由於它實際上僅僅迭代對象的全部可枚舉屬性,讓你本身手動地去訪問屬性來獲得值。
注意: 與以有序數字的方式(for
循環或其餘迭代器)迭代數組的下標比較起來,迭代對象屬性的順序是 不肯定 的,並且可能會因 JS 引擎的不一樣而不一樣。對於須要跨平臺環境保持一致的問題,不要依賴 觀察到的順序,由於這個順序是不可靠的。
可是若是你想直接迭代值,而不是數組下標(或對象屬性)呢?ES6 加入了一個有用的 for..of
循環語法,用來迭代數組(和對象,若是這個對象有定義的迭代器):
var myArray = [1, 2, 3];
for (var v of myArray) {
console.log(v);
}
// 1
// 2
// 3
複製代碼
for..of
循環要求被迭代的 東西 提供一個迭代器對象(從一個在語言規範中叫作 @@iterator
的默認內部函數那裏獲得),每次循環都調用一次這個迭代器對象的 next()
方法,循環迭代的內容就是這些連續的返回值。
數組擁有內建的 @@iterator
,因此正如展現的那樣,for..of
對於它們很容易使用。可是讓咱們使用內建的 @@iterator
來手動迭代一個數組,來看看它是怎麼工做的:
var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
複製代碼
注意: 咱們使用一個 ES6 的 Symbol
:Symbol.iterator
來取得一個對象的 @@iterator
內部屬性。咱們在本章中簡單地提到過 Symbol
的語義(見「計算型屬性名」),一樣的原理也適用於這裏。你老是但願經過 Symbol
名稱,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。另外,儘管這個名稱有這樣的暗示,但 @@iterator
自己 不是迭代器對象, 而是一個返回迭代器對象的 方法 —— 一個重要的細節!
正如上面的代碼段揭示的,迭代器的 next()
調用的返回值是一個 { value: .. , done: .. }
形式的對象,其中 value
是當前迭代的值,而 done
是一個 boolean
,表示是否還有更多內容能夠迭代。
注意值 3
和 done:false
一塊兒返回,猛地一看會有些奇怪。你不得不第四次調用 next()
(在前一個代碼段的 for..of
循環會自動這樣作)來獲得 done:true
,以使本身知道迭代已經完成。這個怪異之處的緣由超出了咱們要在這裏討論的範圍,可是它源自於 ES6 生成器(generator)函數的語義。
雖然數組能夠在 for..of
循環中自動迭代,但普通的對象 沒有內建的 @@iterator。這種故意省略的緣由要比咱們將在這裏解釋的更復雜,但通常來講,爲了將來的對象類型,最好不要加入那些可能最終被證實是麻煩的實現。
JS 中的對象擁有字面形式(好比 var a = { .. }
)和構造形式(好比 var a = new Array(..)
)。字面形式幾乎老是首選,但在某些狀況下,構造形式提供更多的構建選項。
許多人聲稱「Javascript 中的一切都是對象」,這是不對的。對象是六種(或七中,看你從哪一個方面說)基本類型之一。對象有子類型,包括 function
,還能夠被行爲特化,好比 [object Array]
做爲內部的標籤表示子類型數組。
對象是鍵/值對的集合。經過 .propName
或 ["propName"]
語法,值能夠做爲屬性訪問。無論屬性何時被訪問,引擎實際上會調用內部默認的 [[Get]]
操做(在設置值時調用 [[Put]]
操做),它不只直接在對象上查找屬性,在沒有找到時還會遍歷 [[Prototype]]
鏈(見第五章)。
屬性有一些能夠經過屬性描述符控制的特定性質,好比 writable
和 configurable
。另外,對象擁有它的不可變性(它們的屬性也有),能夠經過使用 Object.preventExtensions(..)
、Object.seal(..)
、和 Object.freeze(..)
來控制幾種不一樣等級的不可變性。
屬性沒必要非要包含值 —— 它們也能夠是帶有 getter/setter 的「訪問器屬性」。它們也能夠是可枚舉或不可枚舉的,這控制它們是否會在 for..in
這樣的循環迭代中出現。
你也可使用 ES6 的 for..of
語法,在數據結構(數組,對象等)中迭代 值,它尋找一個內建或自定義的 @@iterator
對象,這個對象由一個 next()
方法組成,經過這個 next()
方法每次迭代一個數據。