详解ES6 Symbol 的用途
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 还可以做很多其他事情,例如:
常量枚举
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 并不小心把 busy 传入,干脆 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.getownperpertysymbols(), reflect.ownkeys(p) 来枚举到 speak 属性。
新的基本类型
symbol 是新的基本类型,从此 javascript 有 7 种类型:
- number
- boolean
- string
- undefined
- null
- symbol
- object
转换为字符串
symbol 支持 symbol.tostring() 方法以及 string(symbol), 但不能通过 + 转换为字符串,也不能直接用于模板字符串输出。 后两种情况都会产生 typeerror,是为了避免把它当做字符串属性名来使用。
转换为数字
不可转换为数字。number(symbol) 或四则运算都会产生 typeerror。
转换为布尔
boolean(symbol) 和取非运算都 ok。这是为了方便判断是否包含属性。
包裹对象
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.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 使用
javascript realm 是指当前代码片段运行的上下文,包括全局变量,比如 array, date 这些全局函数。 在打开新标签页、 加载 iframe 或加载 worker 进程时,都会产生多个 javascript realm。 跨 realm 通信时这些全局变量是不同的,例如从 iframe 中传递给数组 arr 给父窗口, 父窗口中收到的 arr instanceof array 为 false,因为它的原型是 iframe 中的那个 array。
但是一个对象在 iframe 中可以迭代(iterable),那么在父窗口中也应当能被迭代。 这就要求 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 的用途,希望对大家有所帮助