【第766期】你不懂JS:對象

圖片

前言前端

經過最近兩章的閱讀,你們會以爲文章太長嗎?看這種一篇文中,你大概會花多少時間呢?歡迎給早讀君留言。今天繼續由前端早讀課專欄做者@HetfieldJoe帶來連載《你不懂JS》的分享。ps:本文略長,耐點心看看正則表達式


正文從這開始~算法


你不懂JS:this與對象原型 第三章:對象設計模式


【第764期】你不懂JS:this是什麼?【第765期】你不懂JS:this豁然開朗!中,咱們講解了this綁定如何根據函數調用的調用點指向不一樣的對象。但究竟什麼是對象,爲何咱們須要指向它們?這一章咱們就來詳細探索一下對象。數組


語法安全

對象來自於兩種形式:聲明(字面)形式,和構造形式。數據結構


一個對象的字面語法看起來像這樣:框架

圖片


構造形式看起來像這樣:ide

圖片


構造形式和字面形式的結果是徹底同種類的對象。惟一真正的區別在於你能夠向字面聲明一次性添加一個或多個鍵/值對,而對於構造形式,你必須一個一個地添加屬性。函數


注意: 像剛纔展現的那樣使用「構造形式」來建立對象是極其少見的。你頗有可能老是想使用字面語法形式。對大多數內建的對象也同樣(後述)。


類型

對象是大多數JS工程依賴的基本構建塊兒。它們是JS的6中主要類型(在語言規範中稱爲「語言類型」)中的一種。

  • string

  • number

  • boolean

  • null

  • undefined

  • object


注意 簡單基本類型 (string,number,boolean,null,和undefined)自身 不是 object。null有時會被當成一個對象類型,可是這種誤解源自與一個語言中的Bug,它使得typeof null錯誤地(使人困惑地)返回字符串"object"。實際上,null是它本身的基本類型


一個常見的錯誤論斷是「JavaScript中的一切都是對象」。這明顯是不對的。


對比來看,存在幾種特殊的對象子類型,咱們能夠稱之爲 複雜基本類型。


function是對象的一種子類型(技術上講,叫作「可調用對象」)。函數在JS中被稱爲「頭等(first class)」類型,就由於它們基本上就是普通的對象(附帶有可調用的行爲語義),並且它們能夠像其餘普通的對象那樣被處理。


數組也是一種形式的對象,帶有特別的行爲。數組在內容的組織上要稍稍比通常的對象更加結構化。


內建對象

有幾種其餘的對象子類型,一般稱爲內建對象。對於其中的一些來講,它們的名稱看起來暗示着它們和它們對應的基本類型有着直接的聯繫,但事實上,它們的關係更復雜,咱們一下子就開始探索。

  • String

  • Number

  • Boolean

  • Object

  • Function

  • Array

  • Date

  • RegExp

  • Error


若是你依照和其餘語言的類似性來看的話,好比Java語言的String類,這些內建類型有着實際類型的外觀,甚至是類(class)的外觀,


可是在JS中,它們實際上僅僅是內建的函數。這些內建函數的每個均可以被用做構造器(也就是,一個函數能夠和new操做符一塊兒調用——參照第二章),其結果是一個新 構建 的相應子類型的對象。好比:

圖片


咱們會在本章稍後詳細地看到Object.prototype.toString...究竟是如何工做的,但簡單地說,咱們能夠經過借用基本的默認toString()方法來考察子類型的內部,並且你能夠看到它揭示了strObject其實是由String構造器建立的對象。


基本類型值"I am a string"不是一個對象,它是一個不可變的基本字面值。爲了對它進行操做,好比檢查它的長度,訪問它的各個獨立字符內容等等,都須要一個String對象。


幸運的是,在必要的時候語言會自動地將"string"基本類型轉換爲String對象類型,這意味着你幾乎從不須要明確地建立對象。主流的JS社區都 強烈推薦 儘量地使用字面形式的值,而非使用構造的對象形式。


考慮下面的代碼:

image.png


在這兩個例子中,咱們在字符串的基本類型上調用屬性和方法,引擎會自動地將它轉換爲String對象,因此這些屬性/方法的訪問能夠工做。


