當前位置:
首頁 > 科技 > 你不懂JS:ES6與未來 集合

你不懂JS:ES6與未來 集合

你不懂JS:ES6與未來 集合



前言


《你不懂JS》系列好久沒來了,今天早讀文章由前端早讀課專欄作者@HetfieldJoe翻譯分享。


正文從這開始~

結構化的集合與數據訪問對於任何JS程序來說都是一個關鍵組成部分。從這門語言的最開始到現在,數組和對象一直都是我們創建數據結構的主要機制。當然,許多更高級的數據結構作為用戶方的庫都曾建立在這些之上。


到了ES6,最有用(而且優化性能的!)的數據結構抽象中的一些已經作為這門語言的原生組件被加入了進來。


我們將通過檢視 類型化數組(TypedArrays) 來開始這一章,技術上講它與幾年前的ES5是同一時期的產物,但是僅僅作為WebGL的同伴被標準化了,而不是作為JavaScript自身的一部分。到了ES6,這些東西已經被語言規範直接採納了,這給予了它們頭等的地位。


Map就像對象(鍵/值對),但是與僅能使用一個字元串作為鍵不同的是,你可以使用任何值 —— 即使是另一個對象或map!Set與數組很相似(值的列表),但是這些值都是唯一的;如果你添加一個重複的值,它會被忽略。還有與之相對應的weak結構(與內存/垃圾回收有關聯):WeakMap和WeakSet。


類型化數組(TypedArrays)


正如我們在本系列的 類型與文法 中講到過的,JS確實擁有一組內建類型,比如number和string。看到一個稱為「類型化的數組」的特性,可能會誘使你推測它意味著一個特定類型的值的數組,比如一個僅含字元串的數組。


然而,類型化數組其實更多的是關於使用類似數組的語義(索引訪問,等等)提供對二進位數據的結構化訪問。名稱中的「類型」指的是在大量二進位位(比特桶)的類型之上覆蓋的「視圖」,它實質上是一個映射,控制著這些二進位位是否應當被看作8位有符號整數的數組,還是被看作16位有符號整數的數組,等等。


你怎樣才能構建這樣的比特桶呢?它被稱為一個「緩衝(buffer)」,而你可以用ArrayBuffer(..)構造器直接地構建它:


varbuf=newArrayBuffer(32);


buf.byteLength;// 32

現在buf是一個長度為32位元組(256比特)的二進位緩衝,它被預初始化為全0。除了檢查它的byteLength屬性,一個緩衝本身不會允許你進行任何操作。


提示: 有幾種web平台特性都使用或返回緩衝,比如FileReader#readAsArrayBuffer(..),XMLHttpRequest#send(..),和ImageData(canvas數據)。


但是在這個數組緩衝的上面,你可以平鋪一層「視圖」,它就是用類型化數組的形式表現的。考慮如下代碼:


vararr=newUint16Array(buf);


arr.length;// 16


arr是一個256位的buf緩衝在16位無符號整數的類型化數組的映射,意味著你得到16個元素。


位元組順序


明白一個事實非常重要:arr是使用JS所運行的平台的位元組順序設定(大端法或小端法)被映射的。如果二進位數據是由一種位元組順序創建,但是在一個擁有相反位元組數序的平台被解釋時,這就可能是個問題。


位元組順序指的是一個多位元組數字的低位位元組(8個比特位的集合) —— 比如我們在早先的代碼段中創建的16位無符號整數 —— 是在這個數字的位元組序列的左邊還是右邊。


但是如果3085使用兩個8位數字來表示的話,位元組順序就像會極大地影響它在內存中的存儲:

如今在web上最常見的表現形式是小端法,但是絕對存在一些與此不同的瀏覽器。你明白一塊二進位數據的生產者和消費者的位元組順序是十分重要的。


在MDN上有一種快速的方法測試你的JavaScript的位元組順序:


varlittleEndian=(function(){


varbuffer=newArrayBuffer(2);


newDataView(buffer).setInt16(,256,true);


returnnewInt16Array(buffer)[]===256;


})();


littleEndian將是true或false;對大多數瀏覽器來說,它應當返回true。這個測試使用DataView(..),它允許更底層,更精細地控制如何從你平鋪在緩衝上的視圖中訪問二進位位。前面代碼段中的setInt16(..)方法的第三個參數告訴DataView,對於這個操作你想使用什麼位元組順序。


多視圖


一個單獨的緩衝可以連接多個視圖,例如:

類型化數組的構造器擁有多種簽名。目前我們展示過的只是向它們傳遞一個既存的緩衝。然而,這種形式還接受兩個額外的參數:byteOffset和length。換句話講,你可以從0以外的位置開始類型化數組視圖,也可以使它的長度小於整個緩衝的長度。


如果二進位數據的緩衝包含規格不一的大小/位置,這種技術可能十分有用。


例如,考慮一個這樣的二進位緩衝:在開頭擁有一個2位元組數字(也叫做「字」),緊跟著兩個1位元組數字,然後跟著一個32位浮點數。這是你如何在同一個緩衝,偏移量,和長度上使用多視圖來訪問數據:


varfirst=newUint16Array(buf,,2)[],


second=newUint8Array(buf,2,1)[],


third=newUint8Array(buf,3,1)[],


fourth=newFloat32Array(buf,4,4)[];


類型化數組構造器


除了前一節我們檢視的(buffer,[offset, [length]])形式之外,類型化數組的構造器還支持這些形式:


[constructor](length):在一個長度為length位元組的緩衝上創建一個新視圖

[constructor](typedArr):創建一個新視圖和緩衝,並拷貝typedArr視圖中的內容


[constructor](obj):創建一個新視圖和緩衝,并迭代類數組或對象obj來拷貝它的內容


在ES6中可以使用下面的類型化數組構造器:


Int8Array(8位有符號整數),Uint8Array(8位無符號整數)


Uint8ClampedArray(8位無符號整數,每個值都被卡在0 - 255範圍內)


Int16Array(16位有符號整數),Uint16Array(16位無符號整數)


Int32Array(32位有符號整數),Uint32Array(32位無符號整數)


Float32Array(32位浮點數,IEEE-754)


Float64Array(64位浮點數,IEEE-754)


類型化數組構造器的實例基本上和原生的普通數組是一樣的。一些區別包括它有一個固定的長度並且值都是同種「類型」。

但是,它們共享絕大多數相同的prototype方法。這樣一來,你很可能將會像普通數組那樣使用它們而不必進行轉換。


例如:


vara=newInt32Array(3);


a[]=10;


a[1]=20;


a[2]=30;


a.map(function(v){


console.log(v);


});


// 10 20 30

a.join("-");


// "10-20-30"


警告: 你不能對類型化數組使用沒有意義的特定Array.prototype方法,比如修改器(splice(..),push(..),等等)和concat(..)。


要小心,在類型化數組中的元素被限制在它被聲明的位長度中。如果你有一個Uint8Array並試著向它的一個元素賦予某些大於8為的值,那麼這個值將被截斷以保持在相應的位長度中。


這可能造成一些問題,例如,如果你試著對一個類型化數組中的所有值求平方。考慮如下代碼:


vara=newUint8Array(3);


a[]=10;


a[1]=20;


a[2]=30;


varb=a.map(function(v){

returnv*v;


});


b;// [100, 144, 132]


在被平方後,值20和30的結果會位溢出。要繞過這樣的限制,你可以使用TypedArray#from(..)函數:


關於被類型化數組所共享的Array.from(..)函數的更多信息,參見第六章的「Array.from(..)靜態方法」一節。特別地,「映射」一節講解了作為第二個參數值被接受的映射函數。


