Symbol类型
简介
在传统的JavaScript中,对象的属性名都是由字符串构成的。这样就会带来一个问题,假如一个对象继承了另一个对象的属性,又需要定义新的属性时,很容易造成属性名的冲突。
ES6引入了一种新的基本数据类型Symbol,它表示的是一个独一无二的值。至此JavaScript中就一共存在6种基本数据类型,分别是Undefined类型、Null类型、Boolean类型、String类型、Number类型、Symbol类型。
属性
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable
特性
1. Symbol值的唯一性
Symbol类型的功能类似于一种唯一标识性的ID,通过Symbol()函数来创建一个Symbol值。
let s = Symbol();
console.log(typeof s); // symbol
在Symbol()函数中可以传递一个字符串参数,表示对Symbol值的描述,主要是方便对不同Symbol值的区分。但是需要注意的是,由于Symbol值的唯一性,任何通过Symbol()函数创建的Symbol值都是不相同的,即使传递了相同的字符串。
const a = Symbol();
const b = Symbol();
const c = Symbol('one');
const d = Symbol('one');
console.log(a === b); // false
console.log(c === d); // false
2. 不能使用new操作符
Symbol函数并不是一个构造函数,因此不能通过new操作符创建Symbol值。
let s1 = new Symbol(); // TypeError: Symbol is not a constructor
3. 不能参与类型运算
Symbol值可以通过toString()函数显示地转换为字符串,但是本身不能参与其他类型值的运算,例如在对Symbol值进行字符串拼接操作时,会抛出异常。
let s4 = Symbol('hello');
s4.toString(); // Symbol(hello)
's4 content is: ' + s4; // TypeError: Cannot convert a Symbol value to a string
4. 可以使用同一个Symbol值
Symbol.for()函数,它接收一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
let s1 = Symbol.for('one');
let s2 = Symbol.for('one');
s1 === s2; // true
Symbol.for()函数与Symbol()函数这两种写法,都会生成新的Symbol值。它们的区别是,前者会被登记在全局环境中以供搜索,而后者不会。
用法
1. 用作对象属性名
由于每一个Symbol值都是不相等的,它会经常用作对象的属性名,尤其是当一个对象由多个模块组成时,这样能够避免属性名被覆盖的情况。遵循的一个原则就是为对象字面量新增属性时需要使用方括号[],不能通过点运算符为对象添加Symbol属性。因为通过点运算符添加的属性实际是一个字符串,并不是一个Symbol变量。
// 新增一个symbol属性
let PROP_NAME = Symbol();
// 第一种写法
let obj = {};
obj[PROP_NAME] = 'Hello';
// 第二种写法
let obj = {
[PROP_NAME]: 'Hello'
};
// 第三种写法
let obj = {};
Object.defineProperty(obj, PROP_NAME, {
value: 'Hello'
});
obj.PROP_NAME = 'Hello!'; // 这个PROP_NAME实际是一个字符串,并不是一个Symbol变量
console.log(obj[PROP_NAME]); // undefined
console.log(obj['PROP_NAME']); // 'Hello'
2. 用于属性名遍历
使用Symbol作为属性名时,不能通过Object.keys()函数、Object.getOwnPropertyNames()或者for…in来枚举,这样我们可以将一些不需要对外操作和访问的属性通过Symbol来定义。
let obj = {
[Symbol('name')]: 'Hello',
age: 18,
title: 'Engineer'
};
console.log(Object.keys(obj)); // ['age', 'title']
for (let p in obj) {
console.log(p); // 分别会输出:'age' 和 'title'
}
console.log(Object.getOwnPropertyNames(obj)); // ['age', 'title']
因为Symbol属性不会出现在属性遍历的过程中,所以在使用JSON.stringify()函数将对象转换为JSON字符串时,Symbol值也不会出现在结果中。
JSON.stringify(obj); // {"age":18,"title":"Engineer"}
当需要获取Symbol属性时,可以使用专门针对Symbol的API。
// 使用Object的API
Object.getOwnPropertySymbols(obj); // [Symbol(name)]
// 使用新增的反射API
Reflect.ownKeys(obj); // [Symbol(name), 'age', 'title']
3. 用于属性区分
// 求图形的面积
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'triangle':
area = .5 * options.width * options.height;
break;
case 'rectangle':
area = options.width * options.height;
break;
}
return area;
}
console.log(getArea('triangle', { width: 100, height: 100 })); // 5000
console.log(getArea('rectangle', { width: 100, height: 100 })); // 10000
// 字符串'triangle'和'rectangle'会强耦合在代码中。而事实上仅想区分各种不同的形状,并不关心每个形状使用什么字符串表示,只需要知道每个变量的值是独一无二的即可,此时使用Symbol就会很合适。
// 事先声明两个Symbol值,用于作判断
let shapeType = {
triangle: Symbol('triangle'),
rectangle: Symbol('rectangle')
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
case shapeType.rectangle:
area = options.width * options.height;
break;
}
return area;
}
console.log(getArea(shapeType.triangle, { width: 100, height: 100 })); // 5000
console.log(getArea(shapeType.rectangle, { width: 100, height: 100 })); // 10000
Set数据结构
简介
ES6中新增了一种数据结构Set,表示的是一组数据的集合,类数组,但是Set的成员值都是唯一的,没有重复。Set本身是一个构造函数,可以接收一个数组或者类数组对象作为参数。
属性
- Set.prototype.constructor:构造函数,默认就是Set函数。
-
函数
Set.prototype.add(value):添加一个值,返回Set结构本身。
- Set.prototype.delete(value):删除某个值,返回布尔值。
- Set.prototype.has(value):是否是成员,返回布尔值。
- Set.prototype.clear():清除所有成员,无返回值。
需要注意的是,向Set实例中添加新的值时,不会发生类型转换。这可以理解为使用add()函数添加新值时,新值与Set实例中原有值是采用严格相等(===)进行比较的,只有在严格相等的比较结果为不相等时,才会将新值添加到Set实例中。
let set = new Set();
set.add(1);
set.add('1');
console.log(set); // Set { 1, '1' }
// 但是NaN是一个特例,NaN与NaN在进行严格相等的比较时是不相等的,但是在Set内部,NaN与NaN是严格相等的,因此一个Set实例中只可以添加一个NaN。
let set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set); // Set { NaN }
用法
(1)单一数组的去重
由于Set成员值具有唯一性,因此可以使用Set来进行数组的去重。
let arr = [1, 3, 4, 2, 3, 2, 5];
console.log(new Set(arr)); // Set { 1, 3, 4, 2, 5 }
(2)多个数组的合并去重
Set可以用于单个数组的去重,也可以用于多个数组的合并去重。实现方法是先使用扩展运算符将多个数组处理成一个数组,然后将合并后得到的数组传递给Set构造函数。
let arr1 = [1, 2, 3, 4];
let arr2 = [2, 3, 4, 5, 6];
let set1 = new Set([...arr1, ...arr2]);
console.log(set1); // Set { 1, 2, 3, 4, 5, 6 }
(3)Set与数组的转换
数组转换为Set:调用Set的构造函数即可;
Set转换为数组:通过Array.from()函数或者扩展运算符即可。
let arr = [1, 3, 5, 7];
// 将数组转换为Set
let set = new Set(arr);
console.log(set); // Set { 1, 3, 5, 7 }
let set = new Set();
set.add('a');
set.add('b');
// 将Set转换为数组,通过Array.from()函数
let arr = Array.from(set);
console.log(arr); // [ 'a', 'b' ]
// 将Set转换为数组,通过扩展运算符
let arr2 = [...set];
console.log(arr2); // [ 'a', 'b' ]
遍历
- forEach():第一个参数表示的是Set中的每个元素,第二个参数表示的是元素的索引,从0开始。而在Set中没有索引的概念,它实际是键和值相同的集合,第二个参数表示的是键,实际与第一个参数相同,也返回数据值本身。
- keys():返回键名的遍历器对象Iterator。
- values():返回键值的遍历器对象Iterator。Set的键值是相等的,所以keys()函数和values()函数实际返回的是相同的值。
- entries():返回键值对的遍历器对象Iterator。通过for…of循环可以获取每一项的值。
- 也可以直接用for…of遍历 ```javascript let set = new Set([4, 5, ‘hello’]);
// forEach() set.forEach((item, index) => { console.log(item, index); }); // 4 4 // 5 5 // hello hello
// keys() for (let item of set.keys()) { console.log(item); } // 4 // 5 // hello
// values() for (let item of set.values()) { console.log(item); } // 4 // 5 // hello
// entries() for (let item of set.entries()) { console.log(item); } // [4, 4] // [5, 5] // [‘hello’, ‘hello’]
for (let item of set) { // 不能 let [k,v] of set console.log(item); } // 4 // 5 // hello
<a name="SDF2w"></a>
## Map数据结构
<a name="E9lOf"></a>
### 简介
ES6还增加了另一种数据结构Map,**类对象**,它的本质是一种键值对的组合。<br />但是与对象字面量不同的是,**对象字面量的键只能是字符串,对于非字符串类型的值会采用强制类型转换成字符串**,而**Map的键却可以由各种类型的值组成**。
```javascript
const data = {};
const map = new Map();
const element = document.getElementById('home');
// 传统的对象类型
data[element] = 'first';
console.log(data); // {[object HTMLDivElement]: "first"} 键的值被转换成字符串
// Map
map.set(element, 'first');
console.log(map); // {div#home => "first"} 键的值为DOM元素的真实值,并没有转换为字符串的值。
Map本身是一个构造函数,可以接收一个数组作为参数,数组的每个元素同样是一个子数组,子数组元素表示的是键和值。
const map = new Map([
['name', 'kingx'],
['age', 123]
]);
console.log(map); // Map { 'name' => 'kingx', 'age' => 123 }
属性函数
- size属性:返回Map结构的成员总数。
- set(key, value):设置键值,set()函数返回的是当前Map对象,因此set()函数可以采用链式调用的写法。
- get(key):读取key对应的键值,如果找不到key,返回“undefined”。
- has(key):表示某个键是否在当前Map对象中,返回一个布尔值
- delete(key):删除某个键,返回布尔值。
- clear():清除所有成员,没有返回值。
类似于Set数据结构的元素值唯一性,在Map数据结构中,所有的键都必须具有唯一性。如果对同一个键进行多次赋值,那么后面的值会覆盖前面的值。
如果Map实例的键是基本数据类型,则采用严格相等判断是否为同一个键(NaN除外)。如果Map实例的键是引用数据类型,则需要判断对象是否为同一个引用、是否占据同一个内存地址。
new Map().set(1, 'aaa').set(1, 'bbb');.get(1); // "bbb"
// 对于Number类型数据,+0和-0严格相等
new Map().set(-0, 123).get(+0); // 123
// 字符串'true'与Boolean类型true不严格相等,是两个不同的键。
new Map().set(true, 1).set('true', 2).get(true); // 1
// 对于Undefined类型和Null类型,undefined与null也是两个不同的键。
let map = new Map();
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined); // 3
map.get(null); // 4
// NaN与NaN不严格相等,但是Map会将其视为一个相同的键。
new Map().set(NaN, 123).set(NaN, 234).get(NaN); // 234
const map = new Map();
// 将数组[0]作为map的键,[0]作为引用类型数据,每次生成一个新的值都会占据新的内存地址,实际为不同的键
map.set([0], '0');
map.set([0], '1');
console.log(map); // Map { [ 0 ] => '0', [ 0 ] => '1' }
let arr = [0];
const map = new Map();
// arr对应的值[0]被两次添加至map中,但是实际指向的是同一个引用,在内存中占据同一个地址,因此后面的值会覆盖前一个值
map.set(arr, '0');
map.set(arr, '1');
console.log(map); // Map { [ 0 ] => '1' }
遍历
与Set一样,Map的遍历同样可以采用4种函数:
- forEach():第一个参数表示的是值,第二个参数表示的是键
- keys():返回的是键的集合
- values():返回的是值的集合
- entries():返回的键值对的集合,这些集合都是Iterator的实例,可以通过for…of进行遍历。
- 也可以直接用for…of进行遍历 ```javascript const map = new Map(); map.set(‘name’, ‘kingx’); map.set(‘age’, 12);
map.forEach(function (item, key) { console.log(item, key); }); // kingx name // 12 age
for (let key of map.keys()) { console.log(key); } // name // age
for (let value of map.values()) { console.log(value); } // kingx // 12
for (let obj of map.entries()) { console.log(obj); } // [ ‘name’, ‘kingx’ ] // [ ‘age’, 12 ]
for (let [k,v] of map) { console.log(k,v) } // name kingx // age 12
for (let item of map) { console.log(k,v) } // [ ‘name’, ‘kingx’ ] // [ ‘age’, 12 ]
<a name="IaKK9"></a>
## weakMap
Map是强引用,weakMap是弱引用,并且key值必须是对象!
```javascript
let obj = {}
let map = new Map([[obj, 1]])
obj = null // 这里把obj设为null,但是obj并没有被清空,因为它的引用一直在map那里!
console.log(map) // Map(1) {{…} => 1} obj的引用一直存在,没有被垃圾回收!!
let obj = {}
let weakmap = new WeakMap([[obj, 2]])
obj = null
console.log(weakmap) // WeakMap {} 不会马上回收,会在合适的被回收掉
数据结构的转换
Map转换为数组
const map = new Map();
map.set('name', 'kingx');
map.set('age', 12);
// 可以通过扩展运算符实现
const arr = [...map];
console.log(arr); // [ [ 'name', 'kingx' ], [ 'age', 12 ] ]
console.log(arr.flat()); // ['name', 'kingx', 'age', 12]
数组转换为Map
// 可以通过new Map()构造函数实现
const arr = [[ 'name', 'kingx' ], [ 'age', 12 ]];
const map = new Map(arr);
console.log(map); // Map { 'name' => 'kingx', 'age' => 12 }
Map转换为对象
// 如果键是字符串,可以直接转换;不是则会先转换成字符串然后再进行转换。
function mapToObj(map) {
let obj = {};
for(let [key, value] of map) {
obj[key] = value;
}
return obj;
}
console.log(mapToObj(map)); // { name: 'kingx', age: 12 }
或者使用ES10新增的Object.fromEntries
方法:
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
对象转换为Map
// 只需要遍历对象的属性并通过set()函数添加到Map的实例中即可
function objToMap(obj) {
let map = new Map();
for (let k of Object.keys(obj)) {
map.set(k, obj[k]);
}
return map;
}
console.log(objToMap({yes: true, no: false}));
// Map {"yes" => true, "no" => false}
Map转换为JSON字符串
//第一种是当Map的键名都是字符串时,可以先将Map转换为对象,然后调用JSON.stringify()函数。
function mapToJson(strMap) {
// 先将map转换为对象,然后转换为JSON
return JSON.stringify(mapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
console.log(mapToJson(myMap)); // {"yes":true,"no":false}
// 第二种是当Map的键名有非字符串时,可以先将Map转换为数组,然后调用JSON.stringify()函数。
function mapToArrayJson(map) {
// 先通过扩展运算符转换为数组,再转换为JSON
return JSON.stringify([...map]);
}
let myMap2 = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap2); // [[true,7],[{"foo":3},["abc"]]]
JSON转换为Map
// JSON.parse()==> 对象 ==> Map。
function jsonToMap(jsonStr) {
// 先转换为JSON对象,再转换为Map
return objToMap(JSON.parse(jsonStr));
}
jsonToMap('{"yes": true, "no": false}'); // Map { 'yes' => true, 'no' => false }
Set转换为Map
// Set中以数组形式存在的数据可以直接通过Map的构造函数转换为Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
console.log(new Map(set)); // Map { 'foo' => 1, 'bar' => 2 }
Map转换为Set
// 遍历Map本身获取到的键和值构成一个数组,然后通过add()函数添加至set实例中。
function mapToSet(map) {
let set = new Set();
for (let [k,v] of map) {
set.add([k, v])
}
return set;
}
const map = new Map().set('yes', true).set('no', false);
mapToSet(map); // Set { [ 'yes', true ], [ 'no', false ] }