ES6(8)—— Symbol

首先,我们来回顾一下,javascript的六大数据类型:Undefined、Null、Boolean、String、Number、Object。ES6现在引入了一种新的原始数据类型Symbol,表示第一无二的值。为什么引入Symbol类型呢?ES5的对象属性名都是字符串,这容易造成属性冲突,如果有一种机制可以保证每个属性的名字都是独一无二的就好啦,这样就能从根本上防止属性名冲突,这就是为什么ES6引入Symbol类型。

一、Symbol值是通过Symbol函数生成。Symbol函数接受一个参数,表示对Symbol实例的描述,有了参数在控制台输出的时候,有利于区分到底是哪一个值,这个参数只表示对当前Symbol的描述,所以参数相同的Symbol函数的返回值是不相等的。对象的属性名可以有两种类型:一种是原来就有的字符串,另一种是新增的Symbol类型。只要属性名属于Symbol类型,就是独一无二的,可以保证不与其他属性名冲突。

let s = Symbol(‘foo');
console.log(type s);          //Symbol
consoe.log(s.toString());     //'Symbol(foo)'
//这里的s的值就是独一无二的

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

二、Symbol函数参数需要注意的问题。

1、如果参数是一个对象,就会调用该对象的toString方法,将其转换为字符串,然后生成一个Symbol值。

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

2、Symbol的值不能与其他类型的进行运算,否则会报错。

var sym = Symbol('My Symbol');
"your symbol is" + sym;
//报错:TypeError: Cannot convert a Symbol value to a string

var sym = Symbol('My Symbol');
`your symbol is ${sym}`;
//报错:TypeError: Cannot convert a Symbol value to a string

3、Symbol值可以显式转换为字符串。

var sym =Symbol('My Symbol');
console.log(String(sym));   //'Symbol(My Symbol)'
console.log(sym.toString());  //'Symbol(My Symbol)'

4、Symbol值也可以转换为布尔值,但是不能转换为数值。

var sym = Symbol();
console.log(Boolean(sym));    //true
console.log(!sym);            //false
if(sym){ }
Number(sym);                  //TypeError: Cannot convert a Symbol value to a number

三、Symbol作为属性名。

由于每个Symbol值都是不一样的,Symbol可以作为对象的属性名,保证不会出现同名的属性。能防止某一个键被不小心改写或覆盖。

1、Symbol作为对象属性名的时候的三种写法。

var mySymbol = Symbol();

//第一种写法
var a = {};
a[mySymbol] = 'hello';

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

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

注意:Symbol值作为对象属性名时不能使用点运算符。Symbol作为属性名时,该属性是公开的,不是私有的。

2、在对象内部,使用Symbol值定义属性时,Symbol值必须放在方括号中。

//如下s如果不放在方括号中,该属性的键名就是字符串s,而不是s所代表的Symbol
let s = Symbol();
let obj = {
    [s].function(arg){...}
};
obj[s](123);

//采用增强的对象写法,上面的obj对象可以写的更简洁一些。
let obj = {
    [s](arg){...}
};

3、Symbol类型还可用于定义一组常量,保证这组常量的值都是不相等的。常量使用Symbol值最大的好处就是,其他任何值都不可能有相同的值。

log.leves = {
    DEBUG:Symbol('debug'),
    INFO:Symbol('info'),
    WARN:Symbol('warn')
};
log(log.levels.DEBUG,'debug message');
log(log.levels.INFO,'info message');

四、属性名的遍历

Symbol作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()返回。但它也不是私有属性,有一个Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。

1、Object.getOwnPropertySymbols()方法

var obj = {};
var a = Symbol('a');
var b = Symbol('b');
obj[a] = 'hello';
obj[b] = 'world';
var objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols);         //[Symbol(a),Symbol(b)]

2、Object.getOwnPropertySymbols()与for...in循环、Object.getOwnPropertyNames()对比。

(1)上述方法的对比

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)]

(2)有一个API——Reflect.ownKeys方法可以返回所有类型的键名(包括常规键名和Symbol键名)。

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

//输出结果为:[ 'enum', 'nonEnum', Symbol(my_key) ]

注意:以Symbol值作为名称的属性一般不会被常规方法遍历得到。我们可以利用这个特性为对象定义一些非私有但又希望只用于内部的方法。