一個值得考慮的有趣行為是,類型化數組像普通數組一樣有一個sort(..)方法,但是這個方法默認是數字排序比較而不是將值強制轉換為字元串進行字典順序比較。例如:


就像Array#sort(..)一樣,TypedArray#sort(..)接收一個可選的比較函數作為參數值,它們的工作方式完全一樣。


Maps


如果你對JS經驗豐富,那麼你一定知道對象是創建無序鍵/值對數據結構的主要機制,這也被稱為map。然而,將對象作為map的主要缺陷是不能使用一個非字元串值作為鍵。


例如,考慮如下代碼:

varm={};


varx={id:1},


y={id:2};


m[x]="foo";


m[y]="bar";


m[x];// "bar"


m[y];// "bar"


這裡發生了什麼?x和y這兩個對象都被字元串化為"[object Object]",所以只有這一個鍵被設置為m。


一些人通過在一個值的數組旁邊同時維護一個平行的非字元串鍵的數組實現了山寨的map,比如:


varkeys=[],vals=[];


varx={id:1},


y={id:2};


keys.push(x);


vals.push("foo");


keys.push(y);


vals.push("bar");


keys[]===x;// true


vals[];// "foo"


keys[1]===y;// true


vals[1];// "bar"


當然,你不會想親自管理這些平行數組,所以你可能會定義一個數據解構,使它內部帶有自動管理的方法。除了你不得不自己做這些工作,主要的缺陷是訪問的時間複雜度不再是O(1),而是O(n)。


但在ES6中,不再需要這麼做了!使用Map(..) 就好:


varm=newMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.set(y,"bar");


m.get(x);// "foo"


m.get(y);// "bar"


唯一的缺點是你不能使用[]方括弧訪問語法來設置或取得值。但是get(..)和set(..)可以完美地取代這種語法。


要從一個map中刪除一個元素,不要使用delete操作符,而是使用delete(..) 方法:


m.set(x,"foo");


m.set(y,"bar");


m.delete(y);


使用clear()你可清空整個map的內容。要得到map的長度(也就是,鍵的數量),使用size屬性(不是length)。


m.set(x,"foo");


m.set(y,"bar");


m.size;// 2


m.clear();


m.size;// 0


Map(..)的構造器還可以接受一個可迭代對象(參見第三章的「迭代器」),它必須產生一個數組的列表,每個數組的第一個元素是鍵,第二元素是值。這種用於迭代的格式與entries()方法產生的格式是一樣的,entries()方法將在下一節中講解。這使得製造一個map的拷貝十分簡單:


varm2=newMap(m.entries());


// 等同於:


varm2=newMap(m);


因為一個map實例是一個可迭代對象,而且它的默認迭代器與entries()相同,第二種稍短的形式更理想。


當然,你可以在Map(..)構造器形式中手動指定一個 entries 列表:


varx={id:1},


y={id:2};


varm=newMap([


[x,"foo"],


[y,"bar"]


]);


m.get(x);// "foo"


m.get(y);// "bar"


Map 值


要從一個map得到值的列表,使用values(..),它返回一個迭代器。在第二和第三章,我們講解了幾種序列化(像一個數組那樣)處理一個迭代器的方法,比如...擴散操作符和for..of循環。另外,第六章的「Arrays」將會詳細講解Array.from(..)方法。考慮如下代碼:


varm=newMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.set(y,"bar");


varvals=[...m.values()];


vals;// ["foo","bar"]


Array.from(m.values());// ["foo","bar"]


就像在前一節中討論過的,你可以使用entries()(或者默認的map迭代器)迭代一個map的記錄。考慮如下代碼:


varm=newMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.set(y,"bar");


varvals=[...m.entries()];


vals[][]===x;// true


vals[][1];// "foo"


vals[1][]===y;// true


vals[1][1];// "bar"


Map 鍵


要得到鍵的列表,使用keys(),它返回一個map中鍵的迭代器:


varm=newMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.set(y,"bar");


varkeys=[...m.keys()];


keys[]===x;// true


keys[1]===y;// true


要判定一個map中是否擁有一個給定的鍵,使用has(..):


varm=newMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.has(x);// true


m.has(y);// false


實質上map讓你將一些額外的信息(值)與一個對象(鍵)相關聯,而不用實際上將這些信息放在對象本身中。


雖然在一個map中你可以使用任意種類的值作為鍵,但是你經常使用的將是對象,就像字元串和其他在普通對象中可以合法地作為鍵的基本類型。換句話說,你可能將想要繼續使用普通對象,除非一些或全部的鍵需要是對象,在那種情況下map更合適。


警告: 如果你使用一個對象作為一個map鍵,而且這個對象稍後為了能夠被垃圾回收器(GC)回收它佔用的內存而被丟棄(解除所有的引用),那麼map本身將依然持有它的記錄。你需要從map中移除這個記錄來使它能夠被垃圾回收。在下一節中,我們將看到對於作為對象鍵和GC來說更好的選擇 —— WeakMaps。


WeakMaps


WeakMap是map的一個變種,它們的大多數外部行為是相同的,而在底層內存分配(明確地說是它的GC)如何工作上有區別。


WeakMap(僅)接收對象作為鍵。這些對象被 弱 持有,這意味著如果對象本身被垃圾回收掉了,那麼在WeakMap中的記錄也會被移除。這是觀察不到的,因為一個對象可以被垃圾回收的唯一方法是不再有指向它的引用 —— 一旦不再有指向它的引用,你就沒有對象引用可以用來檢查它是否存在於這個WeakMap中。


除此以外,WeakMap的API是相似的,雖然限制更多:


varm=newWeakMap();


varx={id:1},


y={id:2};


m.set(x,"foo");


m.has(x);// true


m.has(y);// false


WeakMap沒有size屬性和clear()方法,它們也不對它們的鍵,值和記錄暴露任何迭代器。所以即便你解除了x引用,它將會因GC從m中移除它的記錄,也沒有辦法確定這一事實。你只能相信JavaScript會這麼做!


就像map一樣,WeakMap讓你將信息與一個對象軟關聯。如果你不能完全控制這個對象,比如DOM元素,它們就特別有用。如果你用做map鍵的對象可以被刪除並且應當在被刪除時成為GC的回收對象,那麼一個WeakMap就是更合適的選項。


要注意的是WeakMap只弱持有它的 鍵,而不是它的值。考慮如下代碼:


varm=newWeakMap();


varx={id:1},


y={id:2},


z={id:3},


w={id:4};


m.set(x,y);


x=null;// { id: 1 } 是可以GC的


y=null;// 由於 { id: 1 } 是可以GC的,因此 { id: 2 } 也可以


m.set(z,w);


w=null;// { id: 4 } 不可以 GC


因此,我認為WeakMap被命名為「WeakKeyMap」更好。


Sets


一個set是一個集合,其中的值都是唯一的(重複的會被忽略)。


set的API與map很相似。add(..)方法(有點諷刺地)取代了set(..),而且沒有get(..)方法。


考慮如下代碼:


vars=newSet();


varx={id:1},


y={id:2};


s.add(x);


s.add(y);


s.add(x);


s.size;// 2


s.delete(y);


s.size;// 1


s.clear();


s.size;// 0


Set(..)構造器形式與Map(..)相似,它可以接收一個可迭代對象,比如另一個set或者一個值的數組。但是,與Map(..)期待一個 記錄 的列表(鍵/值數組的數組)不同的是,Set(..)期待一個 值 的列表(值的數組):


varx={id:1},


y={id:2};


vars=newSet([x,y]);


一個set不需要get(..),因為你不會從一個set中取得值,而是使用has(..)測試一個值是否存在:


vars=newSet();


varx={id:1},


