你知道什么是Symbol了吗?

714 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言:在看本篇文章之前,相信各位读者或多或少都对ES6中新增的基础类型Symbol有自己的了解,那我就要问各位几个问题了:

  • symbol 是什么?
  • symbol 能做什么?
  • 为什么要 symbol? 请各位抱着这样的疑惑看下去,希望本文能有一定启发:)

symbol究竟是何方神圣?

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。 ---MDN/JavaScript/Reference/Global_Objects/Symbol)

  • symbol是JS中的一种基础类型, 且无论如何构造,也无法创建两个相同的symbol值。
// 构造函数如下
const a = Symbol()

为什么Symbol()不能使用new?

在思考这个问题之前,读者是否知道Number(1)new Number(1)的区别呢?

let num = Number(1); // num --> 1
let num2 = new Number(1); // num2 --> Number{1}
num2.valueOf(); // 1
num2.__proto__ === num.__proto__; // true

在理解两者之间的差异之后,你就能明白为何不能使用new来创建一个新的symbol类型的值了: 为了避免创建symbol原始值包装对象。理论上来讲,symbol值作为JS的原始类型而非对象,所以不能添加属性。

symbol能做什么呢?

简而言之,我们就是想要用它来做键名。

上代码:

    // a.js
    const value = 123;
    const func = function(obj){
      console.log('a');
      obj.value = value;
    }
    export {func};

    // b.js
    const value = 567;
    const func = function(obj){
      console.log('b');
      obj.value = value;
    }
    export { func };
    
    // c.js
    import {func} from'./a.js';
    import {func as func2} from'./b.js';
    const obj = {};
    func(obj);
    console.log('a', obj);
    func2(obj);
    console.log('b', obj);
    
    // a
    // a { value: 123 }
    // b
    // b { value: 567 }

实际上, 在symbol类型出现之前,对象的键名往往只有一个类型:string

    const obj = {};
    obj.foo = 'foo';
    obj['bar'] = 'bar';
    obj[2] = 2;
    obj[{}] = 'someobj';

    console.log(obj);
    // { '2': 2, foo: 'foo', bar: 'bar',
         '[object Object]': 'someobj' }

这就导致了我们无法避免变量名重名的问题,而在JS中,即使出现重名属性也不会返回一个error,而是后来者直接覆盖之前的值,这也就导致了某些时候调用报错的情况:本来某属性是一个方法,结果被覆盖之后成了一个string的值,再调用就会报错了。

由于每一个symbol值都是不相等的,这意味着symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

let a = Symbol();
let b = Symbol();
let c = Symbol('aaa');
let d = Symbol('aaa');
// --> a != b && c != d

Symbol()可以接受一个字符串作为参数,但这个字符串一般只适用于debug,对值本身可能不存在什么影响: symbol.description可以返回该字符串。

有了symbol之后,我们的代码就可以更新一下:

  // a.js
    const value = 123;
    const key = Symbol();
    const func = function(obj){
      console.log('a');
      obj[key] = value;
    }

    // b.js
    const value = 567;
    const key = Symbol();
    const func = function(obj){
      console.log('b');
      obj[key] = value;
    }
    
    // c.js
    import {func} from'./a.js';
    import {func as func2} from'./b.js';
    const obj = {};
    func(obj);
    console.log('a', obj);
    func2(obj);
    console.log('b', obj);
    
    // a
    // a { [Symbol()]: 123 }
    // b
    // b { [Symbol()]: 123, [Symbol()]: 567 }

可能有人会疑惑的认为键名变化可能会带来部分不良影响,但我想说的是, 我们在构造对象的时候,往往更加看重对象的键值,键名相当于索引,实际上的值其实无所谓的,所以相比与键名绝对唯一的优势而言其实这种问题无伤大雅。

为什么使用symbol ?

  • 预防属性名冲突
  • 创建匿名的对象属性

在之前的内容中我们已经重复强调过symbol类型的值的唯一性,这里暂且不表:我们就第二点来谈。

还记得我们怎么遍历对象的吗?

  • for...in, for...of
  • Object.keys(), Object.values(),Object.entries()
  • Object.getOwnPropertyNames()
  • JSON.stringify() 以上方法目前都不能遍历出属性名类型为symbol类型的值。 当然,世事无绝对,也存在部分方法可以找到所有的值。
  • Object.getOwnProtertySymbols()可以获取所有为symbol类型的属性名。
  • Reflect.ownKeys()可以返回所有类型的属性名。
// 以之前的obj为例
        console.log(obj);
        console.log('for ... in');
        for (let i in obj) {
          console.log(i, obj[i])
        }
        console.log('Object.keys');
        Object.keys(obj).forEach(i => console.log(i));
        console.log('Object.getOwnPropertyNames');
        Object.getOwnPropertyNames(obj).map(i => console.log(i));
        console.log('JSON');
        console.log(JSON.stringify(obj));
        console.log('Object.getOwnPropertySymbols');
        Object.getOwnPropertySymbols(obj).map(i => console.log(i));
        console.log('Reflect.ownKeys');
        Reflect.ownKeys(obj).map(i => console.log(i));
    
    //  here is answer
    //  obj --> { z: 1, [Symbol()]: 123, [Symbol()]: 567 }
    //  for ... in
    //  z 1
    //  Object.keys
    //  z
    //  Object.getOwnPropertyNames
    //  z
    //  JSON
    //  {"z":1}
    //  Object.getOwnPropertySymbols
    //  Symbol()
    //  Symbol()
    //  Reflect.ownKeys
    //  z
    //  Symbol()
    //  Symbol()

在JS类的私有属性成为标准之前,symbol类型的值作为属性可能是模拟的最合适的了。