當使用如42.359.toFixed(2)這樣的方法時,一樣的轉換也發生在數字基本字面量42和包裝對象new Nubmer(42)之間。一樣的還有Boolean對象和"boolean"基本類型。


null和undefined沒有對象包裝的形式,僅有它們的基本類型值。相比之下,Date的值 僅能夠 由它們的構造對象形式建立,由於它們沒有對應的字面形式。


不管使用字面仍是構造形式,Object,Array,Function,和RegExp(正則表達式)都是對象。在某些狀況下,構造形式確實會比對應的字面形式提供更多的建立選項。由於對象能夠被任意一種方式建立,更簡單的字面形式幾乎是全部人的首選。僅僅在你須要使用額外的選項時使用構建形式。


Error對象不多在代碼中明示地被建立,它們一般在拋出異常時自動地被建立。它們能夠由new Error(..)構造形式建立,但一般是沒必要要的。


內容

正如剛纔提到的,對象的內容由存儲在特定命名的 位置 上的(任意類型的)值組成,咱們稱這些值爲屬性。


有一個重要的事情須要注意:當咱們說「內容」時,彷佛暗示這這些值 實際上 存儲在對象內部,但那隻不過是表面現象。引擎會根據本身的實現來存儲這些值,並且一般都不是把它們存儲在容器對象 內部。在容器內存儲的是這些屬性的名稱,它們像指針(技術上講,叫 引用(reference))同樣指向值存儲的地方。


考慮下面的代碼:

image.png


爲了訪問在myObject在 位置 a的值,咱們須要使用.或[ ]操做符。.a語法一般稱爲「屬性(property)」訪問,而["a"]語法一般稱爲「鍵(key)」訪問。在現實中,它們倆都訪問相同的 位置,並且會拿出相同的值,2,因此這些術語能夠互換使用。從如今起,咱們將使用最多見的術語——「屬性訪問」。


兩種語法的主要區別在於,.操做符後面須要一個標識符(Identifier)兼容的屬性名,而[".."]語法基本能夠接收任何兼容UTF-8/unicode的字符串做爲屬性名。舉個例子,爲了引用一個名爲「Super-Fun!」的屬性,你不得不使用["Super-Fun!"]語法訪問,由於Super-Fun!不是一個合法的Identifier屬性名。


並且,因爲[".."]語法使用字符串的 值 來指定位置,這意味着程序能夠動態地組建字符串的值。好比:

image.png


在對象中,屬性名 老是 字符串。若是你使用字符串之外(基本)類型的值,它會首先被轉換爲字符串。這甚至包括在數組中經常使用於索引的數字,因此要當心不要將對象和數組使用的數字搞混了。

image.png


計算型屬性名

若是你須要將一個計算表達式 做爲 一個鍵名稱,那麼咱們剛剛描述的myObject[..]屬性訪問語法是十分有用的,好比myObject[prefix + name]。可是當使用字面對象語法聲明對象時則沒有什麼幫助。


ES6加入了 計算型屬性名,在一個字面對象聲明的鍵名稱位置,你能夠指定一個表達式,用[ ]括起來:

image.png


計算型屬性名 的最多見用法,多是用於ES6的Symbol,咱們將不會在本書中涵蓋關於它的細節。簡單地說,它們是新的基本數據類型,擁有一個不透明不可知的值(技術上講是一個string值)。你將會被強烈地不鼓勵使用一個Symbol的 實際值 (這個值理論上會因JS引擎的不一樣而不一樣),因此Symbol的名稱,好比Symbol.Something(這是個瞎編的名稱!),纔是你會使用的:

image.png


屬性(Property) vs. 方法(Method)

有些開發者喜歡在討論對一個對象的屬性訪問時作一個區別,若是這個被訪問的值剛好是一個函數的話。由於這誘令人們認爲函數 屬於 這個對象,並且在其餘語言中,屬於對象(也就是「類」)的函數被稱做「方法」,因此相對於「屬性訪問」,咱們常能聽到「方法訪問」。


有趣的是,語言規範也作出了一樣的區別。


從技術上講,函數毫不會「屬於」對象,因此,說一個對象的引用上恰好被訪問的函數自動是一個「方法」,看起來有些像是延伸了語義。


