es5定义属性名只能使用字符串,这就可能导致一个问题,就是你定义的属性名跟别人定义的属性名冲突。为了解决这个问题,es6引入了一个新的原始数据类型Symbol。Symbol的值是通过Symbol函数生成的,现在的属性名可以有两种类型,一种是字符串,另一种就是Symbol类型,凡是Symbol类型的属性名就是独一无二的,不会与其它任何属性名冲入。
Symbol概述
定义一个Symbol类型的变量看看
嗯,变量s果然是Symbol类型,不是字符串了。
Symbol是一种原始数据类型,不是对象,所以Symbol不能new(会报错),Symbol不能添加属性和方法。基本上,它是一种类似于字符串的数据类型。
Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1; //Symbol(foo)
s2; //Symbol(bar)
let s3 = Symbol('foo');
s1 和 s3是不相等的两个值。
Symbol还有以下一些特征,Symbol可以显示转为字符串
s1.toString(); // "Symbol(foo)"
如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。
var a = {};
var b = Symbol(a);
b; // "Symbol([object Object])"
a['toString'] = function () { return 'a symbol' }
var c = Symbol(a);
c; // "Symbol(a symbol)"
也可以转为布尔类型
Boolean(s1); // true
但是不能转为数字,也不能与其它数据类型参与计算,会报错。
获取Symbol的描述,可以通过 Symbol.prototype提供的description属性获取
s1.description //"foo"
------------------------------------
作为属性名的Symbol
通过方括号结构和Object.defineProperty将对象的属性名指定为一个Symbol值。
var mySymbol = Symbol();
//第一种写法
var a = {};
a[mySymbol] = 'hello';
//第二种写法
var a = {
[mySymbol]: 'hello'
};
//第三种写法
var a = {};
Object.defineProperty(a, mySymbol, {value: 'hello'});
//注意,Symbol值作为对象属性名时不能用点运算符。
使用Symbol作为属性名不会出现同名的属性
1. let obj = {
type: 'PC', //小A定义的
type: 'NEW' //小B定义的,把小A的覆盖了
}
2. const type = 'TYPE'; //小A定义的
const type1 = 'TYPE'; //小B定义的
let obj = {
[type]: 'PC',
[type1]: 'NEW' //把小A的覆盖了
}
3. 使用Symbol不会被覆盖
const type = Symbol('type'); //小A定义的
const type1 = Symbol('type'); //小B定义的
let obj = {
[type]: 'PC',
[type1]: 'NEW'
}
------------------------------------消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
'Triangle'就是一个魔术字符串,它出现多次,不利于维护,所以我们要把他定义成一个变量或者属性,消除强耦合。
triangle:Symbol('triangle') //这里如果triangle的值是什么并不重要,只是一个标识,不会与业务耦合
------------------------------------
属性名的遍历
在a.js文件中定义一个对象testSymbol,并将它对外暴露
let t2 = Symbol('t2');
export let testSymbol = {
t1: 't1value',
[t2]: 't2value'
};
在b.js中引入a.js,只能访问到属性t1, 无法访问到t2属性
import { testSymbol } from '/a.js'
console.log(testSymbol['t1']); //t1value
console.log(testSymbol[t2]); //Uncaught (in promise) ReferenceError: t2 is not defined
for(let i in testSymbol) { console.log(i); } //"t1"
所以可以利用Symbol定义私有属性
-----------------------------------
Symbol.for() Symbol.keyFor()
Symbol()和Symbol.for()这两个方法都会生产Symbol对象,Symbol.for()与Symbol()的不同是它生成的Symbol对象会被注册到全局环境中。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2); // true
Symbol.for('foo')先检查全局的注册表中有没有key是'foo'的Symbol对象,如果存在就返回这个对象,如不不存在就重新生成。
------------------------------------
内置的Symbol值
es6提供了11个内置的Symbol值,指向了语言内部使用的方法,实现元编程(对js默认的行为做操作)。
1. Symbol.hasInstance
对象的Symbol.hasInstance指向内部方法。当对其它对象(a)使用instanceof操作符判断是不是该对象(b)的实例时,会调用该对象(b)的[Symbol.hasInstance
]方法。比如a instanceof b,在方法内部实际调用的是b[Symbol.hasInstance](a)。
let a = [1,2,3], b;
class MyArray {
[Symbol.hasInstance] (val) {
return val instanceof Array;
}
}
b = new MyArray();
console.log(a instanceof b); //true
2.Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性是一个布尔值,它表示对该对象使用Array.prototype.concat()方法时,该对象是否可以展开。
当对象是数组时,Symbol.isConcatSpreadable的默认值是undefined,默认展开。设置为false不可以展开,设置为true可以展开。
当对象是一个类似数组的对象时,Symbol.isConcatSpreadable的默认值是undefined,默认不展开。设置为false不可以展开,设置为true可以展开。
默认情况:
let a1 = [3, 4];
let a2 = {0: 3, 1: 4, length: 2};
console.log([1].concat(a1, [5, 6])); // [1, 3, 4, 5, 6]
console.log([1].concat(a2, [5, 6])); // [1, {0: 3, 1: 4, length: 2}, 5, 6]
设置一下:
a1[Symbol.isConcatSpreadable] = false;
a2[Symbol.isConcatSpreadable] = true;
console.log([1].concat(a1, [5, 6])); // [1, 3, [4, 5], 6]
console.log([1].concat(a2, [5, 6])); // [1, 3, 4, 5, 6]
Symbol.isConcatSpreadable还可以定义在类中
class A1 extends Array {
constructor (args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor (args) {
super(args);
}
get [Symbol.isConcatSpreadable] () {
return false;
}
}
let a1Obj = new A1();
let a2Obj = new A2();
a1Obj[0] = 3;
a1Obj[1] = 4;
a2Obj[0] = 5;
a2Obj[1] = 6;
console.log([1, 2].concat(a1Obj, a2Obj)); //[1, 2, 3, 4, [5, 6]]
A1是定义在实例上,A2是定义在类本身。
3. Symbol.species
对象的Symbol.species属性,指向构造函数。当创建衍生对象时,会调用这个属性。
class MyArray extends Array {};
let ma1 = new MyArray(1, 2, 3);
let ma2 = ma1.map(item => item * 2);
let ma3 = ma1.filter(item => item > 1);
console.log(ma2 instanceof MyArray); //true
console.log(ma2 instanceof Array); //true
console.log(ma3 instanceof MyArray); //true
console.log(ma3 instanceof Array); //true
从上面例子看,类MyArray继承Array,ma1是MyArray的实例,ma2和ma3是ma1的衍生对象。ma2和ma3都是通过数组的方法生成出来的对象,也就是说他们应该是Array的实例,但是他们并不是MyAarry的实例,所以上面得到的结果并不是我们想要的。那么通过设置Symbol.species属性就可以解决这个问题,我们要设置MyArray的Symbol.species属性。
定义Symbol.species使用get取值器。Symbol.species是类的静态属性。Symbol.species内部会指向指定的构造函数(现在我们需要指向Array)。
class MyArray extends Array {
static get [Symbol.species] () {
return Array;
}
};
通过上面的设置后,ma2和ma3只是Array的实例,并不是MyArray的实例。
Symbol.species主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
4.Symbol.match (Symbol.replace, Symbol.search,Symbol.split都是类似的)
对象的Symbol.match属性,指向一个方法,当执行str.match(myObj)时,如果该属性存在,会调用它,返回该方法的返回值。
class MyMatcher {
constructor (bstr) {
this.bstr = bstr;
}
[Symbol.match] (str) {
return str.indexOf(this.bstr);
}
}
'hello world'.match(new MyMatcher('e'));
5. Symbol.replace
class MyReplace {
constructor (bstr) {
this.bstr = bstr;
}
[Symbol.replace](oldStr, newStr){
console.log('"'+ oldStr +'"被替换成了"'+ newStr +'"');
return this.bstr.replace(oldStr, newStr);
}
}
let res = '一'.replace(new MyReplace('我有一盆花'), '很多');
console.log(res);
Symbol.replace方法会收到两个参数,第一个参数是replace 方法正在作用的对象,上面例子第一个参数是“一”,第二个参数是替换后的值“很多”。
6. Symbol.search
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
7. Symbol.split
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo')) // ['', 'bar']
'foobar'.split(new MySplitter('bar')) // ['foo', '']
'foobar'.split(new MySplitter('baz')) // 'foobar'
8. Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。对象执行for...of时,会调用该对象的Symbol.iterator方法,可参见Iterator 和 for...of 循环。
9. Symbol.toPrimitive
对象的Symbol.toPrimitive属性,指向一个方法。把一个对象转为原始类型值时。会调用Symbol.toPrimitive方法,返回值是该对象的原始类型值。Symbol.toPrimitive接受一个参数,该参数表示当前运算的模式,一共有三种模式:
Number:该场合需要转成数值
String:该场合需要转成字符串
Default: 该场合可以转成数值,也可以转成字符串
let obj = {
[Symbol.toPrimitive] (hint) {
switch (hint) {
case 'number':
return 1;
case 'string':
return 's';
case 'default':
return 'default';
default:
return new Error();
}
}
}
console.log(obj * 2); //2
console.log(String(obj)); //s
console.log(obj + 'abc'); //defaultabc
10. Symbol.toStringTag
对象的Symbol.toStringTag属性,指向一个方法。我们在获取一个对象的类型时会调用Object.prototype.toString.call('abc') 返回的是[object String]。这里,Symbol.toStringTag的返回值就会出现在toString方法的返回字符串中(就是结果中String的部分),看个例子。
class StringTag {
get [Symbol.toStringTag] () {
return 'arr';
}
}
Object.prototype.toString.call(new StringTag()); //"[object arr]"
11. Symbol.unscopables
对象的Symbol.unscopables属性,指向一个对象。指定了使用with关键字时,哪些属性是被with环境排除的。
举个例子,Array.prototype[Symbol.unscopables] 默认返回的是
我们来验证一下
let a = [1, 2, 3]
with(a){console.log(indexOf)} //ƒ indexOf() { [native code] }
with(a){console.log(findIndex)} //VM15227:1 Uncaught ReferenceError: findIndex is not defined