9 Symbol
#9.1 介绍
ES6引入Symbol
作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突。
ES6之后,JavaScript一共有其中数据类型:Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。
简单实用:
let a = Symbol();
typeof a; // "symbol"
注意:
-
Symbol
函数不能用new
,会报错。由于Symbol
是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。 -
Symbol
都是不相等的,即使参数相同。
// 没有参数
let a1 = Symbol();
let a2 = Symbol();
a1 === a2; // false
// 有参数
let a1 = Symbol('abc');
let a2 = Symbol('abc');
a1 === a2; // false
-
Symbol
不能与其他类型的值计算,会报错。
let a = Symbol('hello');
a + " world!"; // 报错
`${a} world!`; // 报错
Symbol可以显式转换为字符串:
let a1 = Symbol('hello');
String(a1); // "Symbol(hello)"
a1.toString(); // "Symbol(hello)"
Symbol可以转换为布尔值,但不能转为数值:
let a1 = Symbol();
Boolean(a1);
!a1; // false
Number(a1); // TypeError
a1 + 1 ; // TypeError
#9.2 Symbol作为属性名
好处:防止同名属性,还有防止键被改写或覆盖。
let a1 = Symbol();
// 写法1
let b = {};
b[a1] = 'hello';
// 写法2
let b = {
[a1] : 'hello'
}
// 写法3
let b = {};
Object.defineProperty(b, a1, {value : 'hello' });
// 3种写法 结果相同
b[a1]; // 'hello'
需要注意: Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。
let a = Symbol();
let b = {};
// 不能用点运算
b.a = 'hello';
b[a] ; // undefined
b['a'] ; // 'hello'
// 必须放在方括号内
let c = {
[a] : function (text){
console.log(text);
}
}
c[a]('leo'); // 'leo'
// 上面等价于 更简洁
let c = {
[a](text){
console.log(text);
}
}
常常还用于创建一组常量,保证所有值不相等:
let a = {};
a.a1 = {
AAA: Symbol('aaa'),
BBB: Symbol('bbb'),
CCC: Symbol('ccc')
}
#9.3 应用:消除魔术字符串
魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。
function f(a){
if(a == 'leo') {
console.log('hello');
}
}
f('leo'); // 'leo' 为魔术字符串
常使用变量,消除魔术字符串:
let obj = {
name: 'leo'
};
function f (a){
if(a == obj.name){
console.log('hello');
}
}
f(obj.name); // 'leo'
使用Symbol消除强耦合,使得不需关系具体的值:
let obj = {
name: Symbol()
};
function f (a){
if(a == obj.name){
console.log('hello');
}
}
f(obj.name);
#9.4 属性名遍历
Symbol作为属性名遍历,不出现在for...in
、for...of
循环,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
let a = Symbol('aa'),b= Symbol('bb');
let obj = {
[a]:'11', [b]:'22'
}
for(let k of Object.values(obj)){console.log(k)}
// 无输出
let obj = {};
let aa = Symbol('leo');
Object.defineProperty(obj, aa, {value: 'hi'});
for(let k in obj){
console.log(k); // 无输出
}
Object.getOwnPropertyNames(obj); // []
Object.getOwnPropertySymbols(obj); // [Symbol(leo)]
Object.getOwnPropertySymbols
方法返回一个数组,包含当前对象所有用做属性名的Symbol值。
let a = {};
let a1 = Symbol('a');
let a2 = Symbol('b');
a[a1] = 'hi';
a[a2] = 'oi';
let obj = Object.getOwnPropertySymbols(a);
obj; // [Symbol(a), Symbol(b)]
另外可以使用Reflect.ownKeys
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let a = {
[Symbol('leo')]: 1,
aa : 2,
bb : 3,
}
Reflect.ownKeys(a); // ['aa', 'bb',Symbol('leo')]
由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。
#9.5 Symbol.for()、Symbol.keyFor()
- Symbol.for()
用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。
let a = Symbol.for('aaa');
let b = Symbol.for('aaa');
a === b; // true
Symbol()
和 Symbol.for()
区别:
Symbol.for('aa') === Symbol.for('aa'); // true
Symbol('aa') === Symbol('aa'); // false
- Symbol.keyFor()
用于返回一个已使用的Symbol类型的key:
let a = Symbol.for('aa');
Symbol.keyFor(a); // 'aa'
let b = Symbol('aa');
Symbol.keyFor(b); // undefined
#9.6 内置的Symbol值
ES6提供11个内置的Symbol值,指向语言内部使用的方法:
-
1.Symbol.hasInstance
当其他对象使用instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。
class P {
[Symbol.hasInstance](a){
return a instanceof Array;
}
}
[1, 2, 3] instanceof new P(); // true
P是一个类,new P()会返回一个实例,该实例的Symbol.hasInstance
方法,会在进行instanceof
运算时自动调用,判断左侧的运算子是否为Array
的实例。
-
2.Symbol.isConcatSpreadable
值为布尔值,表示该对象用于Array.prototype.concat()
时,是否可以展开。
let a = ['aa','bb'];
['cc','dd'].concat(a, 'ee');
// ['cc', 'dd', 'aa', 'bb', 'ee']
a[Symbol.isConcatSpreadable]; // undefined
let b = ['aa','bb'];
b[Symbol.isConcatSpreadable] = false;
['cc','dd'].concat(b, 'ee');
// ['cc', 'dd',[ 'aa', 'bb'], 'ee']
-
3.Symbol.species
指向一个构造函数,在创建衍生对象时会使用,使用时需要用get
取值器。
class P extends Array {
static get [Symbol.species](){
return this;
}
}
解决下面问题:
// 问题: b应该是 Array 的实例,实际上是 P 的实例
class P extends Array{}
let a = new P(1,2,3);
let b = a.map(x => x);
b instanceof Array; // true
b instanceof P; // true
// 解决: 通过使用 Symbol.species
class P extends Array {
static get [Symbol.species]() { return Array; }
}
let a = new P();
let b = a.map(x => x);
b instanceof P; // false
b instanceof Array; // true
-
4.Symbol.match
当执行str.match(myObject)
,传入的属性存在时会调用,并返回该方法的返回值。
class P {
[Symbol.match](string){
return 'hello world'.indexOf(string);
}
}
'h'.match(new P()); // 0
-
5.Symbol.replace 当该对象被
String.prototype.replace
方法调用时,会返回该方法的返回值。
let a = {};
a[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(a , 'World') // ["Hello", "World"]
-
6.Symbol.hasInstance
当该对象被String.prototype.search
方法调用时,会返回该方法的返回值。
class P {
constructor(val) {
this.val = val;
}
[Symbol.search](s){
return s.indexOf(this.val);
}
}
'hileo'.search(new P('leo')); // 2
-
7.Symbol.split
当该对象被String.prototype.split
方法调用时,会返回该方法的返回值。
// 重新定义了字符串对象的split方法的行为
class P {
constructor(val) {
this.val = val;
}
[Symbol.split](s) {
let i = s.indexOf(this.val);
if(i == -1) return s;
return [
s.substr(0, i),
s.substr(i + this.val.length)
]
}
}
'helloworld'.split(new P('hello')); // ["hello", ""]
'helloworld'.split(new P('world')); // ["", "world"]
'helloworld'.split(new P('leo')); // "helloworld"
-
8.Symbol.iterator
对象进行for...of
循环时,会调用Symbol.iterator
方法,返回该对象的默认遍历器。
class P {
*[Symbol.interator]() {
let i = 0;
while(this[i] !== undefined ) {
yield this[i];
++i;
}
}
}
let a = new P();
a[0] = 1;
a[1] = 2;
for (let k of a){
console.log(k);
}
-
9.Symbol.toPrimitive
该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。调用时,需要接收一个字符串参数,表示当前运算模式,运算模式有:- Number : 此时需要转换成数值
- String : 此时需要转换成字符串
- Default : 此时可以转换成数值或字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
-
10.Symbol.toStringTag
在该对象上面调用Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object
]或[object Array]
中object
后面的那个字符串。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
-
11.Symbol.unscopables
该对象指定了使用with关键字时,哪些属性会被with环境排除。
// 没有 unscopables 时
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
上面代码通过指定Symbol.unscopables
属性,使得with
语法块不会在当前作用域寻找foo
属性,即foo
将指向外层作用域的变量。
10 Set和Map数据结构
#10.1 Set
介绍:
Set
数据结构类似数组,但所有成员的值唯一。
Set
本身为一个构造函数,用来生成Set
数据结构,使用add
方法来添加新成员。
let a = new Set();
[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x));
for(let k of a){
console.log(k)
};
// 1 2 3 4 5
基础使用:
let a = new Set([1,2,3,3,4]);
[...a]; // [1,2,3,4]
a.size; // 4
// 数组去重
[...new Set([1,2,3,4,4,4])];// [1,2,3,4]
注意:
- 向
Set
中添加值的时候,不会类型转换,即5
和'5'
是不同的。
[...new Set([5,'5'])]; // [5, "5"]
属性和方法:
-
属性:
-
Set.prototype.constructor
:构造函数,默认就是Set
函数。 -
Set.prototype.size
:返回Set
实例的成员总数。
-
-
操作方法:
-
add(value)
:添加某个值,返回 Set 结构本身。 -
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。 -
has(value)
:返回一个布尔值,表示该值是否为Set的成员。 -
clear()
:清除所有成员,没有返回值。
-
let a = new Set();
a.add(1).add(2); // a => Set(2) {1, 2}
a.has(2); // true
a.has(3); // false
a.delete(2); // true a => Set(1) {1}
a.clear(); // a => Set(0) {}
数组去重:
let a = new Set([1,2,3,3,3,3]);
#10.2 Set的应用
数组去重:
// 方法1
[...new Set([1,2,3,4,4,4])]; // [1,2,3,4]
// 方法2
Array.from(new Set([1,2,3,4,4,4])); // [1,2,3,4]
遍历和过滤:
let a = new Set([1,2,3,4]);
// map 遍历操作
let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8}
// filter 过滤操作
let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4}
获取并集、交集和差集:
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
// 并集
let c1 = new Set([...a, ...b]); // Set {1,2,3,4}
// 交集
let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3}
// 差集
let c3 = new Set([...a].filter(x => !b.has(x))); // set {1}
- 遍历方法:
-
keys()
:返回键名的遍历器。 -
values()
:返回键值的遍历器。 -
entries()
:返回键值对的遍历器。 -
forEach()
:使用回调函数遍历每个成员。
-
Set
遍历顺序是插入顺序,当保存多个回调函数,只需按照顺序调用。但由于Set
结构没有键名只有键值,所以keys()
和values()
是返回结果相同。
let a = new Set(['a','b','c']);
for(let i of a.keys()){console.log(i)}; // 'a' 'b' 'c'
for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c'
for(let i of a.entries()){console.log(i)};
// ['a','a'] ['b','b'] ['c','c']
并且 还可以使用for...of
直接遍历Set
。
let a = new Set(['a','b','c']);
for(let k of a){console.log(k)}; // 'a' 'b' 'c'
forEach
与数组相同,对每个成员执行操作,且无返回值。
let a = new Set(['a','b','c']);
a.forEach((v,k) => console.log(k + ' : ' + v));
#10.3 Map
由于传统的JavaScript
对象只能用字符串当做键,给开发带来很大限制,ES6增加Map
数据结构,使得各种类型的值(包括对象)都可以作为键。
Map
结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。 基础使用:
let a = new Map();
let b = {name: 'leo' };
a.set(b,'my name'); // 添加值
a.get(b); // 获取值
a.size; // 获取总数
a.has(b); // 查询是否存在
a.delete(b); // 删除一个值
a.clear(); // 清空所有成员 无返回
注意:
- 传入数组作为参数,指定键值对的数组。
let a = new Map([
['name','leo'],
['age',18]
])
- 如果对同一个键多次赋值,后面的值将覆盖前面的值。
let a = new Map();
a.set(1,'aaa').set(1,'bbb');
a.get(1); // 'bbb'
- 如果读取一个未知的键,则返回
undefined
。
new Map().get('abcdef'); // undefined
- 同样的值的两个实例,在 Map 结构中被视为两个键。
let a = new Map();
let a1 = ['aaa'];
let a2 = ['aaa'];
a.set(a1,111).set(a2,222);
a.get(a1); // 111
a.get(a2); // 222
遍历方法: Map 的遍历顺序就是插入顺序。
-
keys()
:返回键名的遍历器。 -
values()
:返回键值的遍历器。 -
entries()
:返回所有成员的遍历器。 -
forEach()
:遍历 Map 的所有成员。
let a = new Map([
['name','leo'],
['age',18]
])
for (let i of a.keys()){...};
for (let i of a.values()){...};
for (let i of a.entries()){...};
a.forEach((v,k,m)=>{
console.log(`key:${k},value:${v},map:${m}`)
})
将Map结构转成数组结构:
let a = new Map([
['name','leo'],
['age',18]
])
let a1 = [...a.keys()]; // a1 => ["name", "age"]
let a2 = [...a.values()]; // a2 => ["leo", 18]
let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]]
#10.4 Map与其他数据结构互相转换
- Map 转 数组
let a = new Map().set(true,1).set({f:2},['abc']);
[...a]; // [[true:1], [ {f:2},['abc'] ]]
- 数组 转 Map
let a = [ ['name','leo'], [1, 'hi' ]]
let b = new Map(a);
- Map 转 对象 如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function fun(s) {
let obj = Object.create(null);
for (let [k,v] of s) {
obj[k] = v;
}
return obj;
}
const a = new Map().set('yes', true).set('no', false);
fun(a)
// { yes: true, no: false }
- 对象 转 Map
function fun(obj) {
let a = new Map();
for (let k of Object.keys(obj)) {
a.set(k, obj[k]);
}
return a;
}
fun({yes: true, no: false})
// Map {"yes" => true, "no" => false}
- Map 转 JSON
(1)Map键名都是字符串,转为对象JSON:
function fun (s) {
let obj = Object.create(null);
for (let [k,v] of s) {
obj[k] = v;
}
return JSON.stringify(obj)
}
let a = new Map().set('yes', true).set('no', false);
fun(a);
// '{"yes":true,"no":false}'
(2)Map键名有非字符串,转为数组JSON:
function fun (map) {
return JSON.stringify([...map]);
}
let a = new Map().set(true, 7).set({foo: 3}, ['abc']);
fun(a)
// '[[true,7],[{"foo":3},["abc"]]]'
- JSON 转 Map
(1)所有键名都是字符串:
function fun (s) {
let strMap = new Map();
for (let k of Object.keys(s)) {
strMap.set(k, s[k]);
}
return strMap;
return JSON.parse(strMap);
}
fun('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
(2)整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组:
function fun2(s) {
return new Map(JSON.parse(s));
}
fun2('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}