ES6語法——Set、Map、遍歷器、代理、Reflect

http://es6.ruanyifeng.com/阮一峯ES6

目錄

十、Set和Map數據結構

    1、Set    2、WeakSet    3、Map    4、WeakMap

十一、Iterator和for...of循環

    1、Iterator的部署和調用    2、默認調用Iterator的場合    3、遍歷器對象的return(),throw()    4、for...of循環

十二、Proxy和Reflect

    1、Proxy代理器    2、Reflect

十、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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章