有些函數確實擁有this引用,並且 有時 這些this引用指向調用點的對象引用。但這個用法真的沒有使這個函數比其餘函數更像「方法」,由於this是在運行時在調用點動態綁定的,這使得它與這個對象的關係至可能是間接的。


每次你訪問一個對象的屬性都是一個 屬性訪問,不管你獲得什麼類型的值。若是你 剛好 從屬性訪問中獲得一個函數,它也沒有魔法般地在那時成爲一個「方法」。一個從屬性訪問得來的函數沒有任何特殊性(隱式this綁定以外的可能性在剛纔已經解釋過了)。


舉個例子:

image.png


someFoo和myObject.someFoo只不過是同一個函數的兩個分離的引用,它們中的任何一個都不意味着這個函數很特別或被其餘對象所「擁有」。若是上面的foo()定義裏面擁有一個this引用,那麼myObject.someFoo的 隱式綁定 將會是這個兩個引用間 惟一 能夠觀察到的不一樣。它們中的任何一個都沒有稱爲「方法」的道理。


也許有人會爭辯,函數 變成了方法,不是在定義期間,而是在調用的執行期間,根據它是如何在調用點被調用的(是否帶有一個環境對象引用 —— 細節見第二章)。甚至這種解讀也有些牽強。


可能最安全的結論是,在JavaScript中,「函數」和「方法」是能夠互換使用的。


注意: ES6加入了super引用,它一般是和class(見附錄A)一塊兒使用的。super的行爲方式(靜態綁定,而非動態綁定),給了這種說法更多的權重:一個super綁定到某處的函數比起「函數」更像一個「方法」。可是一樣地,這僅僅是微妙的語義上的(和機制上的)細微區別。


就算你聲明一個函數表達式做爲字面對象的一部分,那個函數都不會魔法般地 屬於 這個對象——仍然僅僅是同一個函數對象的多個引用罷了。

image.png


注意: 在第六章中,咱們會爲字面對象的foo: function foo(){ .. }聲明語法介紹一種ES6的簡化語法。


數組

數組也使用[ ]訪問形式,但正如上面提到的,在存儲值的方式和位置上它們的組織更加結構化(雖然仍然在存儲值的類型上沒有限制)。數組採用 數字索引,這意味着值被存儲的位置,一般稱爲 下標,是一個非負整數,好比0和42。

image.png


數組也是對象,因此即使每一個索引都是正整數,你還能夠在數組上添加屬性:

image.png


注意,添加命名屬性(不管是使用.仍是[ ]操做符語法)不會改變數組的length所報告的值。


你 能夠 把一個數組當作普通的鍵/值對象使用,而且從不添加任何數字下標,但這不是好主意,由於數組對它原本的用途有特定的行爲和優化,正如普通對象那樣。使用對象來存儲鍵/值對,而用數組在數字下標上存儲值。


當心: 若是你試圖在一個數組上添加屬性,可是屬性名 看起來 像一個數字,那麼最終它會成爲一個數字索引(也就是改變了數組的內容):

image.png


複製對象

當開發者們初次拿起Javascript語言時,最常須要的特性就是如何複製一個對象。看起來應該有一個內建的copy()方法,對吧?可是事情實際上比這複雜一些,由於在默認狀況下,複製的算法應當是什麼,並不明確。


好比,考慮這個對象:

image.png


一個myObject的 拷貝 究竟應該怎麼表現?


首先,咱們應該回答它是一個 淺(shallow) 仍是一個 深(deep) 拷貝?一個 淺拷貝(shallow copy) 會獲得一個新對象,它的a是值2的拷貝,但b,c和d屬性僅僅是引用,它們指向被拷貝對象中引用的相同位置。一個 深拷貝(deep copy) 將不只複製myObject,還會複製anotherObject和anotherArray。但以後咱們讓anotherArray擁有anotherObject和myObject的引用,因此 那些 也應當被複制而不是僅保留引用。如今因爲循環引用,咱們獲得了一個無限循環複製的問題。


咱們應當檢測循環引用並打破循環遍歷嗎(無論位於深處的,沒有徹底複製的元素)?咱們應當報錯退出嗎?或者介於二者之間?


