欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JS的第七种基本类型Symbols详细介绍

程序员文章站 2022-04-15 10:49:01
es6作为新一代javascript标准,已正式与广大者见面。 本期我们要讨论的symbols是个什么东西呢? 这里的symbols不是指的徽标。 也不是能在代码中使用的小图片。 它也不是代表其它任...

es6作为新一代javascript标准,已正式与广大者见面。

本期我们要讨论的symbols是个什么东西呢?

这里的symbols不是指的徽标。

也不是能在代码中使用的小图片。

它也不是代表其它任何东西的一个别名。

当然咯,symbols和cymbals(铜钹)完全是两回事。

(在过程中使用铜钹可不是一个好主意,吵到你炸!)

言归正传,什么是symbols呢?

它是javascript的第七种基本类型

自1997年javascript被标准化以来,它定义了六种基本类型。直到es6,js程序中任何一个值都属于以下几种类型之一。

undefined

null

boolean

number

string

object

每种类型都是一系列值的集。前五个都是有限集。当然,boolean类型只有true和false两个值,而且他们应该不会给boolean型增加新值了。其它类型的值基本上都是数字和字符串。理论上说numbers类型有18,437,736,874,454,810,627个值(包括了nan,nan是“not a number”的缩写)。string类型中可能的值就太多了,我算算大概有 (2144,115,188,075,855,872 − 1) &pide; 65,535个……当然,我这种算法不一定是精确的。

object是一个无限集,每一个object都是独一无二的。你随意打开一个web页面就会生成一大堆新的object。

es6 symbols也是一个集,但它的元素既不是字符串也不是对象。它是es6的新成员:第七种基本类型。

让我们来谈谈它的应用场景。

以一个简单的布尔型来举例

javascript中,有时候将一个对象中的数据扩展到其它某个对象中是十分方便的。

例如,假设你正在写一个js库,目的是使用css过渡让dom元素在屏幕上移动。你应该知道同时使用多个css过渡在同一个p上是行不通的。这会引起p不规律跳跃。你打算解决这个问题,不过首先你得想法知道这个元素是否正处在一个过渡中。

怎样来解决这个问题呢?

其中一种方式是使用css apis让来告诉你元素是否在位移过程中。但这未免有点杀鸡用牛刀了。你的库应该存储了移动状态:代码中触发过渡的时候就应该记录了!

你真正需要的是一种方法来跟踪记录哪些元素在过渡。你可以把过渡中的元素存在一个数组中。每当你的库触发一个元素的过渡之前,先检测那个元素是否在数组中。

遗憾的是,如果数组很大的话,遍历起来会很耗时。

在你看来最简单的方法其实是为元素设置一个标识:

if (element.ismoving) {

  smoothanimations(element);

}

element.ismoving = true;

这样也会有一些潜在的问题。无法避免的事实是代码中会用到这个dom的地方不止这一处。

其它代码中如果使用了for-in 或者 object.keys()会遍历dom的所有属性(会造成额外性能消耗)。

一些思维灵活的库作者会从技术方面考虑——你的库与其它库兼容性会很差。

一些思维灵活的库作者也会考虑扩展性——你的库扩展性也会很差。

js标准委员会将来也许会为所有元素提供一个.ismoving()的方法,那么你需要重构你的代码,那时候你就傻眼了。

当然,你可以用一个冗长或傻瓜式的字符串来作为属性名,只需确保不会和别的属性重名。

if (element.__$jorendorff_animation_library$please_do_not_use_this_property$ismoving__) {

  smoothanimations(element);

}

element.__$jorendorff_animation_library$please_do_not_use_this_property$ismoving__ = true;

代码写成这样也太虐待自己的眼睛了。

使用方法你可以生成一个理论上唯一的属性名:

// get 1024 unicode characters of gibberish

var ismoving = securerandom.generatename();

...

if (element[ismoving]) {

  smoothanimations(element);

}

element[ismoving] = true;

object[name]语法使你可以使用任何字符串作为属性名。所以这样是可行的:不会有命名冲突,看起来还清爽!

但是,这样会导致调试体验糟透了?当你使用console.log()来打印元素的这个属性时,你会看到一大段字符串的垃圾数据。并且,这样的属性不止一个吧?你将如何保持连续性?每次重新加载的时候它们都生成不同的属性名。

为什么要搞得这么复杂?我们要得仅仅是一个简单的布尔值而已!

symbols可以解决这个问题

symbols集中的值可以由程序创建和并作为属性的键来使用,也不用担心名称冲突。

var mysymbol = symbol();

调用symbol()来创建一个新的symbol值,它不会等同于其他值。

与字符串和数字一样,你可以使用symbol来作为属性值。因为它不等同于其它任何字符串,这个symbol-keyed属性可以确保不会与其它任何属性冲突。

obj[mysymbol] = "ok!";  // guaranteed not to collide

console.log(obj[mysymbol]);  // ok!

接下来这方法就可以解决上面我们所讨论的那种情况:

// create a unique symbol

var ismoving = symbol("ismoving");

...

if (element[ismoving]) {

  smoothanimations(element);

}

element[ismoving] = true;

关于这段代码的几个说明:

