※スマホ対応はしてません。

タグ: Set

配列で重複する項目を取り除くやつ4種。(配列とかおれおれAdvent Calendar2018 – 18日目)

カテゴリー: JavaScript

LINDORのAdvent Calendar(本物)の18日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 18日目

ありそうでないメソッド、それが重複排除です。

Underscore.jsやLodashには uniq() てのがあって、こんな感じ↓で使えます。

const arr0 = [11, 22, 11, 22, 33];
const arr1 = _.uniq(arr0);

JavaScriptにはありません。

Set を使う

単純な値ならこれが一番簡単。

Set は重複しない値を格納できるオブジェクトです。 ... で展開して配列オブジェクトへ再変換します。

const arr0 = [11, 22, 11, 22, 33];
const arr1 = [...new Set(arr0)];
console.log(arr1); // => [ 11, 22, 33 ]

includes() で確認する

これが一番素直。

includes() は配列が指定の要素を含むかどうか調べるやつです。 重複してなかったら追加するだけ。

const arr0 = [11, 22, 11, 22, 33];

const arr1 = arr0.reduce((a, v) => {
  if (!a.includes(v)) {
    a.push(v);
  }
  return a;
}, []);

console.log(arr1); // => [ 11, 22, 33 ]

reduce() に自信がなければ forEach() でも。

const arr1 = [];
arr0.forEach((v) => {
  if (!arr1.includes(v)) {
    arr1.push(v);
  }
});

some() でやる

Set も includes() もオブジェクトの場合は完全に同じインスタンスでないと反応しないので、その場合は some() がよろしいかと。

もっと自由度が高い。

const arr0 = [
  { id: '11', num: 1 },
  { id: '22', num: 2 },
  { id: '11', num: 3 },
  { id: '22', num: 4 },
  { id: '33', num: 5 },
];

const arr1 = arr0.reduce((a, v) => {
  if (!a.some((e) => e.id === v.id)) {
    a.push(v);
  }
  return a;
}, []);

console.log(arr1);
// [ { id: '11', num: 1 },
//   { id: '22', num: 2 },
//   { id: '33', num: 5 } ]

重複分は先に出てくるやつが優先です。

Map を使う

これもオブジェクトに対応。初見で「ぎょえー」てなりそう。

const arr0 = [
  { id: '11', num: 1 },
  { id: '22', num: 2 },
  { id: '11', num: 3 },
  { id: '22', num: 4 },
  { id: '33', num: 5 },
];

const arr1 = [...new Map(arr0.map((v) => [v.id, v])).values()];

console.log(arr1);
// [ { id: '11', num: 3 },
//   { id: '22', num: 4 },
//   { id: '33', num: 5 } ]

仕組みは Set と some() の組み合わせです、だいたい。

  1. map() でIDとオブジェクト本体の組み合わせへ変換
  2. 組み合わせを元に Map オブジェクトを作成。ここでID( Map のキー)が重複するものは排除)
  3. values() でオブジェクト( Map の値)のみの反復子を得る
  4. スプレッド構文 ... を伴う配列初期化子 [] で配列に。完成

重複分は後ろにあるやつが優先(上書き)です。

おしまい

なんか良いのある?

参考

WeakMap、WeakSetで「弱い参照」を使えるようになったぞ。(現代的JavaScriptおれおれアドベントカレンダー2017 – 21日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 21日目

概要

基本的な使い方は Map や Set と一緒です。キーに使われるオブジェクトの参照カウントを増やさないとかそういうアレで、「弱い参照」で保持します。メモリリーク対策とかに。

Map 、 Set との違い

基本は Map 、 Set と同様なんだけど、キーが弱い参照(後述)になります。

できること:

  • get() (WeakMap)
  • set() (WeakMap)
  • add() (WeakSet)
  • has()
  • delete()

できないこと:

  • size
  • forEach()
  • clear()
  • keys()
  • 他

キーはオブジェクトのみ

Map は何でもキーに使えたんだけど、 WeakMap はオブジェクトだけです。

まあ意味ないしね。

弱い参照?

JavaScript界隈では新しい概念のはず。

プログラム内で使ってる情報ってメモリに置かれるじゃないですか。あれって使われてる間は保持されてて、使われなくなったら消してくれるんですよ、誰かが。上位のプログラムが。

で、問題はどうやって「使われているか」を判断するかなんだけど、変数から参照されてる数を覚えておく、というのがよくある基本的な仕組みです。そんでその数を「参照カウンタ」とか呼んだりします。参照カウンタが0になったらもう誰も見てないから消しちゃおう、ていうね。

参照を数える様子。

変数はスコープが終わると消えて、参照カウンタが減って、他から参照されてなければ参照先の情報も消える、と。

弱い参照だと、その参照カウンタを増やさずにおくことができます。なので、弱い参照が残っていてもメモリ上の情報は消えてしまうこともあります。

WeakMapのキーは弱い参照

WeakMapのキーとしてのみ使われている場合、そのリソースは削除されます。

WeakMap objects are collections of key/value pairs where the keys are objects and values may be arbitrary ECMAScript language values. A WeakMap may be queried to see if it contains a key/value pair with a specific key, but no mechanism is provided for enumerating the objects it holds as keys. If an object that is being used as the key of a WeakMap key/value pair is only reachable by following a chain of references that start within that WeakMap, then that key/value pair is inaccessible and is automatically removed from the WeakMap. WeakMap implementations must detect and remove such key/value pairs and any associated resources.

WeakMapオブジェクトはkey/valueペアのコレクションです。keyはオブジェクト、valueはECMAScript言語の任意の値を取ります。WeakMapはあるkey/valueペアを格納しているかを、特定のkeyを用いて確認できるよう要求されるかもしれませんが、キーとして保持しているオブジェクトの列挙についての手順は提供されていません。もしWeakMapのkey/valueペアのキーとして使われているオブジェクトが、そのWeakMap内から開始する参照チェインを辿ってのみ到達できる場合、そのkey/valueペアはアクセスできず、また自動的にそのWeakMapから削除されます。WeakMapの実装は、このようなkey/valueペア及び関連リソースを検出し、削除する必要があります。

(強調は引用者。)

弱参照とか言ったけど、仕様書ではそういう単語は使ってないし、実装方法が任意なので参照カウンタが用いられているとも限らないです。たぶん使ってると思うんだけど。

値の方は普通の参照です。

継承して

仕様書によると「サブクラスとして使えるように設計されている」とのことです。

内部の初期処理が必要なので、コンストラクタで super 呼び出ししないと怒られます。

class ElementData extends WeakMap {
  constructor() {
    // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from deriv
  }
}

HTML要素をキーに

DOMノードだったら同じ内容で別インスタンスみたいなものってあんまり作られない感じがするので、キーにするのに良さそう。jQueryの data() みたいなやつ。

const elementData = new WeakMap();

const el = document.querySelector('#foo');
elementData(el, { text: '追加情報 ' });

console.log(elementData.get(el));

Reactとかでがんがん要素再生成しますーみたいな設計の場合は、たぶん識別子とか持ってるんじゃないかな。そんなら普通の Map で十分そう。いや自動削除ないか。

実際の利用ケース

Stack Overflowの解答から。

Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:

  • Keeping private data about a specific object and only giving access to it to people with a reference to the Map. A more ad-hoc approach is coming with the private-symbols proposal but that’s a long time from now.
  • Keeping data about library objects without changing them or incurring overhead.
  • Keeping data about a small set of objects where many objects of the type exists to not incur problems with hidden classes JS engines use for objects of the same type.
  • Keeping data about host objects like DOM nodes in the browser.
  • Adding a capability to an object from the outside (like the event emitter example in the other answer).

WeakMapを使わないとメモリリークを引き起こす利用ケース:

  • あるオブジェクトに関するプライベートな情報を保持し、Mapへの参照を経由してのアクセスのみを公開する。より直接的な手段としてプライベートシンボルが提案されているが、まだ時間かかりそう
  • ライブラリオブジェクトに関する情報を、ライブラリを変更したりオーバーヘッドを招かずに保持する
  • 小さなオブジェクトの組み合わせに関する情報を保持する。この種の多数のオブジェクトは、JSエンジンが同種のオブジェクトのために使う隠しクラスに関する問題を引き起こさないために存在する(訳注: ごめんイミフ)
  • ブラウザのDOMノードのような、ホストオブジェクトに関する情報を保持する
  • あるオブジェクトの外側から能力 (capability) を追加する(他の解答にあるイベントエミッタの例みたいに)

載せといてなんだけど正直何言ってんのかよくわかんない……。

その他

キー一覧機能の提供は禁止

速度問題に発展しがちなので、そういうのは実装しちゃだめ (must not) と仕様書に書かれてます。

(あれ、そういう理解で合ってるよね?)

参考

値だけ覚えておくならSetという手が。(現代的JavaScriptおれおれアドベントカレンダー2017 – 20日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 20日目

概要

Map のキーだけを保持する版です。

const set = new Set();
set.add(1);
set.add(2);

console.log(set.size);  // 2
console.log(set.has(2));  // => true

使い方

なんとなく Map と同じ感じ。ただし値を追加するのは set() じゃなくて add() になってます。

// 作成
const set = new Set();

// 追加
set.add(1);
set.add(2);
set.add(3);
set.add(1);  // 同じ値は無視される

// 有無
console.log(set.has(2));  // => true

// 削除
set.delete(2);
console.log(set.has(2));  // => false

// サイズ
console.log('Size:', set.size);  // 2

// 反復
set.forEach((value) => {
    console.log('forEach', value);
})

for (let value of set) {
    console.log('for-of', value);
}

// 値だけまとめて取得
const values = set.values();

// 全削除
set.clear();

keys() もある

ただし keys() と values() は完全に同じ関数です。

console.log(set.keys === set.values);  // => true

あと entries() もあって、なんか想像通りの変な感じの結果を返します。インターフェイスを統一する的なやつなんだろか。

その他

キー vs 値

Set の仕様内では “value” という表現を使ってるっぽい。でも重複しないという特性はキーっぽいよね。

参考