假设你是开发人员,在网站上每个文本——例如“Ninjas for hire”——你需要创建对应的语言,例如日语,汉语,韩语等。这种集合,将key映射到指定的值上,在不同的编程语言中具有不同的名称,通常称为字典或Map。
在JavaScript中如何有效地管理这种定位呢?一种传统的定位方法是利用对象是属性名和属性值的特性。
创建如下字典:
上面的方法看似是一种完美的解决方案,但是这种方法并不可靠。
别把对象当做Map
<script type="text/javascript">
const dictionary = {
'ja': {
"Ninjas for hire": '1234456'
},
'zh': {
"Ninjas for hire": 'aaaaaaa'
},
"ko": {
"Ninjas for hire": 'bbbbbbb'
}
}
alert(dictionary.ja['Ninjas for hire'] === '1234456');
if (dictionary.ja['Ninjas for hire'] === '1234456') {
alert(dictionary.ja['Ninjas for hire']);
}
alert("before:" + dictionary.ja['constructor']);
if (typeof dictionary.ja['construtor'] === 'undefined') {
alert(dictionary.ja['constructor']);//别打错了constructor
}
</script>
运行结果:
因为试图访问constructor属性,这是在字典中未定义的单词。在本例中我们期望返回undefined。但是结果并非如此。
实际返回的是function Object() { [native code] }
这是为什么呢?每个对象都有原型,尽管定义新的空对象作为map,仍然可以访问原型对象的属性。原型对象的属性之一是constructor(回顾一下,constructor是原型对象的原型,指回构造函数本身),它正是造成混乱的罪魁祸首。
同时,对象的key必须是字符串。如果想映射为其它类型,它会默默转化为字符串,没有任何提示。
/**********************************************************************************************/
<script>
const firstElement = document.getElementById("firstElement");
const secondElement = document.getElementById("secondElement");
//定义空对象,使用映射存储html节点的额外信息
const map = {}
//存储第一个元素信息,并且验证是否正确存储
map[firstElement] = {data: 'firstElement'};
//alert(map[firstElement].data === 'firstElement');
if (map[firstElement].data === 'firstElement') {
alert("The firstElement is correctly mapped");
}
//存储第二个元素信息,并验证是否正确存储
map[secondElement] = {data: 'secondElement'};
if (map[secondElement].data === 'secondElement') {
alert("The second element is correctly overriden");
}
//第1个元素的映射关系无效
if (map[firstElement].data === 'firstElement') {
alert("But now the firstElement is overriden");
}
</script>
在上面的代码清单中,我们创建了两个HTML元素:firstElement个secondElement,通过docunment.getElementById方法从DOM中获取到这两个元素。为了存储每个元素的更多信息,就定义一个JavaScript对象。
<!-- 创建两个HTML元素,并分别通过document.getElementById获取-->
<div id="firstElement"></div>
<div id="secondElement"></div>
<script>
const firstElement = document.getElementById("firstElement");
const secondElement = document.getElementById("secondElement");
//定义空对象,使用映射存储html节点的额外信息
const map = {}
//存储第一个元素信息,并且验证是否正确存储
map[firstElement] = {data: 'firstElement'};
//alert(map[firstElement].data === 'firstElement');
if (map[firstElement].data === 'firstElement') {
alert("The firstElement is correctly mapped");
}
//存储第二个元素信息,并验证是否正确存储
map[secondElement] = {data: 'secondElement'};
if (map[secondElement].data === 'secondElement') {
alert("The second element is correctly overriden");
}
//第1个元素的映射关系无效
if (map[firstElement].data === 'firstElement') {
alert("But now the firstElement is overriden");
}
</script>
我们创建了两个HTML元素:firstElement和secondElement,通过document.getElementById方法从DOM中获取到两个元素。为了存储每个元素的更多信息,我们定义了一个Javascript对象:
const map = {}
然后使用HTML元素作为对象的key值,存储一些数据:
map[firstElement] = {data: 'firstElement'};
然后检查数据。对第二个元素执行相同的过程:
map[secondElement] = {data: 'secondElement'};
看起来一切很完美,成功将数据关联到HTML元素上。但是,如果访问第一个元素就会出现问题:
map[firstElement].data
预期是可以取回第一个元素的对应数据,但是事实并非如此,但会的其实是第二个元素对象。
上面代码的执行结果如下:
1.第一次然后检查数据
2.第二次检查元素
3.然而第三次获取元素就失败了
//第1个元素的映射关系无效,注意,只要改为if (map[firstElement].data === 'secondElement')就可以了。
if (map[firstElement].data === 'firstElement') {
alert("But now the firstElement is overriden");
}
这是因为对象的key必须是字符串,这意味着当视图使用非字符串类型如HTML元素作为key时,其值被toString方法静默转换为字符串类型。HTML元素转换为字符串后的值为[object HTMLDivElement],第1个元素的数据信息被存储在[object HTMLDivElement]属性中。接着,当试图为第二个元素创建映射时,发生了相同的过程。第2个元素也是HTML元素,也被转换成为字符串,对应的数据也被存储在[object HTMLDivElement]属性上,覆盖了第一个元素的值。由于这两个原因:原型继承属性以及key仅支持字符串,所以通常不能使用对象作为map。由于这种限制,ECMAScript委员会定义了一个全新的类型Map。
/************************************************************************************/
<script>
const ninjaIslandMap = new Map();//使用Map构造函数创建map
//定义3个ninja对象
const ninja1 = { name: "Yoshi"};
const ninja2 = { name: 'Hattori'};
const ninja3 = { name: "Kuma"};
//使用Map的set方法,建立两个ninja对象的映射关系
ninjaIslandMap.set(ninja1, {homeIsland: "Hongshu"});
ninjaIslandMap.set(ninja2, {homeIsland: "Hokkaido"});
//使用Map的get方法。获取ninja对象
if (ninjaIslandMap.get(ninja1).homeIsland === "Hongshu") {
console.log("The first mapping works");
}
if (ninjaIslandMap.get(ninja2).homeIsland === "Hokkaido") {
console.log("The second mapping works");
}
//验证第三个ninja不存在映射关系
if (ninjaIslandMap.get(ninja3) === undefined) {
console.log("There is no mapping for the third mappings");
}
//验证map中只存在前两个对象的映射,不存在第三个对象的映射
if (ninjaIslandMap.size === 2) {
console.log("we are creating two mappings");
}
//使用has验证map中是否存在指定的key
if (ninjaIslandMap.has(ninja1) && ninjaIslandMap.has(ninja2)) {
console.log("We have mappins for the first two nijas");
}
if (!ninjaIslandMap.has(ninja3)) {
console.log("But not for the third nija!");
}
//使用delete方法从map删除key
ninjaIslandMap.delete(ninja1);
if (!ninjaIslandMap.has(ninja1) && ninjaIslandMap.size === 1) {
console.log("There is no first ninja mapping any more!");
}
//使用clear方法完全清空map
ninjaIslandMap.clear();
if(ninjaIslandMap.size === 0) {
console.log("All mappings have been cleared!");
}
</script>
运行结果如下:
在本例中,调用Map的构造函数创建map:
const ninjaIslandMap = new Map();
然后创建三个ninja对象,分别命名为ninja1,ninja2,nija3.使用set方法:
ninjaIslandMap.set(ninja1, {homeIsland: "Hongshu"});
通过get方法获取前两个ninja对象的映射:
ninjaIslandMap.get(ninja1).homeIsland === "Hongshu"
只有前两个ninja对象存在映射,第三个对象不存在映射,因为第三个对象没有被set调用。当前map的状态如下图:
除了get和set方法以外,map还具有size属性以及has/delete方法。size属性告诉我们已经创建了多少个映射。
has方法用于判断指定的key是否存在:
ninjaIslandMap.has(ninja1) && ninjaIslandMap.has(ninja2)
delete方法用于删除映射:
ninjaIslandMap.delete(ninja1);
clear方法用于清空map:
ninjaIslandMap.clear();
key相等
const map = new Map();
const currentLocation = location.href;//使用内置的location.href属性获取当前页的URL
const firstLink = new URL(currentLocation);//创建两个当前页面的链接
const secondLink = new URL(currentLocation);
map.set(firstLink, {description: 'firstLink'});//分别为两个链接添加映射
map.set(secondLink, {description: 'secondLink'});
//尽管每个链接指向相同的值,但是仍然具有各自的映射
if (map.get(firstLink).description === 'firstLink') {
console.log("First link mapping");
}
if (map.get(secondLink).description === 'secondLink') {
console.log("Second link mapping");
}
if (map.size === 2) {
console.log("There are two mapping");
}
上面使用location.herf属性获取当前页面的URL。然后使用URL构造函数创建两个URL当前页面链接的对象。接着对每个链接对象关联描述信息。最后,检查映射是否正确创建。
常用JavaScript的同学不会觉得出乎意料:两个不同的对象创建不同的映射。但是,两个URL对象指向相同的URL地址:当前页面的地址。我们也许会怀疑两个对象应该相等。但是,在JavaScript中,我们不能重载相等运算符,虽然两个对象的内容相同,但是两个对象仍然不相等。
遍历map
使用map的一些优点:
可以确定map中只存在你放入的内容,可以使用任意类型的数据作为key等。但还有更多优点!
因为map是集合,可以使用forof循环遍历map。也可以确保遍历的顺序与插入的顺序一致。
//创建一个新的map
const directory = new Map();
directory.set("Yoshi", "12345678");
directory.set("Kuma", "126930946294");
directory.set("Hiro", "128489506000");
console.log('----------------------------------------');
//使用for...of循环遍历directory。每个元素具有两个值:key与value
for (let item of directory) {
if (item[0] !== null) {
console.log("Key:" + item[0]);
}
if (item[1] !== null) {
console.log("Value:" + item[1]);
}
}
console.log('----------------------------------------');
//可以使用内置的keys方法遍历所有的key
for (let key of directory.keys()) {
if (key !== null) {
console.log("Key:" + key);
if (directory.get(key) != null) {
console.log("Value:" + directory.get(key));
}
}
}
console.log('----------------------------------------');
//可以使用values方法遍历所有value
for (var value of directory.values()) {
if (value !== null) {
console.log("Value:" + value);
}
}
console.log('----------------------------------------');
运行结果:
在每个迭代中,每个元素是具有两个值的数组,第1个值是key,第二个值是value,也可以分别使用keys与values方法遍历key和value。
参考书籍《JavaScript忍者秘籍》