另外,「複製」一個函數意味着什麼,也不是很清楚。有一些技巧,好比提取一個函數源代碼的toString()序列化表達(這個源代碼會因實現不一樣而不一樣,並且根據被考察的函數的類型,其結果甚至在全部引擎上都不可靠)。


那麼咱們如何解決全部這些刁鑽的問題?不一樣的JS框架都各自挑選本身的解釋而且作出本身的選擇。可是哪種(若是有的話)纔是JS應看成爲標準採用的呢?長久以來,沒有明確答案。


一個解決方案是,JSON安全的對象(也就是,能夠被序列化爲一個JSON字符串,以後還能夠被從新變換爲擁有相同的結構和值的對象)能夠簡單地這樣 複製:

image.png


固然,這要求你保證你的對象是JSON安全的。對於某些狀況,這沒什麼大不了的。而對另外一些狀況,這還不夠。


同時,淺拷貝至關易懂,並且沒有那麼多問題,因此ES6爲此任務已經定義了Object.assign(..)。Object.assign(..)接收 目標 對象做爲第一個參數,而後是一個或多個 源 對象做爲後續參數。它會在 源 對象上迭代全部的 可枚舉(enumerable),owned keys(直接擁有的鍵),並把它們拷貝到 目標 對象上(僅經過=賦值)。它還會很方便地返回 目標 對象,正以下面你能夠看到的:

image.png


注意: 在下一部分中,咱們將討論「屬性描述符(property descriptors)」並展現Object.defineProperty(..)的使用。然而在Object.assign(..)中發生的複製是單純的=式賦值,因此任何在源對象屬性的特殊性質(好比writable)在目標對象上 都不會保留 。


屬性描述符(Property Descriptors)

在ES5以前,JavaScript語言沒有給出直接的方法,讓你的代碼能夠考察或描述屬性的性質間的區別,好比屬性是否爲只讀。


在ES5中,全部的屬性都用 屬性描述符(Property Descriptors) 來描述。


考慮這段代碼:

image.png


正如你所見,咱們普通的對象屬性a的屬性描述符(稱爲「數據描述符」,由於它僅持有一個數據值)的內容要比value爲2多得多。它還包含另外3個性質:writable,enumerable,和configurable。


當咱們建立一個普通屬性時,能夠看到屬性描述符的各類性質的默認值,咱們能夠用Object.defineProperty(..)來添加新屬性,或使用指望的性質來修改既存的屬性(若是它是configurable的!)。


舉例來講:

image.png


使用defineProperty(..),咱們手動明確地在myObject上添加了一個直白的,普通的a屬性。然而,你一般不會使用這種手動方法,除非你想要把描述符的某個性質修改成不一樣的值。


可寫性(Writable)

writable控制着你改變屬性值的能力。


考慮這段代碼:

image.png

如你所見,咱們對value的修改悄無聲息地失敗了。若是咱們在strict mode下進行嘗試,會獲得一個錯誤:

image.png


這個TypeError告訴咱們,咱們不能改變一個不可寫屬性。


注意: 咱們一下子就會討論getters/setters,可是簡單地說,你能夠觀察到writable:false意味着值不可改變,和你定義一個空的setter是有些等價的。實際上,你的空setter在被調用時須要扔出一個TypeError,來和writable:false保持一致。


可配置性(Configurable)

只要屬性當前是可配置的,咱們就可使用一樣的defineProperty(..)工具,修改它的描述符定義。

image.png


最後的defineProperty(..)調用致使了一個TypeError,這與strict mode無關,若是你試圖改變一個不可配置屬性的描述符定義,就會發生TypeError。要當心:如你所看到的,將configurable設置爲false是 一個單向操做,不可撤銷!


注意: 這裏有一個須要注意的微小例外:即使屬性已是configurable:false,writable老是能夠沒有錯誤地從true改變爲false,但若是已是false的話不能變回true。


configurable:false阻止的另一個事情是使用delete操做符移除既存屬性的能力。

image.png


如你所見,最後的delete調用失敗了(無聲地),由於咱們將a屬性設置成了不可配置。


