ES6 Symbol的特性与思考

先简单说说什么是Symbol

  • Symbol是ES6新增的基础数据类型,它的特点就是独一无二的,如同UUID一样;
  • Symbol是函数,通过调用Symbol函数来创建Symbol数据;
  • Symbol还是内置对象,提供一系列函数well-known Symbol方法来改变JS语言的内部行为;

Symbol的特性与使用

示例:创建Symbol数据

  • Symbol没有字面量的创建方式,也不能以new Symbol()构造函数的方式创建,只能通过调用Symbol([description])函数,或者Symbol.for()来创建。

// Symbol不允许以new关键的构造函数方式调用
new Symbol()
// Uncaught TypeError: Symbol is not a constructor

// 创建无描述的Symbol数据
let symbol1 = Symbol()

// 创建带描述的Symbol数据
let localSymbol Symbol('desc1')

// 在全局环境创建Symbol数据
let globalSymbol = Symbol.for('desc1')

// 全局注册表的Symbol和Symbol函数创建的Symbol是不一样的
console.log(localSymbol === globalSymbol)
// 输出:false

复制代码

特性:Symbol总是唯一的

  • 数据总是独一无二的,不仅在函数、模块甚至在window顶层作用域中都是唯一的
// 调用Symbol函数,在当前环境中(函数作用域/模块作用域)创建唯一的symbol数据,虽然toString输出的结果看起来一样,但两者是不相等的
let f1 = Symbol('flag')
let f2 = Symbol('flag')

console.log(f1,f2)
// 输出:Symbol(flag) Symbol(flag)

console.log(f1 === f2)
// 输出:false
console.log(f1 === 'flag')
// 输出false


// 调用Symbol.for(key)方法,在是全局环境中创建Symbol数据。
// 当调用方法是,会根据key名来进行幂等操作,发现不存在则创建,如果已存在则返回  
let lock = Symbol.for('flag')
let lockFlag = Symbol.for('flag')
console.log(lock === lockFlag)
// 输出:true

// Symbol()函数和Symbol.for()创建的Symbol数据不一样
console.log(f1 === lock)
// 输出:false

// 在全局环境下如果不想创建,只想查找,可通过Symbol.keyFor()方法
console.log(Symbol.keyFor('flag')) // flag
console.log(Symbol.keyFor('test')) // undefined但不会创建
复制代码

示例:使用Symbol来定义常量

  • 既然Symbol的特性是唯一标志,我们可以用Symbol来做常量。
  • 以前我们定义常量是这样婶的:
const FRUIT =  {
  APPLE: 'APPLE',
  BANANA: 'BANANA',
  STRAWBERRY: 'STRAWBERRY'  
} 

// 调用的时候,其实我们并不关心value是什么,只看key
console.log(FRUIT.APPLE)

//但万一有个傻子,加了一个菠萝,但值写成了苹果,判断就会炸裂
const FRUIT =  {
  APPLE: 'APPLE',
  BANANA: 'BANANA',
  STRAWBERRY: 'STRAWBERRY',
  PINEAPPLE: 'APPLE'  // 新增
} 

// 而通过Symbol定义的话,就会避免这样的问题
const FRUIT =  {
  APPLE: Symbol(),
  BANANA: Symbol(),
  STRAWBERRY: Symbol()  
}

function translate(FRUIT_TYPE){
  switch (FRUIT_TYPE) {
    case FRUIT.APPLE:
      console.log('苹果')
      break;
    case FRUIT.BANANA:
      console.log('香蕉')
      break;
    case FRUIT.STRAWBERRY:
      console.log('草莓')
      break;
    default:
      console.log('未匹配')
      break;
  }
}

translate(FRUIT.APPLE)
// 输出:苹果
复制代码

示例:使用Symbol来定义人名

  • 比如一个班级里面,想通过人名来作为唯一标识,但人名又没办法避免重复,通过Symbol来实现
const grade = {
  [Symbol('Lily')]: {
    address: 'shenzhen',
    tel: '186******78'
  },
  [Symbol('Annie')]: {
    address: 'guangzhou',
    tel: '183******12'
  },
  // 允许重复的名称
  [Symbol('Lily')]: {
    address: 'beijing',
    tel: '172******10'
  },
}
复制代码

特性:Symbol的类型判断和类型转换

  • 和String类型一样,Symbol类型可以通过typeof操作符进行类型判断
let symbol = Symbol()
console.log(typeof Symbol)
// 输出:symbol
复制代码
  • 但和String类型不一样的是,Symbol不会进行隐式的自动类型转换,所以不能直接进行字符串拼接运算和算术运算。但可以人为的进行显式类型转换,比如转成String、Boolean、Number、Object
let symbolUUID = Symbol('uuid')

// 不能直接进行字符串拼接操作
console.log(symbolUUID + '测试') 
// TypeError: Cannot convert a Symbol value to a string

// 也不能直接进行算数操作
console.log(symbolUUID + 1) 
// TypeError: Cannot convert a Symbol value to a number

// 但可以进行三目运算的boolean判断操作
console.log(symbolUUID ? '真' : '假')
// 输出:真

console.log(String(symbolUUID) + '测试')
// 输出:Symbol(uuid)测试
// 等价于symbolUUID.toString()


复制代码

特性:Symbol可作为对象的属性key名

  • 根据规范,Symbol类型可以作为数据单独存在,也可以作为对象的属性key名。并且,对象的属性key只能是字符串类型或者Symbol类型,没有别的数据类型可以作为属性key,Boolean不行,Number也不行。
  • 但值得注意的是,需要以{[SymbolKey]: value}数组括弧的方式来挂载。
