ECMAScript 6 入门笔记(七)Symbol,set和map

时间:2022-08-27 13:11:48

ECMAScript 6 入门原文 – 阮一峰
ECMAScript 6 入门笔记(一)let,const,解构
ECMAScript 6 入门笔记(二)String,RegExp
ECMAScript 6 入门笔记(三)数值,Array
ECMAScript 6 入门笔记(四)函数,对象

ECMAScript 6 入门笔记(五)异步promise,Generator,async
ECMAScript 6 入门笔记(六)Class
ECMAScript 6 入门笔记(七)Symbol,set和map
ECMAScript 6 入门笔记(八)Proxy,Reflect

Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

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

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)

// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');

s1 === s2 // false

var sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string

var sym = Symbol();
Boolean(sym) // true
!sym // false

if (sym) {
// ...
}

Number(sym) // TypeError
sym + 2 // TypeError

作为属性名的Symbol

var mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

注意,Symbol值作为对象属性名时,不能用点运算符。

var mySymbol = Symbol();
var a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

属性名遍历
Symbol作为属性名,改属性不会被for…in,for…of循环中,也不会被Object.keys() , Object.getOwnPropertyNames(),JSON.stringify()返回,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名

var obj = {};
var a = Symbol('a');
var b = Symbol('b');
obj[a] = 'hello';
obj[b] = 'world';

var objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols //[Symbol(a),Symbol(b)];

for…in和Object.getOwnPropertyNames方法进行对比的例子。

var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
for (var i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

Symbol.hasInstance
对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是

Foo[Symbol.hasInstance](foo)。

class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}

[1, 2, 3] instanceof new MyClass() // true

Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象使用Array.prototype.concat()时,是否可以展开。

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']

对于一个类来说,Symbol.isConcatSpreadable属性必须写成实例的属性。

class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = false;
}
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]

SET和Map数据结构

ES6提供了新的数据结构Set,他类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成Set数据结构

const s = new Set();
[2,3,5,4,5,2,2].forEach( x => s.add(x));

for(let i of s){
console.log(i);
}
//2,3,5,4

Set 函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。

// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5

// 例三
function divs () {
return [...document.querySelectorAll('div')];
}


const set = new Set(divs());

set.size // 56

// 类似于
divs().forEach(div => set.add(div));
set.size // 56

Set 实例的属性和方法

Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。
上面这些属性和方法的实例如下。

s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

Array.from方法可以将 Set 结构转为数组。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);


这就提供了去除数组重复成员的另一种方法。

function dedupe(array) {
return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]

遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。

keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。

(1)keys(),values(),entries()

keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

let set = new Set([‘red’, ‘green’, ‘blue’]);

for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
console.log(item);
}
// [“red”, “red”]
// [“green”, “green”]
// [“blue”, “blue”]
上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。

Set.prototype[Symbol.iterator] === Set.prototype.values
// true
这意味着,可以省略values方法,直接用for…of循环遍历 Set。

let set = new Set([‘red’, ‘green’, ‘blue’]);

for (let x of set) {
console.log(x);
}
// red
// green
// blue
(2)forEach()

Set结构的实例的forEach方法,用于对每个成员执行某种操作,没有返回值。

let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6

(3)遍历的应用
扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。
let set = new Set([‘red’, ‘green’, ‘blue’]);
let arr = […set];
// [‘red’, ‘green’, ‘blue’]
扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。