delete僅用於直接從目標對象移除該對象的屬性(能夠被移除的屬性)。若是一個對象的屬性是某個其餘對象/函數的最後一個現存的引用,而你delete了它,那麼這就移除了這個引用,因而如今那個沒有被任何地方引用的對象/函數就能夠被做爲垃圾回收。可是,將delete當作一個像其餘語言(如C/C++)中那樣的釋放內存工具是不正確的。delete僅僅是一個對象屬性移除操做——沒有更多別的含義。


可枚舉性(Enumerable)

咱們將要在這裏提到的最後一個描述符性質是enumerable(還有另外兩個,咱們將在一下子討論getter/setters時談到)。


它的名稱可能已經使它的功能很明顯了,這個性質控制着一個屬性是否能在特定的對象屬性枚舉操做中出現,好比for..in循環。設置爲false將會阻止它出如今這樣的枚舉中,即便它依然徹底是能夠訪問的。設置爲true會使它出現。


全部普通的用戶定義屬性都默認是可enumerable的,正如你一般但願的那樣。但若是你有一個特殊的屬性,你想讓它對枚舉隱藏,就將它設置爲enumerable:false。


咱們一下子就更加詳細地演示可枚舉性,因此在大腦中給這個話題上打一個書籤。


不可變性(Immutability)

有時咱們但願將屬性或對象(有意或無心地)設置爲不可改變的。ES5用幾種不一樣的微妙方式,加入了對此功能的支持。


一個重要的注意點是:全部 這些方法都建立的是淺不可變性。也就是,它們僅影響對象和它的直屬屬性的性質。若是對象擁有對其餘對象(數組,對象,函數等)的引用,那個對象的 內容 不會受影響,任然保持可變。

image.png


在這段代碼中,咱們假想myImmutableObject已經被建立,並且被保護爲不可變。可是,爲了保護myImmutableObject.foo的內容(也是一個對象——數組),你將須要使用下面的一個或多個方法將foo設置爲不可變。


注意: 在JS程序中建立徹底不可動搖的對象是不那麼常見的。有些特殊狀況固然須要,但做爲一個普通的設計模式,若是你發現本身想要 封印(seal) 或 凍結(freeze) 你全部的對象,那麼你可能想要退一步來從新考慮你的程序設計,讓它對對象值的潛在變化更加健壯。


對象常量(Object Constant)

經過將writable:false與configurable:false組合,你能夠實質上建立了一個做爲對象屬性的 常量(不能被改變,重定義或刪除),好比:

image.png


防止擴展(Prevent Extensions)

若是你想防止一個對象被添加新的屬性,但另外一方面保留其餘既存的對象屬性,調用Object.preventExtensions(..):

image.png


在非-strict mode模式下,b的建立會無聲地失敗。在strict mode下,它會拋出TypeError。


封印(Seal)

Object.seal(..)建立一個「封印」的對象,這意味着它實質上在當前的對象上調用Object.preventExtensions(..),同時也將它全部的既存屬性標記爲configurable:false。


因此,你既不能添加更多的屬性,也不能從新配置或刪除既存屬性(雖然你依然 能夠 修改它們的值)。


凍結(Freeze)

Object.freeze(..)建立一個凍結的對象,這意味着它實質上在當前的對象上調用Object.seal(..),同時也將它全部的「數據訪問」屬性設置爲writable:false,因此他們的值不可改變。


這種方法是你能夠從對象自身得到的最高級別的不可變性,由於它阻止任何對對象或對象的直屬屬性的改變(雖然,如上面提到的,任何被引用的對象的內容不受影響)。


你能夠「深度凍結」一個對象:在這個對象上調用Object.freeze(..),而後遞歸地迭代全部它引用的對象(目前尚未受過影響的),而後在它們上也調用Object.freeze(..)。可是要當心,這可能會影響其餘(共享的)你並不打算影響的對象。


[[Get]]

關於屬性訪問如何工做有一個重要的細節。


考慮下面的代碼:

image.png


myObject.a是一個屬性訪問,可是它並非看起來那樣,僅僅在myObject中尋找一個名爲a的屬性。


根據語言規範,上面的代碼實際上在myObject上執行了一個[[Get]]操做(有些像[[Get]]()函數調用)。對一個對象進行默認的內建[[Get]]操做,會 首先 檢查對象,尋找一個擁有被請求的名稱的屬性,若是找到,就返回相應的值。