symbol(“ismoving”)中的“ismoving”被称作描述。它对调试很有用。当你使用console.log()就可以打印出对应的symbol值,如果你想把它转换为字符串(比如说在打印错误信息的时候)可以使用.tostring()。

element[ismoving]被称作symbol-keyed属性(使用symbol作为键的属性)。从字面意思就可以说明它就是使用symbol作为属性名而不是使用字符串。除去这一点,它和其它属性并没什么区别。

和数组元素一样,symbol-keyed属性不能通过圆点符号来获取值(obj.name 这样是不行的)。它的值必须通过方括号来获取。

通过symbol的值获取symbol-keyed属性值就很容易了。上面的例子展示了如何获取和设置element[ismoving],我们可以判断元素的ismoving状态了,如果有必要的话甚至可以删除ismoving状态。

另一方面,以上的前提是ismoving在当前作用域中。这体现了symbols的弱封装机制:一个模块可以创建几个symbols在对象中任意使用而不用担心与其它模块的属性冲突。

因为symbol键值是被设计来避免冲突的,所以javascript最基本的对象检测特性是会忽略symbol键值的。以for-in循环为例,循环只会遍历对象的字符串类型的键。symbol键直接被忽略过了。object.key(obj)和 object.getownpropertynames(obj) 也是这样运作的。但是sysmbols并不完全是私有的:可以使用新api——object.getownpropertysymbols(obj)将所对象的所有symbol键;另一个新api—— reflect.ownkeys(obj),将会同时返回string和symbol类型的键。(在以后的文章中我们将完整地探讨reflect api。)

在库和框架中symbols将会有很多用途,不久我们会看到,js语言本身对它也会有广泛的使用。

symbols确切定义是什么呢?

> typeof symbol()

"symbol"

symbols和其它基本类型大不一样。

从创建开始就是不可变的。你不能为它设置属性(如果你在严谨模式下尝试,会报类型错误)。它可以作为属性名。这是它的类字符串性质。

另一方面,每一个symbol都是唯一的。与其他的不同(就算他们的描述是一样的)你可以很容易地新创建一个。这是它的类对象性质。

es6 symbols与lisp和ruby中的更传统的symbols很类似,但是没有如此紧密地集成到语言中。在lisp中,所有的标识符都是symbols。在js中,标识符和大多数属性的键值的首先仍是字符串,symbols只是为开发人员提供了一个额外选择。

关于symbols的一个忠告:与js中的其它类型不同,它不能被自动转换为字符串。试图拼接symbol与字符串将会引起类型错误。

> var sym = symbol("<3");

> "your symbol is " + sym

// typeerror: can't convert symbol to string

> `your symbol is ${sym}`

// typeerror: can't convert symbol to string

你可以通过显示地将symbol转换为一个字符串来避免这个问题,通过string(sym)或者sym.tostring()。

symbols的三种形式

有三种方法来获取symbol。

call symbol()。我们已经讨论过这种方法了,每一次调用它都将返回一个唯一的symbol。

call symbol.for(string)。这种方法访问一组已经存在的symbol注册表。与通过symbol()来定一个唯一值不同的是,symbol注册表中的symbols是共享的。如果你调用symbol.for(“cat”)三十次,每一次返回都将是同一个symbol。在多页面或者单页面的多模块需要共享symbol时,这是很有效的方法。

使用标准中定义的symbol.iterator。标准委员会自己定义了几种symbols。每一种都有它的特殊意义。

如果你仍然不确定symbols是否对你有帮助,这最后一个章节会很有趣,因为证实了在实践中symbols是很有用的。

es6的文档中对通用symbols的使用是如何介绍的?

我们已经看过了es6是如何使用symbol来避免与已有代码命名冲突的。几周前,在关于迭代器的文章中,我们了解了循环(var item of myarray)是从调用myarray[symbol.iterator]()开始的。我提到这个方法以前的写法是myarray.iterator(),但是加了symbol以后向后兼容性会更好。

现在我们知道了symbols的用法和作用。那么就很容易理解为什么这样做和这样做的意义是什么。

这里还有其它几个es6使用通用symbols的场景。(这些特性在firefox中还没实现。)

使instanceof可扩展。在es6中,表达式object instanceof constructor被指定为构造函数的一个方法:constructor[symbol.hasinstance](object)。这表明它是可扩展的。

消除新特性和旧代码之间的冲突。这比较难理解,但我们发现一些es6的数组方法将会破坏旧网站的稳定性。其它的web标准也会有类似的问题:仅仅是添加新方法到浏览器中,已存在的网站就会受到影响。无论如何,造成这些不稳定性的主要原因主要是由动态作用域引起的。所以es6引入了一个特殊的symbol——symbol.unscopables,这个web标准可以用来防止某些方法被牵连到动态域中。

支持新的字符串匹配。在es5中,str.match(myobject)尝试将myobject转换为正则表达式对象。在es6中,首先检查myobject是否有myobject[symbol.match](str)方法。现在库就可以给任何有正则表达式对象的地方提供通用的解析类。

所讲到的这几个symbol的应用都不常见,很难看到这些特性本身对我们的日常代码有任何影响。从长远看就比较有意义了。通用symbols是javascript对于phppython中的__doubleunderscores的改进。标准委员会将来会添加新的hooks到js中,而不会有影响已有代码的风险。