Skip to content

Symbol对象是什么 #17

@sunyongjian

Description

@sunyongjian

Symbol对象

称符号对象,是es2015(也就是es6)中新添加的数据类型。通过Symbol()可以得到一个唯一的值,所以这个值很适合做标识符。

概念

Symbol() function 返回一个类型为symbol的值,Symbol有静态属性,原型上也有methods,但是缺少constructor,所以你不能new Symbol()来执行;

引用官方翻译,符号是一种特殊的、不可变的数据类型,它可以作为对象属性的标识符使用。符号对象是一个对的符号原始数据类型的隐式对象包装器。

  • 类型

    let sym = Symbol(); // Symbol中参数为字符串,用来描述。
    console.log(typeof sym); //"symbol"
    const sym1 = Symbol('abc');
    const sym2 = Symbol('cba');
    console.log(sym1,sym2); //Symbol(abc) Symbol(cba)
    //参数的作用就是描述,便于调试
    console.log(sym1.toString()) //'Symbol(abc)' 
  • 唯一性

    Symbol('foo') === Symbol('foo'); // false
  • new关键字

    const sym = new Symbol(); // TypeError

获取symbol类型值的三种方式

  • 通过Symbol对象

    也就是前面说的 let sym = Symbol();得到一个为一个的symbol值

  • Symbol.for(string)

    可以注册一个symbol,再次调用Symbol.for(string)会得到这个symbol值,区别于Symbol()是唯一的

    let sym = Symbol('abc');
    const sym1 = Symbol.for('abc');
    console.log(sym === sym1); //false
    console.log(sym1 === Symbol.for('abc')); //true
  • Symbol.iterator

    这个比较特别,得到的也是symbol类型的值,是es6中提出用到对象中,被for...of遍历所用,下面将进行详细介绍。

    由于这个值是标准规范提出,用于每个object中,所以应该是固定的值。

    console.log(Symbol.iterator === Symbol.iterator); //true

用途

在我看来,symbol更多是应用于es6规范中,由于它的值唯一的特性,可以解决变量名,属性名冲突的问题,并切Symbol提出了一些属性和方法,用于过渡以及实现一些特殊的用途,比如对象的迭代器,instanceof的拓展等等。

  • 栗子。像我们项目中大量的使用常量定义字符串,尤其是react+redux的项目中,我觉得用Symbol就不错。

    const GETLIST_SUCCESS = Symbol('get-list-success');
    
    const getList = () => dispatch({
       type: GETLIST_SUCCESS
     }
    )
    const list = function(state = {}, action) {
      switch(action.type):
        case GETLIST_SUCCESS:
          // code
    }

我这里只是举一个例子,这种标识性的Symbol可以用的地方。大家可以不必关注redux。

  • 另外,Symbol值可以做为对象的key值,这样就能保证不会出现同名的属性名,防止对象属性被覆盖。下面我们将介绍Symbol作为属性名的写法。

作为对象的key

  • 对象[]方括号的形式

    const obj = {}
    const sym = Symbol();
    obj[sym] = 'syj';
  • 对象内部定义

    const sym = Symbol();
    
    const obj = {
      [sym]: 'syj'
    }
  • 通过Object.defineProperty定义

    const sym = Symbol();
    const obj = Object.defineProperty({}, sym, {
      enumerable: true, //可枚举
      writable: true,  //可赋值运算符改变
      configurable: true, //可改变,可删除
      value: 'syj'
     }
    )

    注意,symbol作为属性名不能通过 . 的形式添加。

    通过上述三种方式,我们就可以向对象添加key不会重复的symbol值了。意味着我们可以创建非字符串类型的属性名称,以防止使用常规手段来探查这些名称。

  • symbol类型的key的遍历

    当我们用symbol设置了对象的key以后,他是不会被之前的for...inObject.keys()遍历出来的,需要用Object.getOwnPropertySymbols()获取,得到一个所有这个对象中的symbol属性名的数组。

    const sym1 = Symbol('1');
    const sym2 = Symbol('2');
    const obj = {
      [sym1]: 'syj',
      [sym2]: 'fy'
    }
    const ary = Object.getOwnPropertySymbols(obj);
    console.log(ary); //[ Symbol(1), Symbol(2) ]

内置的Symbol值

