Skip to content

Symbol对象是什么 #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sunyongjian opened this issue Mar 31, 2017 · 6 comments
Open

Symbol对象是什么 #17

sunyongjian opened this issue Mar 31, 2017 · 6 comments
Labels

Comments

@sunyongjian
Copy link
Owner

sunyongjian commented Mar 31, 2017

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这些概念。

@yozman
Copy link

yozman commented Sep 14, 2017

定义 redux action type 的时候

const GETLIST_SUCCESS = Symbol();

@sunyongjian
Copy link
Owner Author

sunyongjian commented Sep 14, 2017

@yozman 为什么呢。 传不传 string,symbol 都是唯一的。string 增加了可读性

@yozman
Copy link

yozman commented Sep 14, 2017

@sunyongjian
一般情况下为了便于查找 type 是定义在一个文件里的
由于 string 是根据变量名来的,本身就保证了唯一性
所以用不用 Symbol 是一样的,
这里用 Symbol 的好处是可以不用写

const ACTION_TYPE_NAME = 'ACTION_TYPE_NAME';

这种愚蠢的代码。
况且可读性的话,变量名已经能体现出来了。
所以在 redux action type 这种场景下我觉得是多余的

@sunyongjian
Copy link
Owner Author

@yozman 嗯。确实, string 不是必要的。我说的可读只是因为变量名是大写的不好辨认 😓
所以我觉得 dva 不错。。。
你是怎么定义的... 这些 type

@yozman
Copy link

yozman commented Sep 14, 2017

@sunyongjian
变量名可以用驼峰这方面其实没啥限制,
全套的话必须 dva 丫,
如果纯用 redux 可以配合 redux-actions,
把聚焦点从 reducer 转到了 action

@vnues
Copy link

vnues commented Jul 18, 2019

const GETLIST_SUCCESS = Symbol();
如果这样子做 实际调试工具redux打印出来的为undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants