愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Symbol

一、基础知识

Symbol是JavaScript的第7种数据类型,用来表示具有唯一性的值,创建一个symbol数据类型的变量,要通过Symbol()函数,如:

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

可以给Symbol()函数传递一个参数,这个参数是个字符串,表示对Symbol对象的描述,如:

let s = Symbol('a symbol variable');
s; // Symbol(a symbol variable)

需要注意的是,每次使用Symbol()创建的变量,都是不同的,即使它们的描述相同:

let s1 = Symbol();
let s2 = Symbol();
s1 === s2; // false

即使描述相同,它们也是两个不同的symbol变量

let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2; // false

symbol值不能与其他的数据类型进行运算,否则会报错,如以下的运算都是不允许的:

s + '';
s + 1;
s + true;

但是symbol值可以 显式转化 为字符串,如:

let s = Symbol('desc');
String(s); // 'Symbol(desc)'

symbol值也可以转为boolean值,但是不能转为数值,如:

let s = Symbol('desc');
Boolean(s); // true
Number(s); // 报错


二、作为属性名

由于每个symbol值都是唯一的、不相等的,所以我们可以使用symbol类型的值来作为标识符,如作为属性的名,这样子就能够避免值被覆盖或改写,如:

const symbolKey = Symbol();
const obj = {
    [symbolKey]: 'Hello, world'
}

需要注意的是:在获取对象的属性的时候,不能使用.语法,如果使用.的话,就会被认为是一个string类型的key,而不是symbol类型的key,正确的方法是使用方括号[]
由于Symbol的这些特性,我们可以很方便的用它来定义一组常量,如:

const COLOR = {
    RED: Symbol('RED'),
    BLUE: Symbol('BLUE'),
    BLACK: Symbol('BLACK')
};


三、属性名的遍历

如果我们使用for-inObject.keys()Object.getOwnPropertyNames()JSON.stringify()这些方法的话,symbol类型的key是不会被遍历进去的,如果我们想要遍历来得到symbol类型的属性,那么可以使用Object.getOwnPropertySymbols(),如:

const obj = {
    a: 'A',
    b: 'B',
    [Symbol()]: 'symbol'
};
Object.defineProperty(obj, 'unenumerable', {
    value: '123',
    enumerable: false
});

Object.keys(obj); // ['a', 'b']
Object.getOwnPropertyNames(obj); // ['a', 'b']
Object.getOwnPropertySymbols(obj); // [Symbol()]

此外,可以使用新的API:Reflect.ownKeys()来得到一个对象下所有的可枚举的、不可枚举的,包括symbol类型在内的key:

Reflect.ownKeys(obj); // ['a', 'b', 'unenumerable', Symbol()]


四、Symbol.for()Symbol.keyFor()

有时候,我们希望重新使用一个Symbol值,那么这种情况下,就可以使用Symbol.for(),这个函数接收一个string类型的参数,表明是以此为名称的Symbol值,和Symbol()函数的区别是:
每次调用Symbol()都会生成一个新的symbol值,而调用Symbol.for(sKey)则会先在全局登记环境里查找是否有名称为sKey的symbol值,如果有的话,直接返回,没有的话先新建一个,然后再返回,如:

const s1 = Symbol();
const s2 = Symbol();
s1 === s2; // false;

const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
s1 === s2; // true;

所以我们Symbol.for()创建的symbol值是带登记的symbol值,带登记的symbol值可以使用Symbol.keyFor()来获取它的名称:

const s1 = Symbol.for('HelloWorld');
Symbol.keyFor(s1); // 'HelloWorld'

而不带登记的symbol值则不行,如:

const s = Symbol();
Symbol.keyFor(s); // undefined


五、内置的Symbol值

ES6内置了11个内置的Symbol值,指向语言内部使用的方法

1、Symbol.hasInstance

当调用instanceof运算符时,会调用右操作符的Symbol.hasInstance方法,如:

const obj = {
    [Symbol.hasInstance]() {
        return true;
    }
}
const a = {};
a instanceof obj; // true

2、Symbol.isConcatSpreadable