let set = new Set([1, 2, 3]);
set = new Set([…set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([…set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}

因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

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 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。

WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

WeakSet是一个构造函数,可以使用new命令,来创建WeakSet数据结构
const a = [[1,2],[3,4]];
const ws = new WeakSet(a);
// WeakSet {[1,2],[3,4]}

WeakSet结构有以下三个方法
WeakSet.prototype.add(value):向weakset实例添加一个新成员。
WeakSet.prototype.delete(value):清除WeakSet实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例中

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

ws.add(window);
ws.add(obj);
ws.has(window); //true
ws.has(foo); //false
ws.delete(window);
ws.has(window); //false

weakset没有size属性,没有办法遍历它的变量
ws.size //undefined
ws.forEach //undefined

weakset不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员那就取不到了.weakset的一个用处,是储存dom节点,而不用担心这些节点从文档移除时,内存泄漏(WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。)

MAP
JavaScript的对象(object),本质上是键值对的集合,但是传统上只能用字符串当作键,带来很大的限制
const data = {};
const element = document.getElementById(“myDiv”);

data[element] = ‘metadata’;
data[‘[object HTMLDivElement]’] //metadata
上面是es5的代码

es6提供了Map数据结构,他类似对象,也是键值对的集合,但是键的范围不限于字符串,各种类型都能用来做键

const m = new Map(); 
const o = {p:'hello world'}

m.set(o,'content');
m.get(o); //content

m.has(o); //true
m.delete(o); //true
m.has(o); //false

Map添加成员,作为构造函数,Map也可以接收一个数组作为参数.
const map = new Map([
['name','张三'],
['title','Auther']
]);

map.size //2
map.has('name') //true
map.get('name') //张三

任何具有Iterator接口的数据结构

const set = new Set([
['foo',1],
['bar',2]
]);
cosnt m1 = new Map(set);
m1.get('foo'); //1
const m2 = new Map([['bar',3]]);
const m3 = new Map(m2);
m3.get('baz'); //3

const map = new Map();

map.set(['a'],555);
map.get(['a']);
const k1 = ['a'];
const k2 = ['a'];
map.set(k1,111).set(k2,222);
map.get(k1); //111
map.get(k2); //222

map的键实际上是跟内存地址绑定的,只要内存地址不一样,就是为两个值,解决了同名属性碰撞的问题

实例的属性和操作方法
(1)size属性
const map = new Map();
map.set(‘foo’,true);
map.set(‘bar’,false);
map.size; //2

(2)set(key,value)
const m = new Map();
m.set(‘edition’,6);
m.set(262,’standrad’);
m.set(undefined,’nah’);

(3)get(key)
m.get(‘edition’);

(4)has(key)
m.has(‘edition’);

(5)delete(key)
m.delete(‘edition’);

(6)clear
清除所有成员没有返回值
let map = new Map();
map.set(‘foo’,true);
map.set(‘bar’,false);

map.size; //2
map.clear();
map.size; //0

遍历方式
Map结构原生提供三个遍历器生成函数和一个遍历方法
keys 返回键名的遍历器
values 返回键值的遍历器
entries 返回所有成员的遍历器
forEach 遍历Map的所有成员

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);
}

for(let item of map.entries()){
console.log(item[0],item[1]);
}
// "F" "no"
// "T" "yes"

for(let [key,value] of map.entries()){
console.log(key,value);
}

for(let [key,value] of map){
console.log(key,value);
}


Map结构转换为数组结构,比较快速的方法是使用扩展运算符
const map = new Map([
[1,'one'],
[2,'two'],
[3,'three']
]);

[...map.keys()] //[1,2,3];
[...map.valus()] //['one','two','three'];
[...map.entries()] //[[1,'one'],[2,'two'],[3,'three']];
[...map] //[[1,'one'],[2,'two'],[3,'three']];

与其他数据结构的相互转换
(1)Map转换为数组
const myMap = new Map().set(true,7).set({foo,3},[‘abc’]);
[…myMap]; //[[true,7],[{foo:3},[‘abc’]]];

(2)数组转换为Map
new Map([
[true,7],
[{foo:3},[‘abc’]]
]);

(3)Map转为对象
如果所有Map的键都是字符串,它可以转为对象
function strMapToObj(strMap){
let obj = Object.create(null);
for(let [k,v] of strMap){
obj[k] = v;
}
return obj;
}

const myMap = new Map().set(‘yes’,true).set(‘no’,false);
strMapToObj(myMap);
// {yes: true,no: false}

(4)对象转为Map
function objToStrMap(obj){
let strMap = new Map();
for(let k of Object.keys(obj)){
strMap.set(k,obj[k]);
}
return strMap;
}

(5)Map转换为JSON
Map转为JSON要区分两种情况.一种情况是,Map的键名都是字符串,这时可以选择转换为对象JSON
function strMapToJson(strMap){
return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set(true, 7).set({foo: 3}, [‘abc’]);
mapToArrayJson(myMap)
// ‘[[true,7],[{“foo”:3},[“abc”]]]’

(6)JSON转为Map
function jsonToStrMap(jsonstring){
return JSON.parse(jsonStr);
}

jsonToStrMap(‘{“yes”: true, “no”: false}’)
// Map {‘yes’ => true, ‘no’ => false}

4.WeakMap
含义WeakMap结构与Map结构类似,也是用于生成键值对
const wm1 = new WeakMap();
const key = {foo:1};
wm1.set(key,2);
wm1.get(key);