天天看點

ES6--Set、Map、Symbol、Proxy及Reflect

版權聲明:本文為部落客原創文章,遵循 CC 4.0 by-sa 版權協定,轉載請附上原文出處連結和本聲明。

本文連結:https://ligang.blog.csdn.net/article/details/70553094

九、Set和Map資料結構

Set

ES6提供了新的資料結構Set。它類似于數組,但是成員的值都是唯一的,沒有重複的值。之前的博文曾闡述過使用ES5實作JavaScript資料結構-集合。

new Set([iterable]);           

複制

var items = new Set([1,2,3,4,5,5,5,5]);
console.log(items.size);  // 5
console.log(items);       // Set {1, 2, 3, 4, 5}
console.log([...items]);  // [1, 2, 3, 4, 5]           

複制

方法 說明
add(value) 在Set對象尾部添加一個元素,傳回該Set對象
clear() 移除Set對象内的所有元素,沒有傳回值
delete(value) 移除Set的中與這個值相等的元素,傳回一個布爾值,表示删除是否成功
has(value) 傳回一個布爾值,表示該值是否為Set的成員
keys()/values() 傳回一個新的疊代器對象,該對象包含Set對象中的按插入順序排列的所有元素的值
entries() 傳回一個新的疊代器對象,該對象包含Set對象中的按插入順序排列的所有元素的值的[value, value]數組
forEach(callbackFn[, thisArg]) 按照插入順序,為Set對象中的每一個值調用一次callBackFn。如果提供了thisArg參數,回調中的this會是這個參數

示例:去除數組的重複成員

var ary = [1, 2, 3, 3, 2, 1, "1"];
[...new Set(ary)];  // [1, 2, 3, "1"]           

複制

注意:向Set加入值的時候,不會發生類型轉換,是以5和”5”是兩個不同的值。

WeakSet

WeakSet結構與Set類似,WeakMap結構與Map結構基本類似。WeakSet的成員隻能是對象;WeakSet中的對象都是弱引用,即垃圾回收機制不考慮WeakSet中對象的引用,其意味着WeakSet不可周遊、不會引發記憶體洩漏。

var ws = new WeakSet();
var obj = {};
var foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo); // false,foo尚未添加到集合中

ws.delete(window); //從集合中删除視窗
ws.has(window); // false,視窗已被删除           

複制

Map

Map資料結構是一個簡單的鍵/值映射。解決了對象中隻能用字元串當鍵的限制(對象和原始值都可以用作鍵或值)。

方法 說明
size 傳回成員總數
set(key, value) 傳回整個Map結構。如果key已經有值,則鍵值會被更新,否則就新生成該鍵。
get(key) 讀取key對應的鍵值,如果找不到key,傳回undefined
has(key) 傳回一個布爾值,表示某個鍵是否在Map資料結構中
delete(key) 删除某個鍵,傳回true。如果删除失敗,傳回false
clear() 清除所有成員,沒有傳回值
keys() 傳回由key組成的新Iterator對象
values() 傳回由value組成的新Iterator對象
entries() 傳回由[key, value]組成的新Iterator對象
forEach() 周遊Map所有成員
let map = new Map();
map.set('stringkey', '123');
map.set('objectKey', {a: '123'});

for(let item of map.entries()) {
    console.log(item);
}           

複制

注意:

map.entries()

map

在疊代中效果一緻;差別是前者傳回新的Iterator對象(具有next()方法),而map隻是部署了Symbol.iterator接口所有可以周遊。通過

map[Symbol.iterator]()

可以擷取map的周遊器對象。

Map轉換

(1)Map轉為數組

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]           

複制

(2)數組轉為Map

new Map([[true, 7], [{foo: 3}, ['abc']]])           

複制

(3)Map轉為對象

function strMapToObj(strMap) {
  let obj = Object.create(null); // 建立空對象
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap);           

複制

(4)對象轉為Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}
objToStrMap({yes: true, no: false});           

複制

(5)Map轉為JSON

一種情況是,Map的鍵名都是字元串,這時可以選擇轉為對象JSON。

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap);           

複制

另一種情況是,Map的鍵名有非字元串,這時可以選擇轉為數組JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap);           

複制

(6)JSON轉為Map

鍵名都是字元串

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes":true,"no":false}');           

複制

WeakMap