然而,若是按照被請求的名稱 沒能 找到屬性,[[Get]]的算法定義了另外一個重要的行爲。咱們會在第五章來解釋 接下來 會發生什麼(遍歷[[Prototype]]鏈,若是有的話)。


但[[Get]]操做的一個重要結果是,若是它經過任何方法都不能找到被請求的屬性的值,那麼它會返回undefined。

image.png


這個行爲和你經過標識符名稱來引用 變量 不一樣。若是你引用了一個在可用的詞法做用域內沒法解析的變量,其結果不是像對象屬性那樣返回undefined,而是拋出ReferenceError。

image.png


從 值 的角度來講,這兩個引用沒有區別——它們的結果都是undefined。然而,在[[Get]]操做的底層,雖然不明顯,可是比起處理引用myObject.a,處理myObject.b的操做要多作一些潛在的工做。


若是僅僅考察結果的值,你沒法分辨一個屬性是存在並持有一個undefined值,仍是由於屬性根本 不 存在因此[[Get]]沒法返回某個特定值而返回默認的undefined。可是,你很快就能看到你其實 能夠 分辨這兩種場景。


[[Put]]

既然爲了從一個屬性中取得值而存在一個內部定義的[[Get]]操做,那麼很明顯應該也存在一個默認的[[Put]]操做。


這很容易讓人認爲,給一個對象的屬性賦值,將會在這個對象上調用[[Put]]來設置或建立這個屬性。可是實際狀況卻有一些微妙的不一樣。


調用[[Put]]時,它根據幾個因素表現不一樣的行爲,包括(影響最大的)屬性是否已經在對象中存在了。


若是屬性存在,[[Put]]算法將會大體檢查:

  • 這個屬性是訪問器描述符嗎(見下一節"Getters 與 Setters")?若是是,並且是setter,就調用setter。

  • 這個屬性是writable爲false數據描述符嗎?若是是,在非strict mode下無聲地失敗,或者在strict mode下拋出TypeError。

  • 不然,像日常同樣設置既存屬性的值。


若是屬性在當前的對象中不存在,[[Put]]操做會變得更微妙和複雜。咱們將在第五章討論[[Prototype]]時再次回到這個場景,更清楚地解釋它。


Getters 與 Setters

對象默認的[[Put]]和[[Get]]操做分別徹底控制着如何設置既存或新屬性的值,和如何取得既存屬性。


注意: 使用較先進的語言特性,覆蓋整個對象(不只是每一個屬性)的默認[[Put]]和[[Get]]操做是可能的。這超出了咱們要在這本書中討論的範圍,但咱們會在後面的「你不懂JS」系列中涵蓋此內容。


ES5引入了一個方法來覆蓋這些默認操做的一部分,但不是在對象級別而是針對每一個屬性,就是經過getters和setters。Getter是實際上調用一個隱藏函數來取得值的屬性。Setter是實際上調用一個隱藏函數來設置值的屬性。


當你將一個屬性定義爲擁有getter或setter或二者兼備,那麼它的定義就成爲了「訪問器描述符」(與「數據描述符」相對)。對於訪問器描述符,它的value和writable性質沒有意義而被忽略,取而代之的是JS將會考慮屬性的set和get性質(還有configurable和enumerable)。


考慮下面的代碼:

image.png


無論是經過在字面對象語法中使用get a() { .. },仍是經過使用defineProperty(..)明肯定義,咱們都在對象上建立了一個沒有實際持有值的屬性,訪問它們將會自動地對getter函數進行隱藏的函數調用,其返回的任何值就是屬性訪問的結果。

image.png


由於咱們僅爲a定義了一個getter,若是以後咱們試着設置a的值,賦值操做並不會拋出錯誤而是無聲地將賦值廢棄。就算這裏有一個合法的setter,咱們的自定義getter將返回值硬編碼爲僅返回2,因此賦值操做是沒有意義的。


爲了使這個場景更合理,正如你可能指望的那樣,每一個屬性還應當被定義一個覆蓋默認[[Put]]操做(也就是賦值)的setter。幾乎可肯定,你將老是想要同時聲明getter和setter(僅有它們中的一個常常會致使之外的行爲):