// 作为对象的属性名
let desc = Symbol('desc')
let person = {
  name: 'huilin',
  sex: '男',
  [desc]: '职位:前端工程师'
}
// 或者可以这样赋值:person[desc] = '职位:前端工程师'
console.log(person)
// 输出:{name: 'huilin',sex: '男',Symbol('desc'): '职位:前工程师'}

复制代码
  • Symbol作为属性名时具有弱隐藏性
/*
 * 常规的方式获取对象属性,会自动忽略Symbol属性的键值对的
 */
// 上面的例子如果进行JSON.stringify()格式化操作,会忽略Symbol
console.log(JSON.stringify(person))
// 输出:{name: 'huilin',sex: '男'}  

// 同样的,像for循环这样的常规遍历操作,会忽略Symbol
for(key in person){
    console.log(key)
}
// 输出: name sex

// Object.keys()会忽略Symbol
console.log(Object.keys(person))
// 输出: [ 'name', 'sex' ]

// Object.getProperty()忽略Symbol
console.log(Object.getOwnPropertyNames(person))
// 输出:[ 'name', 'sex' ]


/*
 * 仅获取Symbol属性的键值对的方法
 */
console.log(Object.getOwnPropertySymbol(person))
// 输出:[ Symbol(desc) ]


/*
 * 同时获取常规属性和Symbol属性的方法
 */

console.log(Reflect.ownKeys(person))
// 输出:[ 'name', 'sex', Symbol(desc) ]
复制代码

示例:通过Symbol模拟对象的私有属性或者私有方法

  • 借助Symbol属性名的弱隐藏性,模拟私有属性
// Symbol类型的属性名
const id = Symbol()

class User {
  constructor(idVal, name, age){
    this[id] = idVal
    this.name = name
    this.age = age
  }

  checkId(id){
    return this[id] === id
  }
}

// 私有属性,外部实例不能直接获取
let u = new User('001', 'Jay', 40)
console.log(u.name, u.age, u[id])
// 输出:Jay 40 001

// 但是通过对外暴露的方法,能访问到私有属性
console.log(u.checkId('001')) // true
console.log(u.checkId('002')) // false


复制代码

示例:利用Symbol进行数据归集和整合

  • 拿张鑫旭大佬打听小美眉的例子来看,通常情况下两个对象合并,key相同则会覆盖:
let info1 = {
  name: '小雪',
  age: 24,
  job: '前端工程师',
  desc: '喜欢看电影,已经有交往对象'
}

let info2 = {
  desc: '喜欢小狗,住在南山区,上下班坐公交车'
}

// 由于使用desc是String作为key,key相同会覆盖
console.log(Object.assgin(info1,info2)) 
// 输出:{name: '小雪',age: 24,job: '前端工程师',desc: '喜欢小狗,住在南山区,上下班坐公交车'}
复制代码
  • 那改成用Symbol作为属性key名会怎样呢?Symbol不会进行覆盖的
let info1 = {
  name: '小雪',
  age: 24,
  job: '前端工程师',
  [Symbol('desc')]: '喜欢看电影,已经有交往对象'
}

let info2 = {
  [Symbol('desc')]: '喜欢小狗,住在南山区,上下班坐公交车'
}

// 通过Symbol作为key,合并会保留
console.log(Object.assgin(info1,info2)) 
// 输出:{name: '小雪',age: 24,job: '前端工程师',Symbol('desc'): '喜欢看电影,已经有交往对象', Symbol('desc'): '喜欢小狗,住在南山区,上下班坐公交车'}
复制代码
  • 可见,Symbol更关注的是value值,而不是key名。可以思考得出,Symbol的特性就是方便对数据进行归集和整合。
  • 拿现实中的例子来说吧,微信文章的点赞墙,数据值都是点赞,但记录不会被覆盖,用户的头像都会罗列出来;再比如签到簿,数据值是时间,很有可能是扎堆签到时间一样,但也不会被覆盖,而是把记录罗列进来。
  • 再回到JavaScript语法层面,可能大家会觉得,名字冲突这种事情,概率很低吧?有必要专门新增一个Symbol嘛?但是你想啊,ES6的Module,导入导出是可以起别名的;还有ES6的解构,可以直接获取对象的属性名到当前环境;这样你还觉得名字冲突的概率低吗?
  • 所以Symbol通过归集和整合的特性,针对基础框架版本升级时,便于同名的方法或者变量向下兼容。

系统Symbol

  • 除了自己创建Symbol标记之外,ES6还提供了一系列内置的well-know(众所周知)的Symbol标记,用于改变JavaScript底层API的行为
API desc
Symbol.hasInstance 当调用instanceof运算符判断实例时,会调用这个方法
Symbol.isConcatSpreadable 当调用Array.prototype.concat()时,判断是否展开
Symbol.unscopables 对象指定使用with关键字时,哪些属性会被with环境排除
Symbol.match 当执行str.match(obj)时,如果该属性存在会调用它,并返回方法的返回值
Symbol.replace 当执行str.replace(obj)时调用,并返回方法的返回值
Symbol.search 当执行str.search(obj)时调用,并返回方法的返回值
Symbol.split 当执行str.split(obj)时调用,并返回方法的返回值
Symbol.iterator 当对象进行for…of循环时,调用Symbol.iterator方法,返回该对象默认遍历器
Symbol.toPrimitive 当对象被转换为原始数据类型时调用,返回该对象对应的原始数据类型
Symbol.toStringTag 在该对象调用toString方法时调用,返回方法的返回值
Symbol.species 创建衍生对象时使用该属性

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享