WeakMap結果與Map結構基本一緻,差別在于WeakMap隻接受對象作為鍵名(null除外),而且其鍵名所指向的對象不計入垃圾回收機制(弱類型)。

十、Iterator和for…of循環

ES6之前表示“集合”的資料結構,主要是數組和對象,ES6中新增了Map和Set。需要一種統一的接口機制來處理所有不同的資料結構。周遊器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的資料結構提供統一的通路機制。

Iterator的作用有三個:一是為各種資料結構,提供一個統一的、簡便的通路接口;二是使得資料結構的成員能夠按某種次序排列;三是ES6創造了一種新的周遊指令for…of循環,Iterator接口主要供for…of消費。

function makeIterator(ary){
  var nextIndex = 0;
  return {
    next: function(){
      return nextIndex < ary.length ? 
        { value: ary[nextIndex++], done: false } :
        { value: undefined, done: true };
    }
  };
}
var it = makeIterator(["1", "2"]);
it.next(); // Object {value: "1", done: false}
it.next(); // Object {value: "2", done: false}
it.next(); // Object {value: undefined, done: true}           

複制

周遊器(Iterator)本質上是一個指針對象,指向目前資料結構的起始位置。第一次調用對象的next方法,指針指向資料結構的第一個成員;第二次調用next方法,指針指向資料結構的第二個成員;一次類推,直到指向資料結構的結束位置。

在ES6中,有些資料結構原生具備Iterator接口(比如數組),即不用任何處理,就可以被for…of循環周遊,有些就不行(比如對象)。原因在于,這些資料結構原生部署了Symbol.iterator屬性。在ES6中,有三類資料結構原生具備Iterator接口:數組、某些類似數組的對象、Set和Map結構。調用Symbol.iterator接口,就會傳回一個周遊器對象。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next();           

複制

調用Iterator接口的場景

一些場景會預設調用Iterator接口(即Symbol.iterator方法)

(1)解構指派

let set = new Set();
set.add(1).add(2);
let [x, y] = set;
console.log(x, y); // 1 2           

複制

(2)擴充運算符

let str = 'hello';
console.log([...str]); // ["h", "e", "l", "l", "o"]           

複制

(3)其他場合

由于數組的周遊都會調用周遊器接口,是以任何接受數組作為參數的場合其實都調用了周遊器接口。

  • for…of
  • Array.from(arguments)
  • Map([[‘a’, 1], [‘b’, 2]])、Set([1, 2, 3])、WeakMap()、WeakSet()
  • Promise.all()、Promise.race()

十一、Symbol

​ ES6引入了一種新的原始資料類型Symbol,表示獨一無二(互不等價)的值。Symbol出現之前,我們會經常遇到多個不相同的庫操作的DOM屬性相同,導緻第三方庫無法正常運作。Symbol解決了“對象屬性名都是字元串、數字,這容易造成屬性名的沖突”的問題。

Symbol([description]) // 并不是構造函數,不能使用new           

複制

​ 很多開發者會誤認為Symbol值與我們為其設定的描述有關,其實這個描述僅僅起到了描述的作用,而不會對Symbol值本身起到任何的改變作用。

let s = Symbol();
typeof s;   // "symbol"

const a = Symbol('123');
const b = Symbol('123');
console.log(a == b); // false           

複制

Symbol是一種值類型而非引用類型。其意味着将Symbol值作為函數形式傳遞時,将會進行複制值傳遞而非引用傳遞。同時需要注意:Symbol作為屬性名,該屬性不會出現在for…in、for…of循環中,也不會被

Object.keys()

Object.getOwnPropertyNames()

傳回。但是,它也不是私有屬性,有一個

Object.getOwnPropertySymbols

方法,可以擷取指定對象的所有Symbol屬性名。

var obj = {};
var a = Symbol('a');
var b = Symbol.for('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols; // [Symbol(a), Symbol(b)]           

複制

Symbol.for()

接受一個字元串作為參數,然後搜尋有沒有以該參數作為名稱的Symbol值。如果有,就傳回這個Symbol值,否則就建立并傳回一個以該字元串為名稱的Symbol值。

const a = Symbol.for('123');
const b = Symbol.for('123');
console.log(a === b); // true           

複制

Symbol.keyFor方法傳回一個已登記的Symbol類型值的key。

var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"           

複制

常用Symbol值

(1)Symbol.iterator

@@iterator

用于為對象定義一個方法并傳回一個屬于所對應對象的疊代器,該疊代器會被for-of語句使用。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }           

