目前的JavaScript实现是基于2011年6月发布的ECMAScript 5.1(现代浏览器均已支持),它包括了我们在之前章节已经提到过的Array
类的实现。ECMAScript 6也包括了Set
类的实现,本章稍后会介绍它的用法。本章中,我们要实现的类就是以ECMAScript 6中Set
类的实现为基础的。
以下是我们的Set
类的骨架:
function Set { let items = {};}
有一个非常重要的细节,我们使用对象而不是数组来表示集合(items
)。但也可以用数组实现。在这里我们用对象来实现,稍微有点儿不一样,也学习一下实现相似数据结构的新方法。同时,JavaScript的对象不允许一个键指向两个不同的属性,也保证了集合里的元素都是唯一的。
接下来,需要声明一些集合可用的方法(我们会尝试模拟与ECMAScript 6实现相同的Set
类)。
add(value)
:向集合添加一个新的项。delete(value)
:从集合移除一个值。has(value)
:如果值在集合中,返回true
,否则返回false
。clear
:移除集合中的所有项。size
:返回集合所包含元素的数量。与数组的length
属性类似。values
:返回一个包含集合中所有值的数组。
6.2.1 has(value)
方法
首先要实现的是has(value)
方法。这是因为它会被add
、remove
等其他方法调用。下面看看它的实现:
this.has = function(value){ return value in items;};
既然我们使用对象来存储集合的值,就可以用JavaScript的in
操作符来验证给定的值是否是items
对象的属性。
但这个方法还有更好的实现方式,如下:
this.has = function(value){ return items.hasOwnProperty(value);};
所有JavaScript对象都有hasOwnProperty
方法。这个方法返回一个表明对象是否具有特定属性的布尔值。
6.2.2 add
方法
接下来要实现add
方法:
this.add = function(value){ if (!this.has(value)){ items[value] = value; //{1} return true; } return false;};
对于给定的value
,可以检查它是否存在于集合中。如果不存在,就把value
添加到集合中(行{1}
),返回true
,表示添加了这个值。如果集合中已经有这个值,就返回false
,表示没有添加它。
添加一个值的时候,把它同时作为键和值保存,因为这样有利于查找这个值。
6.2.3 remove
和clear
方法
下面要实现remove
方法:
this.remove = function(value){ if (this.has(value)){ delete items[value]; //{2} return true; } return false;};
在remove
方法中,我们会验证给定的value
是否存在于集合中。如果存在,就从集合中移除value
(行{2}
),返回true
,表示值被移除;否则返回false
。
既然用对象来存储集合的items
对象,就可以简单地使用delete
操作符从items
对象中移除属性(行{2}
)。
使用Set
类的示例代码如下:
let set = new Set;set.add(1);set.add(2);
出于好奇,如果在执行以上代码之后,在控制台(console.log
)输出items
变量,谷歌Chrome就会输出如下内容:
Object {1: 1, 2: 2}
可以看到,这是一个有两个属性的对象。属性名就是添加到集合的值,同时它也是属性值。
如果想移除集合中的所有值,可以用clear
方法:
this.clear = function{ items = {}; // {3}};
要重置items
对象,需要做的只是把一个空对象重新赋值给它(行{3}
)。我们也可以迭代集合,用remove
方法依次移除所有的值,但既然有更简单的方法,那样做就太麻烦了。
6.2.4 size
方法
下一个要实现的是size
方法(返回集合中有多少项)。这个方法有三种实现方式。
第一种方法是使用一个length
变量,每当使用add
或remove
方法时控制它,就像在上一章中使用LinkedList
类一样。
第二种方法,使用JavaScript内建的Object
类的一个内建函数(ECMAScript 5以上版本):
this.size = function{ return Object.keys(items).length; //{4}};
JavaScript的Object
类有一个keys
方法,它返回一个包含给定对象所有属性的数组。在这种情况下,可以使用这个数组的length
属性(行{4}
)来返回items
对象的属性个数。以上代码只能在现代浏览器中运行(比如IE9以上版本、Firefox 4以上版本、Chrome 5以上版本、Opera 12以上版本、Safari 5以上版本,等等)。
第三种方法是手动提取items
对象的每一个属性,记录属性的个数并返回这个数字。这个方法可以在任何浏览器上运行,和之前的代码是等价的:
this.sizeLegacy = function{ let count = 0; for(let key in items) { //{5} if(items.hasOwnProperty(key)) //{6} ++count; //{7} } return count;};
遍历items
对象的所有属性(行{5}
),检查它们是否是对象自身的属性(避免重复计数——行{6}
)。如果是,就递增count
变量的值(行{7}
),最后在方法结束时返回这个数字。
不能简单地使用
for-in
语句遍历items
对象的属性,并递增count
变量的值。还需要使用hasOwnProperty
方法(以验证items
对象具有该属性),因为对象的原型包含了额外的属性(属性既有继承自JavaScript的Object
类的,也有属于对象自身,未用于数据结构的)。
6.2.5 values
方法
values
方法也应用了相同的逻辑,提取items
对象的所有属性,以数组的形式返回:
this.values = function{ let values = ; for (let i=0, keys=Object.keys(items); i<keys.length; i++) { values.push(items[keys[i]]); } return values;};
以上代码只能在现代浏览器中运行。在本书中我们使用的测试浏览器是Chrome和Firefox,因此代码可以工作。
如果想让代码在任何浏览器中都能执行,可以用与之前代码等价的下面这段代码:
this.valuesLegacy = function{ let values = ; for(let key in items) { //{7} if(items.hasOwnProperty(key)) { //{8} values.push(items[key]); } } return values;};
所以,首先遍历items
对象的所有属性(行{7}
),把它们添加一个数组中(行{8}
),并返回这个数组。该方法类似于我们开发的sizeLegacy
方法,但我们添加一个数组,而不是计算属性个数。
6.2.6 使用Set
类
现在数据结构已经完成了,看看如何使用它吧。试着执行一些命令,测试我们的Set
类:
let set = new Set;set.add(1);console.log(set.values); //输出[/"1/"]console.log(set.has(1)); //输出trueconsole.log(set.size); //输出1set.add(2);console.log(set.values); //输出[/"1/", /"2/"]console.log(set.has(2)); //trueconsole.log(set.size); //2set.remove(1);console.log(set.values); //输出[/"2/"]set.remove(2);console.log(set.values); //输出
现在我们有了一个和ECMAScript 6中非常类似的Set
类实现。如前所述,也可以用数组替代对象,存储元素。既然我们在第2章、第3章和第4章都用过数组,知道有不同方式实现同样的东西,这也不错。