注册列表示例

一个社交网络有一组成员,每个成员有一个存储其朋友信息的注册列表。

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条:避免在枚举期间修改对象的更多相关文章

  1. [Effective JavaScript 笔记] 第4条:原始类型优于封闭对象

    js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //&q ...

  2. [Effective JavaScript 笔记] 第8条:尽量少用全局对象

    初学者容易使用全局变量的原因 创建全局变量毫不费力,不需要任何形式的声明(只要在非函数里用var 你就可以得到一个全局变量) 写得代码简单,不涉及到大的项目或配合(写hello world是不会有什么 ...

  3. [Effective JavaScript 笔记]第24条:使用变量保存arguments对象

    迭代器(iterator)是一个可以顺序存取数据集合的对象.其一个典型的API是next方法.该方法获得序列中的下一个值. 迭代器示例 题目:希望编写一个便利的函数,它可以接收任意数量的参数,并为这些 ...

  4. [Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法

    js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+ ...

  5. [Effective JavaScript 笔记] 第5条:避免对混合类型使用==运算符

    “1.0e0”=={valueOf:function(){return true;}} 是值是多少? 这两个完全不同的值使用==运算符是相等的.为什么呢?请看<[Effective JavaSc ...

  6. [Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码

    函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式 ...

  7. [Effective JavaScript 笔记]第65条:不要在计算时阻塞事件队列

    第61条解释了异步API怎样帮助我们防止一段程序阻塞应用程序的事件队列.使用下面代码,可以很容易使一个应用程序陷入泥潭. while(true){} 而且它并不需要一个无限循环来写一个缓慢的程序.代码 ...

  8. [Effective JavaScript 笔记]第51条:在类数组对象上复用通用的数组方法

    前面有几条都讲过关于Array.prototype的标准方法.这些标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array. arguments对象 在22条中提到的函数argument ...

  9. [Effective JavaScript 笔记]第60条:支持方法链

    无状态的API的部分能力是将复杂操作分解为更小的操作的灵活性.一个很好的例子是字符串的replace方法.由于结果本身也是字符串,可以对前一个replace操作重复执行替换.这种模式的一个常见用例是在 ...

随机推荐

  1. NuGet更新引用Dll

    第一种 通过 "Add Library Package Reference..." 添加 点击 ‘Add Library Package Reference...’ , 搜索你要添 ...

  2. angular的DEMO(用来练习和顺便看看)

    inflector(辅助) 将用户输入的字符串转化成驼峰或者空格或者底线的小插件; 这个是一个小的过滤器, 平常也是用不到的, 合格是过滤器的代码: app.filter("inflecto ...

  3. 【web必知必会】—— DOM:四个常用的方法

    终于开始复习DOM的知识了,这一阵忙乎论文,基本都没好好看技术的书. 记得去年实习的时候,才开始真正的接触前端,发现原来JS可以使用的如此灵活. 说起DOM就不得不提起javascript的组成了,j ...

  4. photoshop将psd导出div+css格式HTML(自动)

    psd切片切好后,导出 web格式,存储时选择html.所有切片,然后,选择其他,选择自定,选择切片,选择生成css css命名有2种方式,根据ID和根据类,一般选择根据类(ID尽量少有,防止js要用 ...

  5. C# 全角符号转半角

    public static string SBCCaseToNumberic(string SBCCase) { char[] c = SBCCase.ToCharArray(); ; i < ...

  6. 图解Android - Zygote, System Server 启动分析

    Init 是所有Linux程序的起点,而Zygote于Android,正如它的英文意思,是所有java程序的'孵化池'(玩过星际虫族的兄弟都晓得的).用ps 输出可以看到 >adb shell ...

  7. office 软件常用备忘(删除线/冻结等)

    Word: 1 大纲显示/navigation display: 视图/View --> Navigation Pane (left) 2 Track changes: 显示删除线 / 修改地方 ...

  8. 【bzoj1562】 NOI2009—变换序列

    http://www.lydsy.com/JudgeOnline/problem.php?id=1562 (题目链接) 题意 给出一个序列(0~n-1),这个序列经过某个变换会成为另外一个序列,但是其 ...

  9. HDU #5507 GT and Strings

    这是AC自动机系列的第一篇 传送门 Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 262144/262144 K (Java/Othe ...

  10. KMP 算法总结

    KMP算法是基本的字符串匹配算法,但是代码实现上有一些细节容易错.这篇随笔将认真总结一下. KMP算法的核心是: The KMP algorithm searches for occurrences ...