複制

class RangeIterator {
    constructor(start, stop) {
        this.start = start;
        this.stop = stop;
    }

    [Symbol.iterator]() {
        return this;
    }

    next() {
        if(this.start < this.stop) {
            this.start++;
            return {done: false, value: this.start};
        }else {
            return {done: true, value: undefined};
        }
    }
}

for(let index of new RangeIterator(0, 3)){
    console.log(index); // 1 2 3
}           

複制

(2)Symbol.hasInstance

對象的

Symbol.hasInstance

屬性,指向一個内部方法。當其他對象使用

instanceof

運算符,判斷是否為該對象的執行個體時,會調用這個方法。

class Even {
    static [Symbol.hasInstance](obj) {
        return Number(obj) % 2 === 0;
    }
}
1 instanceof Even; // false
2 instanceof Even; // true           

複制

(3)Symbol.match

對象的

Symbol.match

屬性,指向一個函數。當執行

str.match(myObject)

時,如果該屬性存在,會調用它,傳回該方法的傳回值。

"ligang".match(/g/) // ["g", index: 2, input: "ligang"]
// 等價于
(/g/)[Symbol.match]("ligang") // ["g", index: 2, input: "ligang"]           

複制

class Matcher {
    constructor(value){
        this.value = value;
    }
     [Symbol.match](str) {
        return this.value.indexOf(str);
    }
}
'g'.match(new Matcher('ligang')); // 2           

複制

(4)Symbol.replace

對象的

Symbol.replace

屬性,指向一個方法,當該對象被

String.prototype.replace

方法調用時,會傳回該方法的傳回值。

String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)           

複制

(5)Symbol.search

對象的

Symbol.search

屬性,指向一個方法,當該對象被

String.prototype.search

方法調用時,會傳回該方法的傳回值。

String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)           

複制

(6)Symbol.split

對象的

Symbol.split

屬性,指向一個方法,當該對象被

String.prototype.split

方法調用時,會傳回該方法的傳回值。

String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)           

複制

十二、Proxy

Proxy可以了解成,在目标對象之前架設一層“攔截”,外界對該對象的通路,都必須先通過這層攔截,是以提供了一種機制,可以對外界的通路進行過濾和改寫,屬于一種“元程式設計”。(注意,ES5無法模拟該特性)。

元程式設計重點在于:在一個程式的内容、運作環境、配置等都不做任何修改的情況下,可以通過其他程式對其進行讀取或修改。

Getter/Setter

ES5中,可以通過Getter/Setter定義其運作的邏輯或将要傳回的值。

var obj = {
    _name: '',
    prefix: '',
    get name() {
        return this.prefix + this._name;
    },
    set name(val) {
        this._name = val;
    }
};

obj.name = 'ligang';
console.log(obj.name); // 'ligang'
obj.prefix = 'hello ';
console.log(obj.name); // 'hello ligang'           

複制

Getter/Setter不能對變量的所有行為進行攔截,Proxy提供了這樣的能力。

Proxy

Proxy并不是以文法形成使用,而是一種包裝器的形式使用。

Syntax: new Proxy(target, handler)

  • target

    參數表示所要攔截的目标對象
  • handler

    參數也是一個對象,用來定制攔截行為
屬性鍵(監聽參數) 監聽内容
has(target, propKey) 監聽in語句的使用
get(target, propKey, receiver) 監聽目标對象的屬性讀取
set(target, propKey, value, receiver) 監聽目标對象的屬性指派
deleteProperty(target, propKey) 監聽delete語句對目标對象的删除屬性行為
ownKeys(target) 監聽Object.getOwnPropertyNames()的讀取
apply(target, object, args) 監聽目标函數的調用行為
construct(target, args) 監聽目标構造函數利用new而生成執行個體的行為
getPrototypeOf(target) 監聽Object.getPrototypeOf()的讀取
setPrototypeOf(target, proto) 監聽Object.setPrototypeOf()的調用
isExtensible(target) 監聽Object.isExtensible()的讀取
preventExtensions(target) 監聽Object.preventExtensions()的讀取
getOwnPropertyDescriptor(target, propKey) 監聽Object.getOwnPropertyDescriptor()的讀取
defineProperty(target, propKey) 監聽Object.defineProperty()的調用
let obj = {
    foo: 1,
    bar: 2
};           

