[Effective JavaScript 笔记]第48条:避免在枚举期间修改对象
注册列表示例
一个社交网络有一组成员,每个成员有一个存储其朋友信息的注册列表。
function Member(name){
this.name=name;
this.friends=[];
}
var a=new Member('钟二'),
b=new Member('张三'),
c=new Member('赵四'),
d=new Member('王五'),
e=new Member('阮六'),
f=new Member('耿七');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d,f);
搜索该网络意味着需要遍历该社交网络图。
通常通过工作集来实现。工作集以单个根节点开始,然后添加发现的节点,移除访问过的节点。
for...in遍历图
使用for...in循环来实现该遍历是很方便的。
Member.prototype.inNetwork=function(other){
var visited={};
var workset={};
workset[this.name]=this;
for(var name in workset){
var member=workset[name];
delete workset[name];
if(name in visited){
continue;
}
visited[name]=member;
if(member===other){
return true;
}
member.friends.forEach(function(friend){
workset[friend.name]=friend;
});
}
return false;
}
上面代码有什么问题嘛?在大多环境下可以工作,但有一些环境这段代码就不能工作了。
a.inNetwork(f);//false
这里为什么呢。这里说明for...in循环在运行时出错了错误,并没有要求枚举对象的修改与当前保持一致。事实上,ES对并发修改在不同js环境下的行为的规范留有余地。标准规定:
如果被枚举的对象在枚举期间添加了新的属性,那么在枚举期间并不能保证新添加的属性能被访问。
上面规范的实际后果:如果我们修改了被枚举的对象,则不能保证for...in循环的行为是可预见的。
另一种遍历图
自己管得循环控制。当使用循环时,应该使用自己的字典抽象以避免原型污染。可以将字典放置在WorkSet类中来追踪当前集合中的元素数量。
function WorkSet(){
this.entries=new Dict();
this.count=0;
}
WorkSet.prototype.isEmpty=function(){
return this.count===0;
};
WorkSet.prototype.add=function(key,val){
if(this.entries.has(key)){
return;
}
this.entries.set(key,val);
this.count++;
};
WorkSet.prototype.get=function(key){
return this.entries.get(key);
};
WorkSet.prototype.remove=function(key){
if(!this.entries.has(key)){
return;
}
this.entries.remove(key);
this.count--;
};
为了提取集合的任意一个元素,给Dict类添加一个新方法
Dict.prototype.pick=function(){
for(var key in this.elements){
if(this.has(key)){
return key;
}
}
throw new Error('empty dictionary');
};
WorkSet.prototype.pick=function(){
return this.entries.pick();
};
下面改写上一版本的inNetwork方法,这里使用while来循环。每次选择任意一个元素并从工作集中删除。
Member.prototype.inNetwork=function(other){
var visited={};
var workset=new WorkSet();
workset.add(this.name,this);
while(!workset.isEmpty()){
var name=workset.pick();
var member=workset.get(name);
workset.remove(name);
if(name in visited){
continue;
}
visited[name]=member;
if(member === other){
return true;
}
member.friends.forEach(function(friend){
workset.add(friend.name,friend);
})
}
return false;
};
其中pick方法是一个不确定性的例子。不确定性是指一个操作并不能保证使用语言的主义产生一个单一的可预见的结果。这个不确定性是因为for...in循环可能在不同的js环境中选择不同的枚举顺序。使用不确定性可能会使你的程序引入一个不可预测的元素。测试可能在某个平台通过,某些平台不通过,或同一平台不同时候,结果也可能不同。
工用列表算法
不确定性的来源是难以避免的,考虑使用一个确定的工作集算法替代方案。即工作列表算法。将工作条目存储到数组中而不是集合中,则inNetwork方法,将总是以相同的顺序遍历图。
Member.prototype.inNetwork=function(other){
var visited={};
var worklist=[this];
while(worklist.length>0){
var member=worklist.pop();
if(member.name in visited){
continue;
}
visited[memeber.name]=member;
if(member === other){
return true;
}
member.friends.forEach(function(friend){
worklist.push(friend);
})
}
return false;
};
这一版本inNetwork方法会确定性地添加和删除工作条目。无论发现什么路径,该方法对于连接的成员总是返回true,所以最终结果是一样的。
提示
当使用for...in循环枚举一个对象的属性时,确保不要修改对象
当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用while循环或经典的for循环来代替for...in循环
为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,例如数组,而不要使用字典
附录:示例完整版
function Member(name){
this.name=name;
this.friends=[];
}
Member.prototype.inNetwork=function(other){
var visited={};
var worklist=[this];
while(worklist.length>0){
var member=worklist.pop();
if(member.name in visited){
continue;
}
visited[member.name]=member;
if(member === other){
return true;
}
member.friends.forEach(function(friend){
worklist.push(friend);
})
}
return false;
};
//测试代码
var a=new Member('钟二'),
b=new Member('张三'),
c=new Member('赵四'),
d=new Member('王五'),
e=new Member('阮六'),
f=new Member('耿七');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d,f);
a.inNetwork(f);//true
a.inNetwork(d);//true
f.inNetwork(a);//false
[Effective JavaScript 笔记]第48条:避免在枚举期间修改对象的更多相关文章
- [Effective JavaScript 笔记] 第4条:原始类型优于封闭对象
js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //&q ...
- [Effective JavaScript 笔记] 第8条:尽量少用全局对象
初学者容易使用全局变量的原因 创建全局变量毫不费力,不需要任何形式的声明(只要在非函数里用var 你就可以得到一个全局变量) 写得代码简单,不涉及到大的项目或配合(写hello world是不会有什么 ...
- [Effective JavaScript 笔记]第24条:使用变量保存arguments对象
迭代器(iterator)是一个可以顺序存取数据集合的对象.其一个典型的API是next方法.该方法获得序列中的下一个值. 迭代器示例 题目:希望编写一个便利的函数,它可以接收任意数量的参数,并为这些 ...
- [Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法
js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+ ...
- [Effective JavaScript 笔记] 第5条:避免对混合类型使用==运算符
“1.0e0”=={valueOf:function(){return true;}} 是值是多少? 这两个完全不同的值使用==运算符是相等的.为什么呢?请看<[Effective JavaSc ...
- [Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码
函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式 ...
- [Effective JavaScript 笔记]第65条:不要在计算时阻塞事件队列
第61条解释了异步API怎样帮助我们防止一段程序阻塞应用程序的事件队列.使用下面代码,可以很容易使一个应用程序陷入泥潭. while(true){} 而且它并不需要一个无限循环来写一个缓慢的程序.代码 ...
- [Effective JavaScript 笔记]第51条:在类数组对象上复用通用的数组方法
前面有几条都讲过关于Array.prototype的标准方法.这些标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array. arguments对象 在22条中提到的函数argument ...
- [Effective JavaScript 笔记]第60条:支持方法链
无状态的API的部分能力是将复杂操作分解为更小的操作的灵活性.一个很好的例子是字符串的replace方法.由于结果本身也是字符串,可以对前一个replace操作重复执行替换.这种模式的一个常见用例是在 ...
随机推荐
- C/C++程序从编译到链接的过程
编译器是一个神奇的东西,它能够将我们所编写的高级语言源代码翻译成机器可识别的语言(二进制代码),并让程序按照我们的意图按步执行.那么,从编写源文件代码到可执行文件,到底分为几步呢?这个过程可以总结为以 ...
- Android Support Annotations :安卓注解快速上手
我们都知道,安卓资源文件都是int类型的ID来保存其引用,通过注解类型,可以让我们在写代码的时候,及时发现参数类型的错误,避免潜在的BUG,如下: 我们通过@LayoutRes指定了参数必须要是R.l ...
- Bootstrap3.0学习第二十一轮(JavaScript插件——工具提示)
详情请查看http://aehyok.com/Blog/Detail/27.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:ht ...
- 第二十七课:滚轮事件,mouseenter与mouseleave事件的修复
滚轮事件 jQuery核心库没有对mousewheel事件的差异性进行处理,但作为一个常用的事件,本文讲解一下. mousewheel事件只有火狐浏览器不支持.mousewheel用于取得滚动距离的属 ...
- Webbench网站压力测试
Webbench是有名的网站压力测试工具,能测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况.webBech的标准测试可以向我们展示服务器的 两项 内容:每秒钟相应请求数和每秒 ...
- bean标签
bean的uri的路径 bean标签是属于struts中的标签,使用要在 Struts 1.3 Libraries中 struts-taglib-1.3.8.jar 中META-INFtld ...
- Java-Vector
package 集合类.list类; import java.util.Vector; public class Vector类 { public static void main(String[] ...
- [转]ubuntu安装光盘修复grub-rescue引导失败问题
Reference:http://liujianqiao398.blog.163.com/blog/static/181827257201292775649815/ 1.步骤一 以试用方式进入ubun ...
- 洛谷P1656 炸铁路
题目描述 因为某国被某红色政权残酷的高压暴力统治.美国派出将军uim,对该国进行战略性措施,以解救涂炭的生灵. 该国有n个城市,这些城市以铁路相连.任意两个城市都可以通过铁路直接或者间接到达. uim ...
- java将数据写入到txt文件中(txt有固定的格式)
java将数据写入到txt文件中,这个应该对于学过java I/O的人来说是很简单的事情了,但是如果要将数据以固定的格式写入到txt文件中,就需要一定的技巧了. 这里举个简单的例子,以供参考: 比如我 ...