3、Symbol.for()、Symbol.keyFor()。

有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为一个参数,然后搜索有没有以该参数作为名称的Symbol的值。如果有就返回这个Symbol值,否则就创建并返回一个以该字符串为名称的Symbol值。

var s1 = Symbol.for("foo");
var s2 = Symbol.for("foo");

s1 === s2;        //true
//s1和s2是都是Symbol值,但它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值得。

Symbol和Symbol.for的区别:Symbol.for()和Symbol()都会生成全新的Symbol。它们的区别是,前者会被登记在全局中供搜索,而后者不会。Symbol.for()不会在每次调用的时都返回一个心得Symbol类型的值,而是会先检查给定的key是否以已经存在,如果不存在才会新建一个值。比如,如果调用Symbol.for("cat")30次 ,每次都会返回同一个Symbol值,但是调用Symbol("cat")30次,每次都会返回30个不同的Symbol值。

Symbol.for("bar") === Symbol.for("bar")     //true
Symbol("bar") === Symbol("bar")             //false

4、Symbol.keyFor()返回一个已登记的Symbol类型值的key。

var s1 = Symbol.for("foo");
Symnol.keyFor("s1");        //"foo"

var s2 = Symbol("foo");
Symnol.keyFor("s2");        //"undefined"
//之所以是undefined,是因为s2属于未登记的Symbol值。

五、内置的Symbol值。

除了定义自己使用的Symbol值,ES6提供了11个内置的Symbol值,指向语言内部使用的方法。

1、Symbol.hasInstance()

对象使用instenceof运算符时会调用这个方法,判断对象是否为某个构造函数的实例。比如,foo instanceof Foo,在语言内实际调用的是Foo[Symbol.hasInstance](foo)。

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

console.log([1,2,3] instanceof new MyClass());    //true

2、Symbol.isConcatSpreadable()

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

(1)基本用法

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

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

//上面的代码说明,数组的默认行为是可以展开的。Symbol.isConcatSpreadable属性等于true或undefined,都有这个效果。
let obj = {length:2,0:'c',1:'e'};
console.log(['a','b'].concat(obj,'e'));       //[ 'a', 'b', { '0': 'c', '1': 'e', length: 2 }, 'e' ]
obj[Symbol.isConcatSpreadable] = true;
console.log(['a','b'].concat(obj,'e'));       //[ 'a', 'b', 'c', 'e', 'e' ]

//类似数组的对象也可以展开,但它可以Symbol.isConcatSpreadable属性默认值是false,必须手动打开。

(2)对于一个类而言,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;
console.log([1,2].concat(a1).concat(a2));     //[ 1, 2, 3, 4, A2 [ 5, 6 ] ]

3、Symbol.species()

对象的Symbol.species属性指向当前对象的构造函数。创建实例时默认会调用这个方法,即使用这个属性返回的函数当作构造函数来创造新的实例对象。定义Symbol.species属性要采用get读取器。

class MyArray extends Array {
    static get [Symbol.species](){
        return Array;
    }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);
console.log(a instanceof Array);               //true
console.log(a instanceof MyArray);             //true
console.log(mapped instanceof Array);         //true
console.log(mapped instanceof MyArray);        //false
//由于构造函数被替换成了Array,所以mapped对象不是MyArray的实例,而是Array的实例。原因是创建MyArray的实例对象时,本来会调用它自己的构造函数,但是定义了Symbol.species属性,所以会使用这个属性返回的后汉书创建MyAarray的实例。

4、Symbol.match()

对象Symbol.match属性指向一个函数,当执行str.match(myObject)时,如果该属性存在,会调用它返回该方法的返回值。

class MyMatcher{
    [Symbol.match](string){
        return 'hello world'.indexOf(string);
    }
}
console.log('e'.match(new MyMatcher()));

//String.prototype.match(regexp) 等同于 regexp[Symbol.match](this)

5、Symbol.replace()

对象Symbol.replace属性指向一个方法,当对象被String.prototype.replace方法调用时会返回该方法的返回值。

const x = { };
x[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(x,'World');
//Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,在上面的例子中是Hello,第二个参数是替换后的值,在上面例子中是World。

//String.prototype.replace(searchValue,replaceValue) 等同于 searchValue[Symbol.repalce](this,repalceValue)

6、Symbol.search()

对象的Symbol.search属性指向一个方法,当对象被String.prototype.search方法调用时会返回该方法的返回值。

class MySearch{
    constrcutor(value){
        this.value = value;
    }
    [Symbol.search](string){
        return string.indexOf(this.value);
    }
}
console.log('foobar'.search(new MySearch('foo')));

//String.prototype.search(regexp) 等同于 regexp[Symbol.search](this)

7、Symbol.split()

对象的Symbol.split属性指向一个方法,当被对象String.prototype.split方法调用时会返回该方法的返回值。

class MySplitter{
    constructor(value){
        this.value = value;
    }
    [Symbol.split](string){
        var index = string.indexOf(this.value);
        if (index === -1){
            return string;
        }
        return [
            string.substr(0,index),
            string.substr(index + this.value.length)
        ];
    }
}

console.log('foobar'.split(new MySplitter('foo')));  //['','bar']
console.log('foobar'.split(new MySplitter('bar')));  //['foo','']
console.log('foobar'.split(new MySplitter('baz')));  //'foobar'

//上面的代码使用Symbol.split方法,重新定义字符串对象的split方法的行为。

8、Symbol.iterator()

对象的Symbol.iterator属性指向该对象的默认遍历器方法。

var myIterable = { };
myIterable[Symbol.iterator] = function* (){
    yield 1;
    yield 2;
    yield 3;
};
console.log([...myIterable]);      //[ 1, 2, 3 ]

//对象进行for...of循环时,会调用Symbol.iterator方法返回该对象的默认遍历器。

9、Symbol.toPrimitive()

对象的Symbol.toPrimitive属性指向一个方法,对象被转换为原始类型的值时会调用这个方法,返回该对象对应的原始类型值。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();
        }
    }
};
console.log(2*obj);                 //246
console.log(3+obj);                 //'3default'
console.log(obj == 'default');     //true
console.log(String(obj));          //'str'

10、Symbol.toStringTag()

对象的Symbol.toStringTag属性指向一个方法,在对象上调用Object.prototype.toString方法时,如果这个属性存在,则返回值会出现在toString方法返回的字符串中,表示对象的类型。也就是说,这个属性可用于定制[object Object]或[object Array]中object后面的字符串。

ES6新增内置对象的Symbol.toStringTag属性值如下:

JSON[Symbol.toStringTag]:'JSON'
Math[Symbol.toStringTag]:'Math'
M[Symbol.toStringTag]:'Module'
ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]:'DataView'
Map.prototype[Symbol.toStringTag]:'Map'
Promise.prototype[Symbol.toStringTag]:'Promise'
Set.prototype[Symbol.toStringTag]:'Set'
%TypedArray%.prototype[Symbol.toStringTag]:'Unit8Array'
WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
Symbol.prototype[Symbol.toStringTag]:'Symbol'
Math[Symbol.toStringTag]:'Math'
Generator.prototype[Symbol.toStringTag]:'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'*/
class Collection{
    get [Symbol.toStringTag](){
        return 'xxx';
    }
}
var x = new Collection();
console.log(Object.prototype.toString.call(x));   //[object xxx]*/

11、Symbol.unscopables()

对象的Symbol.unscopables属性指向一个对象,指定了使用with关键字时哪些关键字会被with环境删除。

数组有7个属性会被with命令排除。

Array.prototype[Symbol.unscopables]{
    //copyWithin:true,
    //entries:true,
    //fill:true,
    //find:true,
    //findIndex:true,
    //includes:true,
    //keys:true
}
//没有unscopables
class MyClass{
    foo(){return 1;}
}
var foo = function(){
    return 2;
}
with(MyClass.prototype){
    console.log(foo());       //1
}
//有unscopables时
class MyClass{
    foo(){return 1;}
    get [Symbol.unscopables](){
        return {foo:true};
    }
}
var foo = function(){return 2};
with(MyClass.prototype){
    console.log(foo());       //2
}
//上述代码通过指定Symbol.unscopables属性使with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的遍量

猜你喜欢

转载自blog.csdn.net/zhanghuali0210/article/details/81193434