表示对象用于Array.prototype.concat()时是否可展开,Symbol.isConcatSpreadable是一个布尔值,当设为true时,表示可展开。默认情况下这个值为undefined(并不表示默认可否展开),但是当我们期望它展开的时候,可以将它设为true

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = {0: 7, 1: 8, 2: 9, length: 3};

arr1.concat(arr2); // [1, 2, 3, 4, 5, 6]
arr1.concat(arr3); // [1, 2, 3, {0: 7, 1: 8, 2: 9, length: 3}]
arr3[Symbol.isConcatSpreadable] = true;
arr1.concat[arr3]; // [1, 2, 3, 7, 8, 9]

3、Symbol.species

创造实例时,会调用Symbol.species方法,用它返回的函数作为构造函数,来创造新的实例对象,如:

class MyArray extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}
const a = new MyArray(1, 2, 3);
const m = a.map(x => x*x);
m instanceof MyArray; // false
m instanceof Array; // true

4、Symbol.match

当一个对象objSymbol.match()方法存在时,调用str.match(obj)时就会调用[Symbol.match](),其返回值作为str.match(obj)的返回值,如:

const isHello = {
    [Symbol.match](str) {
        return str === 'Hello';
    }
}
'Hello'.match(isHello); // true
'HelloWorld'.match(isHello); // false

5、Symbol.replace

调用String.prototype.replace方法中时会调用[Symbol.replace](),其中:

  • 第一个参数为调用replace()方法的字符串
  • 第二个参数为传给replace()方法的第二个参数

如:

const obj = {};
obj[Symbol.replace] = (str, toReplaceWith) => {
    return str + toReplaceWith;
}
'Hello'.replace(obj, ' World'); // Hello World

6、Symbol.search

当调用String.prototype.search()时,如果obj的[Symbol.search]()存在,那么就会调用[Symbol.search]()并将其返回值作为str.search(obj)的返回值:

const obj = {
    [Symbol.search]() {
        return 'Hello';
    }
}
'str'.search(obj); // 'Hello'

7、Symbol.split

当调用String.prototype.split()时,如果obj的[Symbol.split]()存在,那么就会调用[Symbol.split]()并将其返回值作为str.split(obj)的返回值:

const getEachChar = {
    [Symbol.split](str) {
        return str.split('');
    }
}
'abcd'.split(getEachChar); // ['a', 'b', 'c', 'd']

8、Symbol.iterator

指定对象迭代时所调用的方法,如使用for-of循环或者...时,会调用Symbol.iterator方法

9、Symbol.toPrimitive

当对象被转为原始值时,会调用[Symbol.toPrimitive]()方法,以其返回值作为值。而这个方法调用的时候,会得到一个参数,表示当前的运算模式,有:

  • number:该场合需要转为数值
  • string:该场合需要转成字符串
  • default:该场合可以转成数值,也可以转成字符串
let obj = {
    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case 'number': return 10;
            case 'string': return 'World';
            case 'default': return '[Default]';
            default: throw new Error();
        }
    }
}
2 + obj; // '2[Default]'
2 * obj; // 20
String(obj); // 'World'

10、Symbol.toStringTag

用来指定Object.prototype.toString.call()返回的[Object xxx]xxx的值,如:

const obj = {}
obj[Symbol.toStringTag] = 'DIY';
Object.prototype.toString.call(obj); // [object DIY]

ES6里各个内置对象的Symbol.toStringTag的值如下

  • JSONMathArrayBufferDataViewMapPromiseSetWeapMapWeakSetSymbolGeneratorGeneratorFunction:和构造函数的名称一样
  • %TypedArray%:Uint8Array等
  • %MapIteratorPrototype%:Map Iterator
  • %SetIteratorPrototype%:Map Iterator
  • %StringIteratorPrototype%:Map Iterator

11、Symbol.unscopables

指定当该对象使用了with时,有哪些属性会被with环境排除,如:

const obj = {
    foo() {
        console.log('Inner');
    }
}

function foo() {
    console.log('Outer');
}

with(obj) {
    foo(); // Inner
}

// 指定后
obj[Symbol.unscopables] = { foo: true }
with(obj) {
    foo(); // Outer
}