symbol 类型用法介绍

密码朋克
• 阅读 1158

前言

ES6 更新了第 7 种基础数据类型 symbol,又称符号类型,用于作为全局唯一的表示符。并且以 Symbol 静态属性的方式向前端开发者暴露了可以修改 JS 内部语言行为的方法,用于更深层次操作对象。

声明方式

可以通过调用全局的 Symbol 函数来得到一个符号类型的变量,并且可以传递一个字符串类型的参数,用于表示对此符号的描述,可以通过 description 属性访问,但是两个描述相同的符号,并不相等。

const sy = Symbol('符号的描述')
console.log(sy) // Symbol(符号的描述)
console.log(sy.description) // '符号的描述'

const sy2 = Symbol('符号的描述')
console.log(sy2 === sy) // false
console.log(sy2.description === sy.description) // true

符号描述的意义只在于方便开发者调试,是没办法通过描述获取它对应的符号的。

Symbol作为构造函数来说并不完整,所以它不支持语法:new Symbol()

全局符号注册表

Symbol 有两个静态方法,用以维护一个全局符号注册表(可以视为一个 Map),实现了 symbol 与 description 间相互转换。

  • Symbol.for 同样可以创建一个符号,也接受一个字符串作为符号的描述。并将此符号加入全局注册表,如果下次通过此方法创建相同描述的符号,会直接返回已创建的符号。
  • Symbol.keyFor 检测一个符号是否在全局注册表中注册过,如果有,返回它的描述。

说实话,在实际开发中根本用不到这两个方法,不会存在符号与描述相互转换的需求,但为了内容的完整性,还是简单介绍了一下。

Symbol 的静态属性

Symbol 有许多静态属性,开发者可以通过配置它们改变 JS 的内部语言行为。在这里介绍其中较为常用的三个。

  • Symbol.iterator,用于为对象定义迭代器,其值应是一个生成器函数。在下文中的扩展遍历章节,会使用此属性极大扩展 for of 的功能。
  • Symbol.toStringTag,用于修改对象的字符串标签,其值应是一个函数,函数的返回值为字符串,一般用于区分类的实例。
  • Symbol.toPrimitive,用于为对象定义转换为原始值时的行为,接受一个参数表示转换倾向。详情可看 引用数据类型的转换规则

Symbol 还有许多静态方法,在平常开发中几乎用不到,这里不做介绍。

使用场景

Symbol 一般用于以下场景

定义类标签

可以在为类配置 Symbol.toStringTag 属性,很容易得到对象所属的类标签,使用 switch 进行匹配,比使用 instanceof 一次次比较尝试更方便。

class Pattern {
  get [Symbol.toStringTag]() {
    return 'Pattern'
  }
}
class Envelope {
  get [Symbol.toStringTag]() {
    return 'Envelope'
  }
}

const pattern = new Pattern()
const envelope = new Envelope()

console.log(Object.prototype.toString.call(pattern)) // '[object Pattern]'
console.log(Object.prototype.toString.call(envelope)) // '[object Envelope]'

当然,你也可以不依赖 Object.prototype.toString 方法,直接在类中通过其他属性定义它的类型。

class Pattern {
  get classType() {
    return 'Pattern'
  }
}
class Envelope {
  get classType() {
    return 'Envelope'
  }
}

const pattern = new Pattern()
const envelope = new Envelope()

console.log(pattern['classType']) // 'Pattern'
console.log(envelope['classType']) // 'Envelope'

消除魔术字符串

魔术字符串指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串。风格良好的代码中应该尽量消除魔术字符串。

以下实例中,classTypeEnvelopePattern 就是魔术字符串,它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

class Pattern {
  get classType() {
    return 'Pattern'
  }
}
class Envelope {
  get classType() {
    return 'Envelope'
  }
}

function drawItem(item) {
  const type = item['classType']
  switch (type) {
    case 'Pattern':
      break
    case 'Envelope':
      break
  }
}
function deleteItem(item) {
  const type = item['classType']
  switch (type) {
    case 'Pattern':
      break
    case 'Envelope':
      break
  }
}

常用的消除魔术字符串的方法就是把它写成一个变量。

我们并不关心这个变量的值是什么,只需要它全局唯一,这里使用 symbol 类型再合适不过。

const CLASS_TYPE = Symbol('classType')
const PATTERN = Symbol('Pattern')
const ENVELOPE = Symbol('Envelope')

class Pattern {
  get [CLASS_TYPE]() {
    return PATTERN
  }
}
class Envelope {
  get [CLASS_TYPE]() {
    return ENVELOPE
  }
}