JavaScript内建的一些在 ECMAScript 5 之前没有暴露给开发者的符号,它们代表语言的内部行为。

  • Symbol.iterator属性

    我觉得是最重要的属性,它的提出使对象可以使用迭代器遍历,之前只有Array,String等,这种内置Symbol.iterator属性的可以使用迭代器。ECMAScript旨在使JS中使用一种方法,比如for...of就可以遍历序列数据,不需要关注它内部的数据结构。其实就是JS往C,JAVA语言靠拢的趋势吧。
    为了避免此部分过于重,我们后面会专门研究迭代器和生成器。

  • Symbol.hasInstance

    之前我们用instanceof的时候,比如a instaceof A其实就是调用的A对象中的Symbol.hasInstance属性,它指向一个内部方法,现在es6拓展出来,我们可以自己定义啦。

    先看看下面的代码,猜想输出什么?

    class MyClass{
      static [Symbol.hasInstance](num) {
        return num % 2 === 0
      }
      
      [Symbol.hasInstance](num) {
        return num % 2 === 0
      }
    }
    console.log(1 instanceof MyClass); // 序号(1)
    console.log(2 instanceof MyClass); // 序号(2)
    console.log(2 instanceof new MyClass()); // 序号(3)
    
    
    const obj = {
      [Symbol.hasInstance](obj) {
        return num % 2 === 0
      }
    }
    console.log(1 instanceof obj);// 序号(4)
    

    序号(1) MyClass类上有静态方法[Symbol.hasInstance], 1被当做参数传入function,返回结果false。 序号(2)同理,true。
    序号(3) 后面是实例化的对象,通过原型链查找到原型上[Symbol.hasInstance],然后传入2执行,true。序号(4)是普通对象内部存在这个方法,执行返回false。
    success

  • Symbol.match

    意味着我们'abc'.match(/a/)调用的就是RegExp[Symbol.match]。
    也就是String.prototype.match(regexp)等价于Regexp[Symbol.match](this)

    const obj = {
      [Symbol.match](string) {
        console.log(string); //'b'
        return 'abcdefg'.indexOf(string)
      }
    }
    console.log('b'.match(obj)); // 1
  • Symbol.replace

    String.prototype.replace(searchValue, replaceValue)等价于searchValue[Symbol.replace](this,replaceValue)

    let str = 'abc';
    console.log(str.replace(/a/,'A')); //'Abc'
    
    class regObj {
      [Symbol.replace](str, param) {
        console.log(str, param);
      //
      }
    }
    str.replace(new regObj, 'A')

    (划重点) 这里我们模拟了一个Reg对象,str调用replace的时候,第一个参数本是reg实例,而JS内置的class RegExp 上有[Symbol.replace],才调用对用方法。我们传入一个我们模拟的Reg的实例,而这个实例的所属类的原型,也有[Symbol.replace],所以就调用了我们定义的方法。console得以打印。

  • Symbol.split, Symbol.search以以上两个都是字符串中与正则有密切联系的,因为他们的参数都可以是正则表达式。具体代码我就不列举了。

    这些与正则表达式交互的方法,在 ES6 之前其实现细节是对开发者隐藏的,使得开发者无法将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个符号以及对应的方法,将原生行为外包到内置的 RegExp 对象上。引用 也就是我们可以模拟一个内置的RegExp对象,只要他有这四个Symbol属性,那字符串调用match等方法并传入这个对象的时候,就可以找到对应的方法了。

  • Symbol.isConcatSpreadable

    指向一个布尔值,可以使我们的对象被数组的concat拼接成数组,话不多说直接上例子

        const concatObj = {
          0: 'b',
          1: 'c',
          length: 2, // 不可缺少
          [Symbol.isConcatSpreadable]: true
        }
        const result = ['a'].concat(concatObj)
        console.log(result);  //['a', 'b', 'c']

    应该还是比较好理解的。

  • Symbol.toStringTag

    我们都知道,以前我们经常用Object.prototype.toString检测对象具体的数据类型,Array还是Object,因为他是更精确更准确的。区别于自带的toString方法,如array.toString()只能返回一个元素拼接的字符串。以至于之前看的很多js库都会有这样的检测类型的方法

    function isArray(value) {
      return Object.prototype.toString.call(value) === "[object Array]";
    }

    ES6 通过 Symbol.toStringTag 重定义了此行为,我们可以自定义它的返回结果,而不只是之前的几种。

    class Mime {
      constructor() {
        this.name = 'syj'
      }
      toString() {
        return this.name
      }
      get [Symbol.toStringTag]() {
        return 'Handsome'
      }
    }
    const me = new Mime()
    console.log(me.toString()); //'syj'
    console.log(Object.prototype.toString.call(me)); //'[object Handsome]'

    timg

  • Symbol.unscopables

    with语句本来是为了减少代码重复的,不过因为负面性能,难理解易出错,es6严格模式下已经禁止使用了。但是es6为了向下兼容,需要寻找方法使之前用with的代码能正常工作。

       console.log(Array.prototype[Symbol.unscopables])
       
      // {
      //    copyWithin: true,
      //    entries: true,
      //    fill: true,
      //    find: true,
      //    findIndex: true,
      //    keys: true,
      //    values: true
      // }

    这些es6的关键字都会被with环境排除,假如说有values的变量,不会访问到数组原型上。不过我几乎不用with,只做了解吧...

  • Symbol.toPrimitive

    该属性定义在JS数据类型的原型上,当发生基本数据类型的转化的时候,对象->基本数据类型,会调用此方法。此方法会接受一个参数,这个参数是被内部的运算类型规定的。参数有三种类型,number, string, default。直接上例子可能更清楚一些。

    const obj = {
      [Symbol.toPrimitive](value) {
        switch (value) {
          case 'number':
            return 11
            break;
          case 'string':
            return 'str';
          case 'default':
            return 'default'
          default:
            throw new Error();
        }
      }
    }
    //default:
    console.log(1 + obj);
    console.log(obj == 'default');
    console.log(obj == 1);
    //number:
    console.log(1 - obj);
    console.log(obj / 2);
    console.log(2 * obj);
    //str:
    console.log(String(obj));

    推荐去了解一下js中的强势类型转换和运算符的自动转换。

  • Symbol.species

    用于产生派生对象的构造器。指向当前对象的构造函数,存在Symbol.species会调用它对应的方法,使用该方法返回的函数当做构造函数。
    引用

    class MyArray extends Array {
      // 覆盖父类 Array 的构造函数
      static get [Symbol.species]() { return Array; }
    }

    总之这些东西在es6中提出,就是为了暴露它内部的实现,允许使用符号类型的原型属性来定义某些对象的基础行为。使我们可以通过模拟达到类似的效果。

    推荐阅读加深理解

iterator会在接下来的清明假期详细研究,新开一个md,包括Iteration protocols,iterable,iterator,generator这些概念。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions