どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のシリーズでは、第5章の考察をお届けしました。
引き続き、第5章をお送りします。いよいよ後2回で終わりです。
目次と日程
第5章後半は「配列とディクショナリ」です。組み込みオブジェクトのArrayオブジェクトを操作するのかと思いきや、引き続きプロトタイプの考察です。
- 第1章 JavaScriptに慣れ親しむ
- 第2章 変数のスコープ
- 第3章 関数の扱い
- 第4章 オブジェクトとプロトタイプ
- 第5章 配列とディクショナリ
- 第6章 ライブラリとAPI設計
- 第7章 並行処理
LT大会は2021年11月28日です。がんばりましょう。
今日はさっそく1回目で、 「列挙の実行中にオブジェクトを変更しない」 をお届けします。
テキスト第5章「配列とディクショナリ」の項目48に対応しています。
今日のアジェンダ
- for in文の中で列挙を変更する
- WorkSetクラスを追加する
- Mapオブジェクトでリファクタリング
for in文の中で列挙を変更する
以下のような、友だちのネットワークがあるとします。
そして、それぞれのPersonについて調べたときに、ネットワーク内にいるかどうかというメソッドを考えていきます。
Georgeは、誰ともつながっていないので、ネットワーク内にいるかどうかのinNetwork()メソッドで、falseを返すはずです。
function myFunction7_48_01() {
const a = new Member('Alice');
const b = new Member('Bob');
const c = new Member('Carol');
const d = new Member('Dieter');
const e = new Member('Eli');
const f = new Member('Fatima');
const g = new Member('George');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d, f);
console.log(a.inNetwork(g)); //false
}
class Member {
constructor(name) {
this.name = name;
this.friends = [];
}
inNetwork(other) {
const visited = {};
const workset = {};
workset[this.name] = this;
for (const name in workset) {
const member = workset[name];
delete workset[name];
if (name in visited) continue;
visited[name] = member;
if (member === other) return true;
member.friends.forEach(friend => workset[friend.name] = friend);
}
return false;
}
}
しかし、本来は、ネットワーク内にいるはずの、AliceとFatimaの関係性も、falseを返してしまいます。
console.log(a.inNetwork(f)); //false
これは、for in文のループ内では、オブジェクトの列挙の変更(更新)が要求されない、という仕様によるものです。
上記のコードでは、for in文の中で、delete文による要素の削除を要求したのが原因です。
delete workset[name];
この問題を解決してみましょう。
WorkSetクラスを追加する
項目45で紹介されていた、Dictクラスは、Mapオブジェクトに読み替えてGAS化します。
まず、WorkSetクラスのプロパティを、Mapオブジェクトに差し替えます。
pick()メソッドも追加しています。
そして、完成したコードがこちらです。
function myFunction7_48_02() {
class Member {
constructor(name) {
this.name = name;
this.friends = [];
}
inNetwork(other) {
const visited = {};
const workset = new WorkSet();
workset.add(this.name, this);
while (!workset.isEmpty()) {
const name = workset.pick();
const member = workset.get(name);
workset.remove(name);
if (name in visited) continue; //同じメンバーを再訪問しない
visited[name] = member;
if (member === other) return true; //見つかった?
member.friends.forEach(friend => workset.add(friend.name, friend));
}
return false;
}
}
class WorkSet {
constructor() {
this.entries = new Map();
this.count = 0;
}
isEmpty() {
return this.count === 0;
}
add(key, val) {
if (this.entries.has(key)) return;
this.entries.set(key, val);
this.count++;
}
get(key) {
return this.entries.get(key);
}
remove(key) {
if (!this.entries.has(key)) return;
this.entries.delete(key);
this.count--;
}
pick() {
const keys = [...this.entries.keys()];
if (keys.length) return keys[0];
throw new Error('empty dictionary');
}
}
const a = new Member('Alice');
const b = new Member('Bob');
const c = new Member('Carol');
const d = new Member('Dieter');
const e = new Member('Eli');
const f = new Member('Fatima');
const g = new Member('George');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d, f);
console.log(a.inNetwork(g)); //false
console.log(a.inNetwork(f)); //true
}
Mapオブジェクトでリファクタリング
Mapオブジェクトの復習をしましょう。
WorkSetクラスのメンバーは、ほぼ、Mapオブジェクトのメンバーです。
と、いうことは、inNetWork()メソッドの問題は、Mapオブジェクトで解決できそうです。
function myFunction7_48_03() {
class Member {
constructor(name) {
this.name = name;
this.friends = [];
}
inNetwork(other) {
const visited = {};
const workset = new Map([[this.name, this]]);
while (workset.size) {
const name = [...workset.keys()][0]; // pick メソッドを代⽤したステートメント
const member = workset.get(name);
workset.delete(name);
if (name in visited) continue;
visited[name] = member;
if (member === other) return true;
member.friends.forEach(friend => workset.set(friend.name, friend));
}
return false;
}
}
const a = new Member('Alice');
const b = new Member('Bob');
const c = new Member('Carol');
const d = new Member('Dieter');
const e = new Member('Eli');
const f = new Member('Fatima');
const g = new Member('George');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d, f);
console.log(a.inNetwork(g)); //false
console.log(a.inNetwork(f)); //true
}
最後に、visitedsを配列にしたバージョンがこちらです。
function myFunction7_48_04() {
class Member {
constructor(name) {
this.name = name;
this.friends = [];
}
inNetwork(other) {
const visiteds = [];
const workset = new Map([[this.name, this]]);
while (workset.size) {
const name = [...workset.keys()][0]; // pick メソッドを代⽤
const member = workset.get(name);
workset.delete(name);
if (visiteds.map(visited => visited.name).includes(member.name)) continue;
visiteds.push(member);
if (member === other) return true;
member.friends.forEach(friend => workset.set(friend.name, friend));
}
return false;
}
}
const a = new Member('Alice');
const b = new Member('Bob');
const c = new Member('Carol');
const d = new Member('Dieter');
const e = new Member('Eli');
const f = new Member('Fatima');
const g = new Member('George');
a.friends.push(b);
b.friends.push(c);
c.friends.push(e);
d.friends.push(b);
e.friends.push(d, f);
console.log(a.inNetwork(g)); //false
console.log(a.inNetwork(f)); //true
}
まとめ
以上で、「列挙の実行中にオブジェクトを変更しない」をお届けしました。
次回は、「配列の反復処理には、for…inループではなく、forループを使おう」と、「配列コンストラクタよりも配列リテラルのほうが好ましい」 を、2本まとめてお届けします。