版權聲明:本文為部落客原創文章,遵循 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);
}
});
複制