本文深入解析ES6中Symbol类型的核心用途。Symbol作为唯一标识符,主要用于定义对象属性以避免命名冲突,如实现可迭代协议。文章详细探讨了利用Symbol创建常量枚举、隐藏私有属性、类型转换规则及包裹对象的方法,并介绍了Symbol.match、Symbol.toPrimitive等内置符号及跨Realm通信机制。
Symbol的基本概念与唯一性
Symbol的核心价值在于为对象属性提供唯一标识,以此声明对象所支持的功能。与字符串键名不同,Symbol具备唯一性,能有效避免命名冲突。这种机制提供了一种标识类型信息的独特方式,从概念上看,它类似于C++中的Traits机制。
解决的问题
解决功能判断与迭代问题
在JavaScript中判断对象是否支持特定功能时,通常需要进行鸭子类型测试(Duck Test)。例如,判断一个对象是否可按数组方式迭代,这类对象被称为Array-like。Lodash库中的判断逻辑如下:
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
ES6引入了@@iterator方法,所有支持迭代的对象(如Array、Map、Set)均需实现该方法。@@iterator的属性键为Symbol.iterator而非字符串。只要对象定义了Symbol.iterator属性,即可使用for...of语句进行迭代。示例如下:
if (Symbol.iterator in arr) {
for(let n of arr) console.log(n)
}
其他用例
上述示例中,Symbol标识了对象的可迭代性(Iterables),这是Symbol的典型应用场景。更多详情可参考ES6迭代器相关文档。此外,Symbol还可用于其他多种场景,例如:
利用Symbol实现常量枚举
常量枚举
JavaScript原生没有枚举类型,常量通常用字符串或数字表示。例如:
const COLOR_GREEN = 1
const COLOR_RED = 2
function isSafe(trafficLight) {
if (trafficLight === COLOR_RED) return false
if (trafficLight === COLOR_GREEN) return true
throw new Error(`invalid trafficLight: ${trafficLight}`)
}
- 我们需要仔细排列这些常量的值。如果不小心出现两个值重复,调试将变得非常困难,类似于#define false true引发的问题。
- 取值可能重复。如果另一处定义了BUSY = 1并误传,调用isSafe(1)时无法检测错误。理想的枚举概念应在此时抛出异常,但上述代码无法实现此功能。
Symbol提供了另一种解决方案:
const COLOR_GREEN = Symbol('green')
const COLOR_RED = Symbol('red')
即使字符串写错或重复也无所谓,因为每次调用Symbol()都会返回独一无二的值。这样可以确保所有isSafe()调用都传入这两个Symbol之一,从而避免值冲突。
隐藏私有属性
私有属性
由于JavaScript缺乏访问限制,过去常以下划线起始命名私有属性。这不仅无法真正隐藏属性名,还破坏了代码风格。利用Symbol可以隐藏这些私有属性:
let speak = Symbol('speak')
class Person {
[speak]() {
console.log('harttle')
}
}
以下多种访问方式均无法获取speak属性:
let p = new Person()
Object.keys(p) // []
Object.getOwnPropertyNames(p) // []
for(let key in p) console.log(key) // <empty>
然而,Symbol仅能隐藏这些函数,并不能阻止未授权访问。仍可通过Object.getOwnPropertySymbols()或Reflect.ownKeys(p)枚举出speak属性。
Symbol作为新基本类型的特性
新的基本类型
Symbol是一种新的基本类型,这使得JavaScript拥有7种类型:
- Number
- Boolean
- String
- undefined
- null
- Symbol
- Object
转换为字符串
Symbol支持symbol.toString()方法及String(symbol),但不能通过+运算符转换为字符串,也不能直接用于模板字符串输出。后两种情况均会产生TypeError,旨在防止将其误用为字符串属性名。
转换为数字
Symbol不可转换为数字。Number(symbol)或四则运算均会产生TypeError。
转换为布尔
Boolean(symbol)和取非运算均可正常执行。这便于判断对象是否包含特定属性。
包裹对象
Symbol是基本类型,但不能使用new Symbol(sym)将其包裹为对象,需使用Object(sym)。除判等不成立外,包裹对象的使用与原基本类型几乎相同:
let sym = Symbol('author')
let obj = {
[sym]: 'harttle'
}
let wrapped = Object(sym)
wrapped instanceof Symbol // true,真的是true!!!
obj[sym] // 'harttle'
obj[wrapped] // 'harttle'
内置Symbol与类型转换
常见的Symbol
文章开头的例子提到的Symbol.iterator是一个内置Symbol。除此之外,常见的内置Symbol还有:
Symbol.match
Symbol.match在String.prototype.match()中用于获取RegExp对象的匹配方法。我们可以改写Symbol.match标识的方法:
观察String.prototype.match()的表现,以下例子来自MDN:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match
class RegExp1 extends RegExp {
[Symbol.match](str) {
var result = RegExp.prototype[Symbol.match].call(this, str);
return result ? 'VALID' : 'INVALID';
}
}
console.log('2012-07-02'.match(new RegExp1('([0-9]+)-([0-9]+)-([0-9]+)')));
// expected output: "VALID"
Symbol.toPrimitive
对象在运算时通常会转换为"[object Object]",这是对象转换为字符串(基本数据类型)的默认行为,定义在Object.prototype.toString中。例如这个对象:
var count = {
value: 3
};
count + 2 // "[object Object]2"
该对象也表示一个数字,如何使其参与四则运算?为其添加Symbol.toPrimitive属性以改变其转换为基本类型的行为:
count[Symbol.toPrimitive] = function () {
return this.value
};
count + 2 // 5
更多内置Symbol请参考MDN文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#Well-known_symbols
跨Realm通信机制
跨Realm使用
JavaScript Realm指当前代码片段运行的上下文,包括全局变量(如Array、Date等全局函数)。打开新标签页、加载iframe或Worker进程时,会产生多个JavaScript Realm。跨Realm通信时,这些全局变量是不同的。例如,从iframe传递数组arr给父窗口,父窗口中arr instanceof Array为false,因为其原型属于iframe中的Array。
若一个对象在iframe中可迭代,则在父窗口中也应能被迭代。这就要求Symbol支持跨Realm,Symbol.iterator即为此例。若自定义的Symbol也需要跨Realm,请使用Symbol Registry API:
// 在 Symbol Registry 中注册一个跨 Realm Symbol
let sym = Symbol.for('foo')
// 获取 Symbol 的键值字符串
Symbol.keyFor(sym) // 'foo'
内置的跨Realm Symbol其实不在Symbol Registry中:
Symbol.keyFor(Symbol.iterator) // undefined
总结
以上所述是小编给大家介绍的ES6 Symbol 的用途,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!