《 ES6修炼记——Symbol 》

321 阅读7分钟

基础篇

一、什么是Symbol?

  Symbol是ES6规范中新引入的一种数据类型,表示独一无二的值。它是JavaScript的第七种数据类型,前六种分别是:Undefined、Null、Boolean、String、Number、Object。

二、为什么要引入Symbol类型?

  所有新技术的诞生都是为了解决某一问题或提高生产效率,Symbol也不例外。在ES5时代,对象属性名都是字符串,这就可能出现属性名冲突的情况。例如,当我们引入了一个外部对象,如果我们想在这个对象中添加一个新的方法,就有可能会与对象中现有的属性产生冲突。
  想要从根本上解决这个问题,我们需要一个永远不会重复的数据类型,所以Symbol就应运而生了。

三、怎样生成一个Symbol值?

  Symbol值可以直接调用Symbol函数生成(还有另外一种生成方法,后面会讲到),每一个Symbol值都是独一无二的,使用typeof检测实例的类型为symbol。

    let a = Symbol()
    let b = Symbol()
    a === b  // false
    typeof a   // symbol

  Symbol函数也可以接收一个字符串参数,作为对生成的Symbol描述,其主要目的是为在控制台显示时容易区分不同的Symbol值。请注意,不同的Symbol值可以使用相同的描述,但是不代表他们是相等的。

    let a = Symbol()
    let b = Symbol('symbol-describe')
    let c = Symbol('symbol-describe')
    
    a  // Symbol()   -->如果不加描述,那么再控制台中输出的都是Symbol()
    b  // Symbol(symbol-describe)
    c  // Symbol(symbol-describe)
    b === c  // false

  Symbol值可以显式转为字符串,但不能隐式转换成字符串,也不能和其他任何类型的值进行运算,否则代码会报错!此外,Symbol可以转换为布尔值,但不能转为数值。

    let sym = Symbol('symbol')
    String(sym)  // "Symbol(symbol)"
    sym.toString()  // "Symbol(symbol)"
    'this is' + sym   // Cannot convert a Symbol value to a string
    `this is ${sym}`  // Cannot convert a Symbol value to a string
    
    Boolean(sym)  // true
    !sym  // false
    if(sym){
        // true
    }
    
    Number(sym)  // TypeError

  需要注意的是:
  不能使用new命令来调用Symbol函数,否则代码会报错!因为生成的Symbol值是一个原始类型的值,而不是对象。此外,因为Symbol值是原始类型,所以用instanceof判断Symbol值会直接返回false。

不能使用new命令

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

  除了上述方法外,Symbol.for()方法也可以生成一个Symbol值。Symbol.for()接收一个key值(字符串)作为参数。在运行时,会先在全局环境下搜索,是否有用该key值创建过Symbol实例。如果存在,则不会再新创建一个Symbol值,而是直接返回这个Symbol值。如果不存在,才会创建一个新的Symbol值,并在全局环境中登记,供下次搜索。   

    let a = Symbol.for('my-symbol')
    let b = Symbol.for('my-symbol')
    a === b  // true  --> 因为已经存在以'my-symbol'为key值创建的实例,所以在创建b时直接返回了a
    
    let c = Symbol('symbol')
    let d = Symbol.for('symbol')
    c === d  // false  --> Symbol('symbol')方法并不会登记,所以Symbol.for会新创建一个值

  与Symbol.for方法对应的还有一个Symbol.keyFor方法,此方法可以获取一个已登记Symbol值的key值。如果Symbol值没有登记,则返回undefined。   

    let sym1 = Symbol()
    let sym2 = Symbol.for('mySymbol')
    Symbol.keyFor(sym1)  // undefined
    Symbol.keyFor(sym2)  // "mySymbol"

五、Symbol使用场景

(一)、用作对象属性名

  想象一下这个场景,有一天你引入了一个外部提供的对象,你想要在这个对象里加入新的方法,但是你却不知道你定义的属性名已经存在于现有对象中了,这造成了一个让你抓狂bug。。。此时,如果你使用了Symbol作为属性名,就完全不用担心属性名会冲突,因为每一个Symbol都是独一无二的。在对象中使用Symbol属性名有以下三种写法:   

    let sym = Symbol()
    
    // 写法一
    let a = {}
    a[sym] = 'juejin'
    
    // 写法二
    let a = {
     "sym" : 'juejin',
     [sym] : 'juejin',  // 你可以随意使用自己想用的名字,不会产生冲突
     [sym2](){...}  // 也可以采用简洁的写法
    }
    
    // 写法三
    Object.defineProperty(a, sym, {value:'juejin'})

  需要注意的是:
  1. 不能使用点运算符去定义Symbol类型的属性名,因为点运算符始会把后面的值读取为字符串。

    let sym = Symbol() 
    let obj = {}
    obj.sym = 'juejin'
    obj[sym]    // undefined
    obj['sym']  //'juejin'

  2. 用for...in、for...of、Object.keys()、Object.getOwnPropertyNames()方法遍历对象时,均不会遍历出Symbol属性名。有两个方法可以获取Symbol类型的属性名:
  Object.getOwnPropertySymbols,会返回一个数组,包含当前对象所有用作属性名的Symbol值,不返回字符串属性名。

    let obj = {}
    let sym = Symbol()
    obj.a = 'juejin'
    obj[sym] = 'a symbol'
    Object.getOwnPropertySymbols(obj)  // [Symbol()]

  Reflect.ownKeys,会返回一个数组,包含所有类型的键名

    let obj = {
        num:1,
        [Symbol('sym')]:'juejin'
    }
    Reflect.ownKeys(obj)  // ["num", Symbol(sym)]

(二)、用于非私有内部方法

  因为Symbol值作为对象属性名时,不会被常规方法遍历到,所以,我们可以利用这个特性来定义一些非私有的内部方法。   

    var size = Symbol('size')
    class Collection{
        constructor(){
            this[size] = 0
        }
        add(item){
            this[this[size]] = item
            this[size]++
        }
        static sizeOf(instance){
            return instance[size]
        }
    }
    var x = new Collection()
    Collection.sizeOf(x)  // 0
    x.add('foo')
    Collection.sizeOf(x)  // 1
    Object.keys(x)  // ['0'] ,因为size属性名是Symbol值,所以无法获取
    Object.getOwnPropertyNames(x)  // ['0'] ,因为size属性名是Symbol值,所以无法获取
    Object.getOwnPropertySymbols(x)  // [Symbol(size)]

(三)、用作常量

  假如有一个计算面积的函数,可以根据传入的参数,采用不同的计算公式,如下:   

    var shapeType = { triangle: 'Triangle',square: 'Square',...};
    function getArea(shape, options) { 
        var area = 0; 
        switch (shape) { 
          case shapeType.triangle:
          area = .5 * options.width * options.height; 
          break; 
          case shapeType.square:
          area = options.width * options.height;
          break;
          ... 
        } 
        return area;
    }

    getArea(shapeType.triangle, { width: 100, height: 100 });

  仔细观察可以发现,shapeType对象里的键值是什么并不重要,我们只要保证他们不一样就可以了,这种情况就非常适合采用Symbol。   

    var shapeType = { triangle: Symbol(), square: Symbol(),...};

  所以,当某个场景我们仅仅需要不相同的变量时,而不在乎变量的值是什么,这时候我们就可以用Symbol。
  

扩展篇

  运用上述的知识,我们可以定义自己的Symbol值。除此之外,ES6还提供了11个内置的Symbol值,这些值其实与上述的Symbol知识点关系不大,我们可以把他们理解为JavaScript中特定的值,他们代表着某个方法或某个值。我挑选其中两个工作中常用的介绍下,其他的可以参考MDN。

一、Symbol.hasInstance

  Symbol.hasInstance是对象的一个属性,它指向一个方法,对象使用instanceof运算符时会调用这个方法。也就是说,当我们使用arr instanceof Array时,在语言内部实际上是调用了Array[Symbol.hasInstance] (arr) 这个方法,来去检测arr是否是Array的实例。既然instanceof的本质是Symbol.hasInstance方法,那么假如我们改写一个构造函数的Symbol.hasInstance方法,就可以改变instanceof的行为:   

    class Test{
        static [Symbol.hasInstance](param){
            return param === 'juejin'
        }
    }
    'github' instanceof Test  // false
    'juejin' instanceof Test  // true

二、Symbol.species

  Symbol.species是对象的属性,它指向当前对象的构造函数。当创建衍生对象时,会调用这个方法。也就是说,把调用这个函数后返回的函数当做构造函数,来创建衍生对象。默认的Symbol.species属性,会返回当前构造函数本身。请看下面这个例子:   

    class MyArray extends Array {
        // 默认的Symbol.species方法等同于下面的写法,Symbol.species采用了get读取器,避免被修改
        static get [Symbol.species](){  // 
            retrun this
        }
    }

    const a = new MyArray(1, 2, 3);
    const b = a.map(x => x);  
    const c = a.filter(x => x > 1);
    // 在创建b和c时,实际上是使用了MyArray内部的Symbol.species方法返回的this当做构造函数,创造了b和c
    
    b instanceof MyArray // true
    c instanceof MyArray // true

  我们可以覆盖对象原有的Symbol.species属性,这样可以改变一些原有的行为,产生特殊的效果:   

    class myArray extends Array{
        static get [Symbol.species](){
            return Array  // 把构造函数改写成Array
        }
    }
    var arr = new myArray(1,2,3) 
    var mapArr = arr.map(x => x);  // 创建衍生对象mapArr

    mapArr instanceof MyArray // false,因为Symbol.species已经被修改,所以mapArr不是MyArray的实例
    mapArr instanceof Array // true

  
  参考资料:
  《ES6标准入门》_阮一峰
  Symbol — MDN