function drawItem(item) {
  const type = item[CLASS_TYPE]
  switch (type) {
    case PATTERN:
      break
    case ENVELOPE:
      break
  }
}
function deleteItem(item) {
  const type = item[CLASS_TYPE]
  switch (type) {
    case PATTERN:
      break
    case ENVELOPE:
      break
  }
}

当项目大起来后,想要修改一个魔法字符串是真的痛苦,你需要在所有文件中搜索搜索这个字符串,逐个地修改。

如果你是通过变量来使用的话,虽然在使用中需要先导入这个变量,但在你需要修改的时候,只需要通过编辑器重命名符号的功能,便可轻松完成全局修改。

作者目前正在学习 Vue3,在其源码中,存在着大量 const XXX = Symbol('xxx') 的语句。

定义私有属性

symbol 可以用来定义真正意义上的私有属性。

在下面这个例子中,只要 Pattern.js 不导出符号变量,从外部无论如何也访问不到实例的私有属性。

// 类的定义文件 Pattern.js
const PRIVATE_VALUE = Symbol('privateValue')

export class Pattern {
  get [PRIVATE_VALUE]() {
    return '私有属性,你们访问不到'
  }
  fun() {
    console.log(this[PRIVATE_VALUE])
  }
}

// 在其他文件
import { Pattern } from 'Pattern.js'

const pattern = new Pattern()

console.log(pattern) // Pattern {}
console.log(Object.getOwnPropertySymbols(pattern)) // []
console.log(Object.keys(pattern)) // []
pattern.fun() // '私有属性,你们访问不到'

扩展遍历

ES6 新推出的 for of 遍历非常方便,但就是不能直接遍历对象,需要先调用 Object.keys 方法获取属性列表,才能遍历。

我们可以通过 Symbol.iterator 配合生成器,将 Object.keys 操纵提前进行,方便我们遍历对象。

Object.prototype[Symbol.iterator] = function* () {
  let keys = Object.keys(this)
  for (let i = 0; i < keys.length; i++) {
    // yield [keys[i], this[keys[i]]]
    yield keys[i]
  }
}

const obj = { a: 1, b: 2, c: 3 }

for (const key of obj) {
  console.log(key) // 'a' 'b' 'c'
}

// for (const [key, value] of obj) {
//   console.log(key, value)
// }

console.log([...obj]) // ['a', 'b', 'c']
console.log({ ...obj }) // {a: 1, b: 2, c: 3}

如果你想更简便一点,可以使用上面被注释掉的代码,一次性获取对象的键与值。

我们还可以扩展数字,替代手写传统的 for 循环。

Number.prototype[Symbol.iterator] = function* () {
  for (let i = 0; i < this.valueOf(); i++) {
    yield i
  }
}

const arr = [...5]

console.log(arr) // [0, 1, 2, 3, 4]

for (const i of arr.length) {
  console.log(arr[i]) // 0 1 2 3 4
}

禁止对象的类型转换

当你的代码中出现对象向基础数据类型转换时,这往往都是一个错误,而且也不利于代码的阅读与维护。

可以通过配置 Symbol.toPrimitive 属性让代码直接报错。

const obj = {}

console.log(obj == '[object Object]') // true

Object.prototype[Symbol.toPrimitive] = () => {
  throw Error('不允许对象向基础数据类型转换')
}

console.log(obj == '[object Object]') // Error: 不允许对象向基础数据类型转换

兼容性

除了 IE 浏览器这个前端大毒瘤,其他主流浏览器都已经实现了 symbol 的全部功能。

如果你的项目不需要兼容 IE,大胆的使用 symbol 方便你的开发吧。

结语

如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。

内容整理不易,如果喜欢或者有所启发,希望能点赞关注,鼓励一下作者。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
4年前
typeScript数据类型
//布尔类型letisDone:booleanfalse;//数字类型所有数字都是浮点数numberletdecLiteral:number6;lethexLiteral:number0xf00d;letbinaryLiteral:number0b101
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
4年前
NEO从源码分析看UTXO交易
_0x00前言_社区大佬:“交易是操作区块链的唯一方式。”_0x01交易类型_在NEO中,几乎除了共识之外的所有的对区块链的操作都是一种“交易”,甚至在“交易”面前,合约都只是一个小弟。交易类型的定义在Core中的TransactionType中:源码位置:neo/Core/TransactionType
密码朋克
密码朋克
Lv1
数点山浮空,四面天垂水。
文章
3
粉丝
0
获赞
0