自 ECMAScript 2015 起,引入了一种新的基本类型 Symbol
,在日常开发中,基本上用不到,使用场景很少,但是在面试中却深受面试官喜爱,一问一个不吱声。
今天我们就来了解一下这个 Symbol 类型。
Symbol
Symbol 和 Number、String 类似,是一个基本类型,表示一个独一无二的值,唯一的标识符。
可以使用 Symbol()
函数创建这种类型的值。
const sym = Symbol()
console.log(typeof sym) // 'symbol'
Object.prototype.toString.call(sym) // '[object Symbol]'
在 ES5 中,对象的属性名只有一种类型 String,现在 Symbol 类型的值也可以作为对象的属性名。
注意:
Symbol()
函数前不能使用new
关键字,否则会报错。
new Symbol() // Uncaught TypeError: Symbol is not a constructor
这是因为生成的 symbol 是一个基本类型的值,不是一个对象。它与 string 类似,可以理解为字符串的额外扩展。
Symbol
不支持隐式转换。
JavaScript 大部分值都能支持隐式转换。例如:我们可以 alert() 任何一个值,但 symbol 不行。
const sym = Symbol()
alert(sym) // Uncaught TypeError: Failed to execute 'alert' on 'Window': Cannot convert a Symbol value to a string
Symbol 的描述(description)
创建时,我们可以给 symbol 一个描述(也称为symbol名),description
是 Symbol
的一个静态属性。它是可选的,仅用于调试目的或转为字符串时进行区分,它访问的不是 symbol 本身。
const sym1 = Symbol("sym"); // sym1 是一个描述为 “sym” 的 symbol
const sym2 = Symbol("sym");console.log(sym1.description) // 'sym'
console.log(sym1 == sym2) // false
symbol 保证唯一,即使描述相同,它们的值也不一样。
如果想生成相同的 symbol 呢?也是可以的,ES6 提供了 Symbol.for()
方法,接受一个字符串作为参数,它会检索有没有以该参数为名称的 symbol,如果有直接返回值,否则重新创建一个。
Symbol.for()
创建的 symbol 会被登记在全局环境中以供搜索,我们可以通过 Symbol.keyFor()
返回已登记 symbol 的 key。
const sym1 = Symbol.for("symbol")
const sym2 = Symbol.for("symbol")console.log(sym1 == sym2) // true
console.log(Symbol.keyFor(sym1)) // 'symbol'
注意:
Symbol.For()
创建的 symbol 登记的 key,在全局作用域内都起作用。
function Sym() {return Symbol.for("symbol")
}const sym1 = Sym()
const sym2 = Symbol.for("symbol")
console.log(sym1 == sym2) // true
Symbol 的应用场景
1. 作为对象的键(key),防止属性值覆盖
在 ES5 中,一个对象如果出现了同名属性,后者会覆盖前者。
const obj = { a: 1, b: 2 }
Object.assign(obj, { a: 2 })
console.log(obj) // {a: 2, b: 2}
如果我们想要杜绝这种情况,避免意外覆盖的情况,这个时候就可以使用 ES6 中的 symbol
作为对象的键(key),因为它是唯一的。
const sym1 = Symbol("test")
const sym2 = Symbol("test")
const sym3 = Symbol("exam")
const obj = {[sym1]: "鱼钓猫",[sym2]: "的",[sym3]: "小鱼干"
}
console.log(obj) // {Symbol(test): '鱼钓猫', Symbol(test): '的', Symbol(exam): '小鱼干'}
但同时这会导致一个问题:
const obj = {cat: "鱼钓猫",fish: "小鱼干",[Symbol()]: "symbol"
}for(let k in obj) {console.log(k) // cat fish
}console.log(Object.keys(obj)) // ['cat', 'fish']
console.log(Object.getOwnPropertyNames(obj)) // ['cat', 'fish']
可以看到,正常的遍历或属性获取都会把 symbol
忽略掉。这里我们需要用 Object.getOwnPropertySymbol()
获取 symbol 键名。
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol()]
如果想要全部返回,我们可以使用一个全新的 API Reflect.ownKeys()
。它返回对象的所有属性的集合,包含常规键和 symbol。
console.log(Reflect.ownKeys(obj)) // ['cat', 'fish', Symbol()]
2. 定义类的私有属性和方法
JavaScript 是一门弱类型语言,它的类型没有强制性,是没有类似 Java
等面向对象语言的访问对象关键字 private
的,类上的所有变量或方法都是可以公开访问的。
而由于使用 symbol
作为键名,不会被常规方法(for...in
,for...of
)遍历得到,我们就可以基于这一特性,为对象定义一些非私有的、只用于内部的属性或方法,以达到保护私有属性的目的。
const exam = Symbol("exam")
class Test {constructor() {this[exam] = 0}add(v) {this[this[exam]] = vthis[exam]++}static sizeOf(instance) {return instance[exam]}
}const test = new Test() // 实例化一个 Test类
Test.sizeOf(test) // 0test.add("鱼钓猫") // test Test {0: '鱼钓猫', Symbol(exam): 1}
Test.sizeOf(test) // 1
Object.keys(test) // ['0']
Object.getOwnPropertyNames(test) // ['0']
Object.getOwnPropertySymbols(test) // [Symbol(exam)]
上面我们实例化一个 Test类 test
,它的属性 exam
是一个 symbol值,所以 Object.keys()
、Object.getOwnPropertyNames()
都无法获取到属性 exam
,这就形成了一个非私有的内部方法的效果。
3. 模块化机制
把 symbol
与模块化结合,可以更好的实现类的私有化。
exam.js
const EXAM = Symbol()class FishCat {constructor() {this.cat = "鱼钓猫"this.fish = "李东溪"this[EXAM] = 0}keys() {return Object.keys(this)}
}export default FishCat
tset.js
import FishCat from "./exam.js"const fishcat = new FishCat()
const sym = Symbol()/** 在这里无法访问到 exam.js 中的 symbol属性值 */
console.log(fishcat[Symbol()]) // undefined
console.log(fishcat.keys()) // [ 'cat', 'fish' ]/** 我们也可以在 test.js 中添加 symbol属性值 */
fishcat[sym] = "symbol"
fishcat.soul = "July"
console.log(fishcat.keys()) // [ 'cat', 'fish', 'soul' ]
当我们引入外部插件时,为了避免我们添加的属性方法和插件重写或意外操作,我们就可以使用 symbol值绑定,这样我们绑定的值就可以受到保护。
4. 生成器和迭代器
JavaScript 中使用了许多的系统 symbol,这些 symbol 可以通过 Symbol.*
来访问。我们可以使用它来改变一些内建行为,比如,我们可以使用 Symbol.iterator
来进行迭代操作,使用 Symbol.toPrimitive
来设置对象原始值的转换等等.
- 生成器
- function 后跟 *
- yield关键字 后跟返回值,可以同步也可以异步
- next() 进行迭代,返回一个对象,done 为 false 表示继续迭代,为 true 表示迭代完毕。
function* gen() {yield Promise.resolve("鱼钓猫");yield "小鱼干";yield true;yield { name: "一棵松" };
}
const mm = gen()
console.log(mm.next()); // { value: Promise { '鱼钓猫' }, done: false }
console.log(mm.next()); // { value: '小鱼干', done: false }
console.log(mm.next()); // { value: true, done: false }
console.log(mm.next()); // { value: { name: '一棵松' }, done: false }
console.log(mm.next()); // { value: undefined, done: true }
- 迭代器(Symbol.iterator)
伪数组都包含 Symbol.iterator
,可以通过它进行迭代遍历。
// 这些是一些伪数组,都包含 Symbol.iterator
let set = new Set([1, 1, 2, 3, 3])
let map = new Map()
map.set('name', "鱼钓猫的小鱼干")
function args() {console.log(arguments);
}
let list = document.querySelectorAll('div')function each(data) {// 调用迭代器方法let Ite = data[Symbol.iterator]()let next = { done: false }while(!next.done) {next = Ite.next()if(!next.done) console.log(next.value);}
}console.log(each(set)) // 1 2 3 undefined
console.log(each(map)) // [ 'name', '鱼钓猫的小鱼干' ] undefined
- 手动实现一个
Symbol.iterator
伪数组都是使用 Symbol.iterator
进行迭代,但是对象不是,我们可以手动实现一个。
let obj = {current: 0,max: 5,// 手动实现对象的 Symbol.iterator[Symbol.iterator]() {return {current: this.current,max: this.max,next() {if(this.current == this.max) { // 迭代结合return {value: undefined,done: true}} else {return {value: this.current++,done: false}}}}}
}
for(let val of obj) {console.log(val); // 0 1 2 3 4
}
console.log([...obj]); // [ 0, 1, 2, 3, 4 ]
console.log({...obj}); // { current: 0, max: 5, [Symbol(Symbol.iterator)]: [Function: [Symbol.iterator]] }
总结
Symbol
是 ES6 新增的一个基础类型,日常开发我们所用不多。
从技术上说,symbol 不是百分百隐藏的。有一个内建方法 Object.getOwnPropertySymbols()
允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj)
的方法可以返回一个对象的所有键,包括 symbol。但大多数库、内建方法和语法结构都没有使用这些方法。
参考:
https://juejin.cn/post/6846687598249771022#heading-5
https://zh.javascript.info/symbol#yin-cang-shu-xing