[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
时间: 2024-08-09 22:18:42

[Effective JavaScript 笔记]第48条:避免在枚举期间修改对象的相关文章

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

js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //"number" typeof "s";//"string" typeof null;//"object":ECMAScript把null描述为独特的类型,但返回值却是对象类型,有点困惑. 可以使用Object.prototype.t

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

迭代器(iterator)是一个可以顺序存取数据集合的对象.其一个典型的API是next方法.该方法获得序列中的下一个值. 迭代器示例 题目:希望编写一个便利的函数,它可以接收任意数量的参数,并为这些值建立一个迭代器. 测试代码好下: var it=values(1,4,1,4,2,1,3,5,6); it.next();//1 it.next();//4 it.next();//1 分析:由于values函数需要接收任意多个参数,这里就需要用到上一节讲到的构建可变参数的函数的方法.然后里面的迭

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

js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+1}" 反射获取函数源代码的功能很强大,使用函数对象的toString方法有严重的局限性.toString方法的局限性ECMAScript标准对函数对象的toString方法的返回结果(即该字符串)并没有任何要求.这意味着不同的js引擎将产生不同的字符串,甚至产生的字符串与该函数并不相关. 如果函数

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

函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式传递给eval函数以达到同样的功能.程序员面临一个选择:应该将代码表示为函数还是字符串?毫无疑问,应该将代码表示为函数.字符串表示代码不够灵活的一个重要原因是:它们不是闭包. 闭包回顾 看下面这个图 js的函数值包含了比调用它们时执行所需要的代码还要多的信息.而且js函数值还在内部存储它们可能会引用

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

无状态的API的部分能力是将复杂操作分解为更小的操作的灵活性.一个很好的例子是字符串的replace方法.由于结果本身也是字符串,可以对前一个replace操作重复执行替换.这种模式的一个常见用例是在将字符串插入到HTML前替换字符串的特殊字符字母. function escapeBasicHTML(str){ return str.replace(/&/g,"&") .replace(/< /g,"<") .replace(/>/

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

前面有几条都讲过关于Array.prototype的标准方法.这些标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array. arguments对象 在22条中提到的函数arguments对象.它是一个类数组对象,并不是一个标准的数组,所以无法使用数组原型中的方法,因此无法使用arguments.forEach这样的形式来遍历每一个参数.这里我们必须使用call方法来对使用forEach方法. function highlight(){ [].forEach.call(argume

[Effective JavaScript 笔记]第68条:使用promise模式清洁异步逻辑

构建异步API的一种流行的替代方式是使用promise(有时也被称为deferred或future)模式.已经在本章讨论过的异步API使用回调函数作为参数. downloadAsync('file.txt',function(file){ console.log('file:'+file); }); 基于promise的API不接收回调函数作为参数.相反,它返回一个promise对象,该对象通过其自身的then方法接收回调函数. var p=downloadP('file.txt'); p.th

[Effective JavaScript 笔记]第46条:使用数组而不要使用字典来存储有序集合

对象属性无序性 js对象是一个无序属性集合. var obj={}; obj.a=10; obj.b=30; 属性a和属性b并没有谁前谁后之说.for...in循环,先输出哪个属性都有可能.获取和设置不同的属性与顺序无关,都会以大致相同的效率产生相同的结果.也就是说访问属性a和访问属性b,没有哪个访问更快之说.ES标准并未规定属性存储的任何特定顺序,甚至于枚举对象也未涉及.for...in循环会挑选一定的顺序来枚举对象的属性,标准允许js引擎自由选择一个顺序,它们的选择会微妙地改变程序行为.如要

[Effective JavaScript 笔记]第15条:当心局部块函数声明笨拙的作用域

嵌套函数声明.没有标准的方法在局部块里声明函数,但可以在另一个函数的顶部嵌套函数声明. function f(){return "global"} function test(x){ var result=[]; function f(){return "local";}//block-local if(x){ result.push(f()); } result.push(f()); return result; } test(true);//["loc