複制

(1)has(target, propKey)

  • 當目标對象被Object.preventExtensions()禁用,該監聽方法不能傳回false;
  • 當屬性的configurable配置是false時,該監聽方法不能傳回false。
let p = new Proxy(obj, {
    has(target, propKey) {
        console.log(`檢查屬性${propKey}是否在目标對象中`);
        return propKey in target;
    }
});
'foo' in p;

Object.preventExtensions(obj);           

複制

Object.preventExtensions(obj);
let p2 = new Proxy(obj, {
    has(target, propKey) {
        console.log(`檢查屬性${propKey}是否在目标對象中`);
        return false;
    }
});
console.log('foo' in p2); // TypeError: 'has' on proxy: trap returned falsish for property 'foo' but the proxy target is not extensible           

複制

(2)get(target, propKey, receiver)

當目标對象被讀取的屬性的configurable和writable屬性為false時,監聽方法最後傳回值必須與目标對象的原屬性值一直。

(3)construct(target, args)

該監聽方法所傳回值必須是一個對象,否則會抛出一個TypeError錯誤。

解除Proxy對象

Proxy.revocable

方法傳回一個可取消的 Proxy 執行個體。

let {proxy, revoke} = Proxy.revocable(target, handler);           

複制

  • proxy為可解除Proxy對象
  • revoke為解除方法

Proxy.revocable

的一個使用場景是,目标對象不允許直接通路,必須通過代理通路,一旦通路結束,就收回代理權,不允許再次通路。

let target = {
  bar: 1
};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke(); // 當執行revoke函數之後,再通路Proxy執行個體,就會抛出一個錯誤
proxy.foo; // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.bar; // TypeError: Cannot perform 'get' on a proxy that has been revoked           

複制

Proxy使用場景

(1)對象屬性自動填充

const obj = {};
obj.foo.name = 'foo'; // Uncaught TypeError: Cannot set property 'name' of undefined

const p = new Proxy(obj, {
    get(target, propKey, receiver) {
        if(!(propKey in target)) {
            target[propKey] = obj; // 注意,obj為空
        }
        return target[propKey];
    }
});
p.bar.name = 'bar';           

複制

(2)隻讀視圖

const NOPE = () => {
    throw new Error('Cannot modify the readonly data');
};

function readonly(data) {
    return new Proxy(data, {
        set: NOPE,
        deleteProperty: NOPE,
        setPrototypeOf: NOPE,
        preventExtensions: NOPE,
        defineProperty: NOPE
    })
}
const data = {a: 1};
const readonlyData = readonly(data);

readonlyData.a = 2; // Error: Cannot modify the readonly data
delete readonlyData.a; // Error: Cannot modify the readonly data           

複制

(3)入侵式測試架構

通過Proxy在定義方法和邏輯代碼之間建立一個隔離層。

import api from './api'

/**
 * 對目标api進行包裝
 */
export default hook(api, {
  methodName(fn) {
    return function(...args) {
      console.time('methodName');
      return fn(...args).then((...args) => {
        console.timeEnd('methodName');
        return Promise.resolve(...args);
      });
    }
  }
})

/**
 * 對目标對象的get行為進行監聽和幹涉
 */
function hook(obj, cases) {
  return new Proxy(obj, {
    get(target, prop) {
      if((prop in cases) && (typeof target[prop] === 'function')) {
        const fn = target[prop];
        return cases[prop](fn);
      }
      return target[prop];
    }
  })
}           

複制

十三、 Reflect

Reflect對象的設計目的有這樣幾個:

  • 将Object對象的一些明顯屬于語言層面的方法,放到Reflect對象上。現階段,某些方法同時在Object和Reflect對象上部署,未來的新方法将隻部署在Reflect對象上。
  • 更加有用的傳回值,修改某些Object方法的傳回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會抛出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會傳回false。
  • 函數操作,讓Object操作都變成函數行為。某些Object操作是指令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為。
  • Reflect對象的方法與Proxy對象的方法一一對應,隻要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以友善地調用對應的Reflect方法,完成預設行為,作為修改行為的基礎。
var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});           

複制