Symbol 对象是什么?

发布于 2022-07-01 14:12:37 字数 8683 浏览 1270 评论 4

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

-不疑不惑不回忆 2022-05-04 12:48:23

@yozman 嗯。确实, string 不是必要的。我说的可读只是因为变量名是大写的不好辨认

梦境゜ 2022-05-04 12:21:00

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

const ACTION_TYPE_NAME = 'ACTION_TYPE_NAME';

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

酷炫老祖宗 2022-05-04 07:30:24

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

毅然前行 2022-05-04 04:22:12

定义 redux action type 的时候

const GETLIST_SUCCESS = Symbol();
~没有更多了~

关于作者

三生一梦

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文