image.png


注意: 在這個例子中,咱們實際上將賦值操做([[Put]]操做)指定的值2存儲到了另外一個變量_a_中。_a_這個名稱只是用在這個例子中的單純的慣例,並不意味着它的行爲有什麼特別之處——它和其餘普通屬性沒有區別。


存在性(Existence)

咱們早先看到,像myObject.a這樣的屬性訪問可能會獲得一個undefined值,不管是它明確存儲着undefined仍是屬性a根本就不存在。那麼,若是這兩種狀況的值相同,咱們還怎麼區別它們呢?


咱們能夠查詢一個對象是否擁有特定的屬性,而沒必要取得那個屬性的值:

image.png


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]這樣的檢查,可是這老是不像咱們想象的那樣工做。


枚舉(Enumeration)

先前,在學習enumerable屬性描述符性質時,咱們簡單地解釋了"可枚舉性(enumerability)"的含義。如今,讓咱們來更加詳細地從新審視它。

image.png


你會注意到,myObject.b實際上 存在,並且擁有能夠訪問的值,可是它不出如今for..in循環中(然而使人詫異的是,它的in操做符的存在性檢查經過了)。這是由於「enumerable」基本上意味着「若是對象的屬性被迭代時會被包含在內」。


注意: 將for..in循環實施在數組上可能會給出意外的結果,由於枚舉一個數組將不只包含全部的數字下標,還包含全部的可枚舉屬性。因此一個好主意是:將for..in循環 僅 用於對象,而爲存儲在數組中的值使用傳統的for循環並用數字索引迭代。


意外一個能夠區分可枚舉和不可枚舉屬性的方法是:

image.png


propertyIsEnumerable(..)測試一個給定的屬性名是否直 接存 在於對象上,而且是enumerable:true。


Object.keys(..)返回一個全部可枚舉屬性的數組,而Object.getOwnPropertyNames(..)返回一個 全部 屬性的數組,不論能不能枚舉。


in和hasOwnProperty(..)區別於它們是否查詢[[Prototype]]鏈,而Object.keys(..)和Object.getOwnPropertyNames(..)都 只 考察直接給定的對象。


(當下)沒有與in操做符的查詢方式(在整個[[Prototype]]鏈上遍歷全部的屬性,如咱們在第五章解釋的)等價的,內建的方法能夠獲得一個 全部屬性 的列表。你能夠近似地模擬一個這樣的工具:遞歸地遍歷一個對象的[[Prototype]]鏈,在每一層都從Object.keys(..)中取得一個列表——僅包含可枚舉屬性。


迭代(Iteration)

for..in循環迭代一個對象上(包括它的[[Prototype]]鏈)全部的可迭代屬性。但若是你想要迭代值呢?


在數字索引的數組中,典型的迭代全部的值的辦法是使用標準的for循環,好比:

image.png


可是這並無迭代全部的值,而是迭代了全部的下標,而後由你使用索引來引用值,好比myArray[i]。


ES5還爲數組加入了幾個迭代幫助方法,包括forEach(..),every(..),和some(..)。這些幫助方法的每個都接收一個回調函數,這個函數將施用於數組中的每個元素,僅在如何響應回調的返回值上有所不一樣。


forEach(..)將會迭代數組中全部的值,而且忽略回調的返回值。every(..)會一直迭代到最後,或者 當回調返回一個false(或「falsy」)值,而some(..)會一直迭代到最後,或者 當回調返回一個true(或「truthy」)值。


這些在every(..)和some(..)內部的特殊返回值有些像普通for循環中的break語句,它們能夠在迭代執行到末尾以前將它結束掉。


若是你使用for..in循環在一個對象上進行迭代,你也只能間接地獲得值,由於它實際上僅僅迭代對象的全部可枚舉屬性,讓你本身手動地去訪問屬性來獲得值。


注意: 與以有序數字的方式(for循環或其餘迭代器)迭代數組的下標比較起來,迭代對象屬性的順序是 不肯定 的,並且可能會因JS引擎的不一樣而不一樣。對於須要跨平臺環境保持一致性的問題,不要依賴 觀察到的順序,由於這個順序是不可靠的。


可是若是你想直接迭代值,而不是數組下標(或對象屬性)呢?ES6加入了一個有用的for..of循環語法,用來迭代數組(和對象,若是這個對象有定義的迭代器):

image.png


for..of循環要求被迭代的 東西 提供一個迭代器對象(從一個在語言規範中叫作@@iterator的默認內部函數那裏獲得),每次循環都調用一次這個迭代器對象的next()方法,循環迭代的內容就是這些連續的返回值。


數組擁有內建的@@iterator,因此正如展現的那樣,for..of對於它們很容易使用。可是讓咱們使用內建的@@iterator來手動迭代一個數組,來看看它是怎麼工做的:

圖片


注意: 咱們使用一個ES6的Symbol:Symbol.iterator來取得一個對象的@@iterator 內部屬性。咱們在本章中簡單地提到過Symbol的語義(見「計算型屬性名」),一樣的原理適用這裏。你老是但願經過Symbol名稱引用,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。同時,與這個名稱的含義無關,@@iterator自己 不是迭代器對象, 而是一個返回迭代器對象的 方法 ——一個重要的細節!


正如上面的代碼段揭示的,迭代器的next()調用的返回值是一個{ value: .. , done: .. }形式的對象,其中value是當前迭代的值,而done是一個boolean,表示是否還有更多內容能夠迭代。


注意值3和done:false一塊兒返回,猛地一看會有些奇怪。你不得不第四次調用next()(在前一個代碼段的for..of循環會自動這樣作)來獲得done:true,而使本身知道迭代已經完成。這個特別之處的緣由超出了咱們要在這裏討論的範圍,可是它來自於ES6生成器函數的語義。


雖然數組能夠在for..of循環中自動迭代,但普通的對象 沒有內建的@@iterator。這種故意省略的緣由要比咱們將在這裏解釋的更復雜,但通常來講,爲了將來的對象類型,最好不要加入那些可能最終被證實是麻煩的實現。


可是 能夠 爲你想要迭代的對象定義你本身的默認@@iterator。好比:

圖片


注意: 咱們使用了Object.defineProperty(..)來自定義咱們的@@iterator(很大程度上是由於咱們能夠將它指定爲不可枚舉的),可是經過將Symbol做爲一個 計算型屬性名(在本章前面的部分討論過),咱們也能夠直接聲明它,好比var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }。


每次for..of循環在myObject的迭代器對象上調用next()時,迭代器內部的指針將會向前移動並返回對象屬性列表的下一個值(關於對象屬性/值迭代順序,參照前面的注意事項)。


咱們剛剛演示的迭代,是一個簡單的一個值一個值的迭代,固然你能夠爲你的自定義數據結構定義任意複雜的迭代方法,只要你以爲合適。對於操做用自戶定義對象來講,自定義迭代器與ES6的for..of循環相組合,是一個新的強大的語法工具。


舉個例子,一個Pixel(像素)對象列表(擁有x和y的座標值)能夠根據距離原點(0,0)的直線距離決定它的迭代順序,或者過濾掉那些「太遠」的點,等等。只要你的迭代器從next()調用返回指望的{ value: .. }返回值,並在迭代結束後返回一個{ done: true }值,ES6的for..of循環就能夠迭代它。


其實,你甚至能夠生成一個永遠不會「結束」,而且總會返回一個新值(好比隨機數,遞增值,惟一的識別符等等)的「無窮」迭代器,雖然你可能不會將這樣的迭代器用於一個沒有邊界的for..of循環,由於它永遠不會結束,並且會阻塞你的程序。

圖片

這個迭代器會「永遠」生成隨機數,因此咱們當心地僅從中取出100個值,以使咱們的程序不被阻塞。


複習

JS中的對象擁有字面形式(好比var a = { .. }),和構造形式(好比var a = new Array(..))。字面形式幾乎老是首選,但在某些狀況下,構造形式提供更多的構建選項。


許多人錯誤地聲稱「Javascript中的一切都是對象」,這是不對的。對象是6種(或7中,看你從哪一個方面說)基本類型之一。對象有子類型,包括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()方法每次迭代一個數據。

相關文章
相關標籤/搜索