y={id:2};


s.add(x);


s.has(x);// true


s.has(y);// false


注意: has(..)中的比較演算法與Object.is(..)(見第六章)幾乎完全相同,除了-0和0被視為相同而非不同。


Set 迭代器


set和map一樣擁有相同的迭代器方法。set的行為有所不同,但是與map的迭代器的行為是對稱的。考慮如下代碼:


vars=newSet();


varx={id:1},


y={id:2};


s.add(x).add(y);


varkeys=[...s.keys()],


vals=[...s.values()],


entries=[...s.entries()];


keys[]===x;


keys[1]===y;


vals[]===x;


vals[1]===y;


entries[][]===x;


entries[][1]===x;


entries[1][]===y;


entries[1][1]===y;


keys()和values()迭代器都會給出set中唯一值的列表。entries()迭代器給出記錄數組的列表,記錄數組中的兩個元素都是唯一的set值。一個set的默認迭代器是它的values()迭代器。


一個set天生的唯一性是它最有用的性質。例如:


vars=newSet([1,2,3,4,"1",2,4,"5"]),


uniques=[...s];


uniques;// [1,2,3,4,"1","5"]


set的唯一性不允許強制轉換,所以1和"1"被認為是不同的值。


WeakSets


一個WeakMap弱持有它的鍵(但強持有它的值),而一個WeakSet弱持有它的值(不存在真正的鍵)。


vars=newWeakSet();


varx={id:1},


y={id:2};


s.add(x);


s.add(y);


x=null;// `x` 可以GC


y=null;// `y` 可以 GC


警告: WeakSet的值必須是對象,在set中被允許的基本類型值是不行的。


複習


ES6定義了幾種有用的集合,它們使得處理解構化的數據更加高效和有效。


類型化數組提供了二進位數據緩衝的「視圖」,它使用各種整數類型對齊,比如8位無符號整數和32位浮點數。二進位數據的數組訪問使得操作更加容易表達和維護,它可以讓你更簡單地處理如視頻,音頻,canvas數據等複雜的數組。


Map是鍵-值對集合,它的鍵可以是對象而非只可以是字元串/基本類型。Set是(任何類型的)唯一值的列表。


WeakMap是鍵(對象)被弱持有的map,所以如果它是最後一個指向這個對象的引用,GC就可以自由地回收這個記錄。WeakSet是值被弱持有的set,所以同樣地,如果它是最後一個指向這個對象的引用,GC就可以移除這個記錄。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

廣州唯品會,找像你這樣優秀的工程師
如何設計優秀的 HTML API
騰訊Web前端大會熱力來襲
比較與理解React的Components,Elements和Instances
高效前端團隊的秘密

TAG:前端早讀課 |

您可能感興趣

SGU合格速報:明治大學SGJS項目OFFER!
HTML5+CSS3從入門到精通 CSS3及JS媒體查詢詳解
SGU合格速報:摘3枚明治大學SGJS項目OFFER!
JSON 使用
ReactJS:我就是想把代碼和HTML混在一起!
JS/CSS體積減少了67%,我們是如何做到的?
Sorry,會JS真的了不起
關於 JDK 9 中的 JShell,你應該了解的 10 件事
Spring MVC請求及返回JSON數據
Springboot 2.0整合JSP與JSP的熱部署
在Python中使用JSON
GJSAY:Facebook竊取5000萬用戶數據大數據時代或許比你想像的更恐怖
JSON不是合格的配置語言!
淺談WKWebView使用、JS的交互
Dota2:北美賽區三預選資格存疑 OpTic、VGJS還有EG真的欽定TI么
新ANSI、ESDA、JEDEC JS-002 CDM測試標準概覽
CSS-in-JS,向Web組件化再邁一大步
face-api.js中加入MTCNN:進一步支持使用JS實時進行人臉跟蹤和識別
我所不知的JS
10分鐘了解JSON Web令牌