http://es6.ruanyifeng.com/阮一峯ES6
目錄
1、Set 2、WeakSet 3、Map 4、WeakMap
1、Iterator的部署和調用 2、默認調用Iterator的場合 3、遍歷器對象的return(),throw() 4、for...of循環
十、Set和Map數據結構
優先使用Map,如果主要保證數據的唯一性,使用Set。儘量不使用數組和對象。
1、Set
Set類似於數組,但是成員的值都是唯一的,沒有重複的值。Set本身是一個構造函數,用來生成Set數據結構。Set函數可以接受一個數組或具有iterable接口的其它數據結構作爲參數(如querySelectorAll選擇的DOM元素列表),用來初始化。
向Set加入值時,不會發生類型轉換,類似於===且NaN等於自身,所以5和"5"是兩個不同的值。另外,兩個對象總是不相等的。
const set=new Set([1,2,2,3,4,4,4]);
console.log([...set]); // [1, 2, 3, 4]
(1)Set實例的屬性
Set.prototype.constructor:構造函數,默認就是Set函數。
Set.prototype.size:返回Set實例的成員總數。
(2)Set實例的操作方法(用於操作數據)
add(value):添加某個值,返回Set結構本身。
delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
has(value):返回一個布爾值,表示該值是否爲Set的成員。
clear():清除所有成員,沒有返回值。
const s=new Set();
s.add(1).add(2).add(2);
s.size // 2
s.has(1) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
s.clear();
(3)Set實例的遍歷方法(用於遍歷成員)
keys(),values(),entries():返回鍵名、鍵值、鍵值對的遍歷器對象。Set結構的鍵名和鍵值是同一個值,keys和values方法的行爲完全一致。
forEach():使用回調函數遍歷每個成員,沒有返回值。回調函數的參數依次爲鍵值、鍵名、集合本身。第二個參數表示綁定處理函數內部的this對象。
Set的遍歷順序就是插入順序。如使用Set保存一個回調函數列表,調用時就能保證按照添加順序調用。
Set結構的實例默認可遍歷,它的默認遍歷器生成函數就是它的values方法。可以省略values方法,直接用for...of循環遍歷Set。
let set=new Set(['red','green','blue']);
for(let item of set.keys()){
console.log(item);
} // red green blue
for(let item of set.entries()) {
console.log(item);
} // ["red", "red"] ["green", "green"] ["blue", "blue"]
用於去除數組的重複成員。實現並集(Union)、交集(Intersect)和差集(Difference)。
// 去除數組的重複成員
[...new Set(array)]
Array.from(new Set(array))
let a=new Set([1, 2, 3]);
let b=new Set([4, 3, 2]);
// 並集
let union=new Set([...a,...b]); // Set {1, 2, 3, 4}
// 交集
let intersect=new Set([...a].filter(x=>b.has(x))); // set {2, 3}
// 差集
let difference=new Set([...a].filter(x=>!b.has(x))); // Set {1}
如果想在遍歷操作中,同步改變原來的Set結構。
// 使原Set中每個值變爲2倍[2,4,6]
let set=new Set([1,2,3]);
set=new Set([...set].map(val=>val*2)); // 方法一
set=new Set(Array.from(set,val=>val*2)); // 方法二
2、WeakSet
WeakSet與Set有兩個區別。首先,WeakSet的成員只能是對象,不能是其它類型的值。其次,WeakSet中的對象都是弱引用,如果其它對象都不再引用該對象,那麼垃圾回收機制會自動回收該對象所佔用的內存,只要這些對象在外部消失,它在WeakSet裏面的引用就會自動消失。
WeakSet不可遍歷,沒有size屬性。由於WeakSet內部有多少個成員,取決於垃圾回收機制有沒有運行,運行前後很可能成員個數是不一樣的,而垃圾回收機制何時運行是不可預測的。
WeakSet是一個構造函數,可以使用new命令,創建WeakSet數據結構。WeakSet可以接受一個數組或類似數組的對象(任何具有Iterable接口的對象)作爲參數。該數組的所有成員,都會自動成爲WeakSet實例對象的成員。
const a=[[1,2],[3,4]];
const ws1=new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
const b=[3,4];
const ws2=new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…)
WeakSet.prototype.add(value):向WeakSet實例添加一個新成員。
WeakSet.prototype.delete(value):清除WeakSet實例的指定成員。
WeakSet.prototype.has(value):返回一個布爾值,表示某個值是否在WeakSet實例之中。
WeakSet 適合臨時存放一組對象,以及存放跟對象綁定的信息。如儲存DOM節點,而不用擔心這些節點從文檔移除時,會引發內存泄漏。
3、Map
Map數據結構類似於對象,也是鍵值對的集合,但是“鍵”的範圍不限於字符串,各種類型的值(包括對象)都可以當作鍵。Object結構提供了“字符串—值”的對應,Map結構提供了“值—值”的對應,是一種更完善的Hash結構實現。
Map可以接受一個數組或其它具有Iterator接口、且每個成員都是一個雙元素的數組的數據結構作爲參數。Set和Map都可以用來生成新的Map。
const map=new Map([
['name','張三'],
['title','Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "張三"
只有對同一個對象的引用,Map結構纔將其視爲同一個鍵。下面代碼的set和get方法,鍵值相同但內存地址是不一樣的。Map的鍵實際上是跟內存地址綁定的,解決了同名屬性碰撞(clash)的問題,擴展別人的庫時,如果使用對象作爲鍵名,就不用擔心屬性同名。
如果Map的鍵是一個簡單類型的值(數字、字符串、布爾值),則只要兩個值嚴格相等,Map 將其視爲一個鍵。0和-0是一個鍵,布爾值true和字符串true是不同的鍵,undefined和null是不同的鍵,NaN是同一個鍵。
const map=new Map();
map.set(['a'],555);
map.get(['a']) // undefined
(1)Map實例的屬性和操作方法
size屬性返回Map結構的成員總數。
set(key, value)方法設置鍵名key對應的鍵值爲value,返回整個Map結構,可以採用鏈式寫法。如果key已經有值,則鍵值會被更新,否則就新生成該鍵。
get(key)方法讀取key對應的鍵值,如果找不到key,返回undefined。
has(key)方法返回一個布爾值,表示某個鍵是否在當前Map對象之中。
delete(key)方法刪除某個鍵,返回true。如果刪除失敗,返回false。
clear()方法清除所有成員,沒有返回值。
const m=new Map();
const o={p:'Hello World'};
m.size // 0
m.set(o,'content1').set(o,"content2");
m.get(o) // "content2"
m.get(123); // undefined
m.has(o) // true
m.delete(o) // true
m.has(o) // false
(2)Map實例的遍歷方法
keys(),values(),entries():返回鍵名、鍵值、所有成員的遍歷器對象。
forEach():遍歷Map的所有成員。回調函數的參數依次爲鍵值、鍵名、集合本身。第二個參數表示綁定處理函數內部的this對象。
Map的遍歷順序就是插入順序。Map結構的默認遍歷器接口(Symbol.iterator屬性)是entries方法。
const map=new Map([
['F','no'],
['T','yes'],
]);
for(let key of map.keys()){
console.log(key);
} // "F" "T"
for(let value of map.values()){
console.log(value);
} // "no" "yes"
// 等同於使用map.entries()
for(let [key,value] of map){
console.log(key, value);
} // "F" "no" "T" "yes"
(3)與其他數據結構的互相轉換
Map->數組,使用擴展運算符(...)。
數組->Map,將數組傳入Map構造函數new Map()。
Map->對象,如果所有Map的鍵都是字符串,它可以無損地轉爲對象。非字符串的鍵名會被轉成字符串,再作爲對象的鍵名。
function strMapToObj(strMap){
let obj=Object.create(null);
for(let [k,v] of strMap){
obj[k]=v;
}
return obj;
}
function objToStrMap(obj){
let strMap=new Map();
for(let k of Object.keys(obj)){
strMap.set(k,obj[k]);
}
return strMap;
}
Map->JSON,如果Map的鍵名都是字符串,轉爲對象JSON。Map的鍵名有非字符串,轉爲數組JSON。
JSON->Map,正常情況下,所有鍵名都是字符串。但如果整個JSON就是一個數組,且每個數組成員本身,又是一個有兩個成員的數組,可以一一對應地轉爲Map。
function strMapToJson(strMap){
return JSON.stringify(strMapToObj(strMap));
}
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
function jsonToStrMap(jsonStr){
return objToStrMap(JSON.parse(jsonStr));
}
function jsonToMap(jsonStr){
return new Map(JSON.parse(jsonStr));
}
4、WeakMap
WeakMap結構與Map結構類似,也是用於生成鍵值對的集合。區別有兩點,WeakMap只接受對象作爲鍵名(null除外),WeakMap的鍵名所指向的對象,不計入垃圾回收機制。
參考SetMap。
十一、Iterator和for...of循環
JavaScript表示“集合”的數據結構Array、Object、Map和Set,還可以組合使用它們定義自己的數據結構。遍歷器(Iterator)是一種接口,爲各種數據結構提供一個統一的訪問機制,即for...of循環,使得數據結構的成員能夠按某種次序排列。任何數據結構只要部署Iterator接口,就可以完成遍歷操作,稱這種數據結構是“可遍歷的”(iterable)。
Iterator的遍歷過程:創建一個指針對象,指向當前數據結構的起始位置;不斷調用指針對象的next方法,直到它指向數據結構的結束位置。每一次調用next方法,都會返回一個對象包含當前成員的信息,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。(像是數據結構中的鏈表結構)
1、Iterator的部署和調用
ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性,該屬性是一個函數,即當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。
有些數據結構原生具備Iterator接口,稱爲部署了遍歷器接口,不用任何處理就可以被for...of循環遍歷。Array、Map、Set、String、TypedArray、函數的arguments對象、NodeList對象。
let arr=['a','b','c'];
let iter=arr[Symbol.iterator]();
console.log(iter.next()); // {value: "a", done: false}
console.log(iter.next()); // {value: "b", done: false}
console.log(iter.next()); // {value: "c", done: false}
console.log(iter.next()); // {value: undefined, done: true}
Object對象默認沒有部署遍歷器接口,因爲對象屬性的遍歷順序不確定,且對象是非線性的數據結構,遍歷器是一種線性處理。
自定義Iterator接口時,done:false和value:undefined屬性可以省略。
const obj={
data:["hello","world"],
[Symbol.iterator](){
const self=this;
let index=0;
return {
next(){
if(index<self.data.length){
return {value:self.data[index++]};
}else{
return {done:true};
}
}
}
}
}
for(let item of obj){
console.log(item);
}
// "hello"
// "world"
對於類似數組的對象(存在數值鍵名和length屬性),部署Iterator接口的簡便方法,就是Symbol.iterator方法直接引用數組的Iterator接口。
let iterable={
0:'a',
1:'b',
2:'c',
length:3,
[Symbol.iterator]:Array.prototype[Symbol.iterator]
};
Symbol.iterator方法的最簡單實現是使用Generator函數。
let obj={
* [Symbol.iterator](){ // [Symbol.iterator]:function*()
yield 'hello';
yield 'world';
}
};
2、默認調用Iterator的場合
默認調用Iterator接口,即Symbol.iterator方法。
(1)for...of循環
(2)解構賦值:對數組和 Set 結構進行解構賦值時
(3)擴展運算符...:將任何部署了Iterator接口的數據結構,轉爲數組
(4)yield*:yield*後面跟的是一個可遍歷的結構
(5)其它
由於數組的遍歷會調用遍歷器接口,所以任何接受數組作爲參數的場合,都調用了遍歷器接口。for...of、Array.from()、Map(), Set(), WeakMap(), WeakSet()、Promise.all()、Promise.race()等。
3、遍歷器對象的return(),throw()
遍歷器對象的return和throw方法是否部署是可選的。
如果for...of循環提前退出(出錯或有break語句),就會調用return方法。如果一個對象在完成遍歷前,需要清理或釋放資源,就可以部署return方法。return方法必須返回一個對象。
throw方法主要是配合Generator函數使用,一般的遍歷器對象用不到這個方法。
4、for...of循環
十二、Proxy和Reflect
1、Proxy代理器
Proxy代理,“代理”某些操作,修改某些操作的默認行爲。在目標對象之前架設一層“攔截”,外界對該對象的訪問都必須先通過這層攔截,可以對外界的訪問進行過濾和改寫。Proxy實際上重載了點運算符,即用自己的定義覆蓋了語言的原始定義。
ES6原生提供Proxy構造函數,用來生成Proxy實例。target參數表示要攔截的目標對象,handler參數表示用來定製攔截行爲的對象。如果handler沒有設置任何攔截,就等同於直接通向原對象。
要使得Proxy起作用,必須針對Proxy實例進行操作,而不是針對目標對象進行操作。
var proxy=new Proxy(target,handler);
var proxy=new Proxy({},{
get:function(target,property){
return 35;
}
});
// 要使得Proxy起作用,必須針對Proxy實例進行操作,而不是針對目標對象進行操作。
proxy.time // 35
// Proxy實例也可以作爲其他對象的原型對象。obj對象本身沒有time屬性,根據原型鏈會在proxy對象上讀取該屬性,導致被攔截。
let obj=Object.create(proxy);
obj.time // 35
// 將Proxy對象,設置到object.proxy屬性,從而可以在object對象上調用。
var object={proxy:new Proxy(target,handler)};
(1)get(target, propKey, receiver)
攔截某個屬性的讀取操作,如proxy.foo和proxy['foo']。可以接受三個參數,依次爲目標對象、屬性名和proxy實例本身(操作行爲所針對的對象),最後一個參數可選。
get方法可以繼承。
如果一個屬性不可配置(configurable)且不可寫(writable),則Proxy不能修改該屬性,否則通過Proxy對象訪問該屬性會報錯。
(2)set(target, propKey, value, receiver)
攔截某個屬性的賦值操作,如proxy.foo=v和proxy['foo']=v。可以接受四個參數,依次爲目標對象、屬性名、屬性值和Proxy實例本身,其中最後一個參數可選,返回一個布爾值。嚴格模式下,set代理返回false或者undefined,沒有返回true,都會報錯。
可以數據綁定,即每當對象發生變化時,會自動更新DOM。
如果目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用。
(3)has(target, propKey)
攔截HasProperty操作,而不是HasOwnProperty操作,判斷對象是否具有某個屬性時,這個方法會生效。典型的操作是in運算符,但是對for...in循環不生效。接受兩個參數,分別是目標對象、需查詢的屬性名,返回一個布爾值。
如果原對象不可配置或者禁止擴展,這時has攔截會報錯。
(4)deleteProperty(target, propKey)
攔截delete proxy[propKey]操作,如果這個方法拋出錯誤或者返回false,當前屬性就無法被delete命令刪除。
目標對象自身的不可配置(configurable)的屬性,不能被該方法刪除,否則報錯。
(5)ownKeys(target)
攔截對象自身屬性的讀取操作,包括Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環。返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
使用Object.keys方法時,有三類屬性會被ownKeys方法自動過濾,不會返回。包括目標對象上不存在的屬性、屬性名爲Symbol值、不可遍歷(enumerable)的屬性。
(6)其它
getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性描述對象或undefined。
defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy,propKey,propDesc)、Object.defineProperties(proxy,propDescs),返回一個布爾值。
preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。
如果目標對象是函數,那麼還有兩種額外操作可以攔截。
apply(target, object, args):攔截Proxy實例作爲函數調用的操作,比如proxy(...args)、proxy.call(object,...args)、proxy.apply(...)。
construct(target, args):攔截Proxy實例作爲構造函數調用的操作,比如new proxy(...args)。
let obj={ // 原始對象存儲真實的數據
time:"2018-12-24",
name:'yc',
_r:123
};
let proxy=new Proxy(obj,{ // 代理器
get(target,key){
if(key in target){
return target[key];
}else{ // 如果沒有攔截函數,訪問不存在的屬性,只會返回undefined
throw new ReferenceError("Property \""+key+"\" does not exist.");
}
},
set(target,key,value){
if(key==="time"){ // 只有time屬性可以賦值修改
target[key]=value;
}
return true;
},
has(target,key){
if(key[0]==='_'){
return false
}
return key in target;
},
deleteProperty(target,key){
if(key[0]==='_'){
return false;
}
delete target[key];
return true;
},
ownKeys(target){
return Reflect.ownKeys(target).filter(key=>key[0]!='_');
}
});
console.log(proxy.time); // "2018-12-24"
console.log(proxy.age); // Uncaught ReferenceError: Property "age" does not exist.
proxy.time="2019-12-25";
console.log(proxy.time); // "2019-12-25"
proxy.name="hsg";
console.log(proxy.name); // "yc"
console.log("name" in proxy); // true
console.log("_r" in proxy); // false
for(let key in proxy){
console.log(key); // time name _r has不起作用
}
delete proxy.time;
delete proxy._r;
console.log(proxy); // {name: "yc", _r: 123}
console.log(Object.keys(proxy)); // ["name"]
2、Reflect
Reflect對象與Proxy對象一樣,也是ES6爲了操作對象而提供的新API。Reflect對象的設計目的:將Object對象的一些明顯屬於語言內部的方法放到Reflect對象上;修改某些Object方法的返回結果,讓其變得更合理; 讓Object操作都變成函數行爲;Reflect對象與Proxy對象的方法一一對應。
(1)Reflect.get(target, name, receiver),查找並返回target對象的name屬性,如果沒有該屬性,則返回undefined。
(2)Reflect.set(target, name, value, receiver),設置target對象的name屬性等於value。
(3)Reflect.has(obj, name),對應name in obj裏面的in運算符。
(4)Reflect.deleteProperty(obj, name),等同於delete obj[name],用於刪除對象的屬性。如果刪除成功或被刪除的屬性不存在,返回true;刪除失敗,被刪除的屬性依然存在,返回false。
(5)Reflect.ownKeys (target),用於返回對象的所有屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。
(6)其它
let obj={
time:"2018-12-24",
name:'yc',
_r:123,
[Symbol('foo')]:456
};
console.log(Reflect.get(obj,"name")); // "yc"
Reflect.set(obj,"name","hsg");
console.log(Reflect.get(obj,"name")); // "hsg"
console.log(Reflect.has(obj,"name")); // true
Reflect.deleteProperty(obj,"name");
console.log(Reflect.get(obj,"name")); // undefined
console.log(Object.getOwnPropertyNames(obj)); // ["time", "_r"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(foo)]
console.log(Reflect.ownKeys(obj)); // ["time", "_r", Symbol(foo)]
如果Proxy對象和Reflect對象聯合使用,前者攔截操作,後者完成默認行爲。
// 用Proxy和Reflect實現和業務解耦的校驗模塊
function validator(target,validator){
return new Proxy(target,{
_validator:validator,
set(target,key,value,proxy){
if(target.hasOwnProperty(key)){
let va=this._validator[key];
if(!!va(value)){
return Reflect.set(target,key,value,proxy);
}else{
throw Error(`不能設置${key}到${value}`);
}
}else{
throw Error(`${key}不存在`);
}
}
})
}
const personValidator={
name(val){
return typeof val==='string'
},
age(val){
return typeof val==='number'&&val>=18
}
}
class Person{
constructor(name,age){
this.name=name;
this.age=age;
return validator(this,personValidator);
}
}
const person=new Person("yc",25);
console.log(person); // {name: "yc", age: 25}
person.name=23;
console.log(person); // Uncaught Error: 不能設置name到23