配列のユニーク化(オブジェクト要素対応)

配列の内容から重複を取り除きます。ユニーク化では、配列の内容がオブジェクトの場合もあります。その際は、同じか否かを判定するために一工夫が必要です。この関数では引数として、オブジェクトを文字列化する関数を、指定可能にしています。 2018-07-06

 配列の要素から、重複を除いたユニーク化をしたい場合は、時折あります。たとえばゲームで、「複数のエフェクトの予約を配列に格納した際、高速化のために、同じ座標のエフェクトは除去したい」などが考えられます。

 JavaScriptの場合、配列の要素が単純な数値や文字列の場合は、ユニーク化は簡単です。

 よくある方法としては、各要素を、オブジェクトのプロパティに設定して、値を「true」や「1」にして、重複を確認するというものがあります。

 しかし、要素がオブジェクトの場合は、一工夫が必要になります。

 要素がオブジェクトの際、このオブジェクトをプロパティに設定すると問題が出ます。プロパティ名として登録する際、内部処理で「toString」メソッドが呼び出されます。そして、その戻り値である「[object Object]」という文字列が、プロパティとして登録されます。

 この名前は全てのオブジェクトで同じため、ユニーク化すると1つの要素だけが抽出されることになります。

 そこで、このページで用意した、ユニーク化を行う「unique」関数には、第2引数として、オブジェクトを文字列化するための関数を指定可能にしています。

 また、何も指定しない場合は、単純に各要素を、オブジェクトのプロパティに設定して処理を行います。数値や文字列の配列の場合は、第2引数は不要でしょう。

 このようにして、数値や文字列だけでなく、オブジェクトを各要素に持つ配列も、ユニーク化することができます。

 ファイル構成は以下の通りです。>>ダウンロード

※ 初期化用関数「init」の処理については「Webブラウザ/Node.js 互換関数」をご覧下さい。

サンプル動作

sample/unique.js

'use strict';

(function() {
	const _t = init('com.crocro.util');

	// unique | 配列のユニーク化
	// fncは関数。オブジェクトの比較用に、文字列化して戻します。
	_t.unique = function(arr, fnc) {
		const o = {};

		if (typeof fnc === 'function') {
			return arr.filter(x => {
				x = fnc(x);
				if (o[x]) {return false}
				return (o[x] = true);
			});
		} else {
			return arr.filter(x => {
				if (o[x]) {return false}
				return (o[x] = true);
			});
		}
	};

	// 初期化用関数
	function init(p) {
		try {return module.exports = {}} catch(e) {}
		return ((o, p) => {
			p.split('.').forEach(k => o = o[k]||(o[k]={}));
			return o})(window, p);
	};
})();

解説

 ユニーク化の処理では、第2引数の「fnc」が関数かどうかを、「typeof fnc === 'function'」という判定で分岐させています。これは、パフォーマンスを上げるためです。第2引数が設定されていない場合は、無駄な処理を行わないようにしています。

 関数の場合は「x = fnc(x)」という処理を行っています。分岐した処理は、ここだけ違います。あとは同じです。

 配列の「filter」メソッドは、「true」を戻した要素のみを集めて、新しい配列を作成します。ここでは、「o[x]」が存在する場合は「false」を、「o[x]」が存在しない(undefinedの)場合は「true」を格納して、この「true」を戻り値にしています。

 サンプルでは「unique(arrSrc, JSON.stringify)」として、「JSON.stringify」を引数にしています。この関数は、オブジェクトを文字列にしてくれます。そのため、この文字列を元に、プロパティが存在するかを判定します。


 応用として、「fnc」関数を用いて、配列の「重複と見なしてよい要素」を間引くこともできます。たとえば以下のようにすれば、1桁目の数字を誤差と見なして、ユニーク化することができます。

sample2/node_sample.js

// モジュールの読み込み
const unique = require('./unique.js').unique;

// 実行
const arrSrc = [1, 2, 5, 12, 15, 23, 26, 27, 28, 32, 35,
	40, 55, 65, 66, 67, 79, 81, 83, 84, 90];

const arrDst = unique(arrSrc, x => x / 10 | 0);
console.log(arrDst);
// [ 1, 12, 23, 32, 40, 55, 65, 79, 81, 90 ]

 こうした方法を用いて、大量のキャラのうち、描画しないキャラを間引いたりできます。

unique | 配列のユニーク化

配列の要素を、重複のない一意なものにします。

@param {Array} arr - 配列。

@param {Function} fnc - 要素を文字列化するための関数。
無指定の場合は、単純な値の比較や、文字列化した結果(toStringの戻り値)で比較されます。

@return ユニーク化した配列。

サンプル

const unique = com.crocro.util.unique;
/*
Node.jsの場合は、こちら。
const unique = require('./unique.js').unique;
*/

const arrSrc = [
	{ name: 'cat'},
	{ name: 'cat'},
	{ name: 'cat', age: 1},
	{ name: 'cat', age: 2}
];

// 各要素を普通に文字列化すると「[object Object]」になり、
// 全て同じと見なされます
const arrDst0 = unique(arrSrc);
console.log(JSON.stringify(arrDst0, null, '  '));
// [
//   {
//     "name": "cat"
//   }
// ] 

console.log('\n----------\n');

// 文字列化する関数を別途用意すると、正しくユニーク化されます
const arrDst1 = unique(arrSrc, x => JSON.stringify(x));
console.log(JSON.stringify(arrDst1, null, '  '));
// [
//   {
//     "name": "cat"
//   },
//   {
//     "name": "cat",
//     "age": 1
//   },
//   {
//     "name": "cat",
//     "age": 2
//   }
// ]

sample/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>配列のユニーク化 - JavaScript実用サンプルコード解説付き</title>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js">
    </script>

    <script src="unique.js"></script>
    <script src="main.js"></script>
    <link rel="stylesheet" href="main.css">
  </head>
  <body>
    <textarea id="src"></textarea>
    <button id="exec">実行</button>
    <textarea id="dst"></textarea>
  </body>
</html>

sample/main.js

'use strict';

$(function() {
	// 変数の初期化
	const $src   = $('#src');
	const $dst   = $('#dst');
	const $exec  = $('#exec');
	const $reset = $('#reset');

	// 値のセット
	$src.val(String.raw`
const unique = com.crocro.util.unique;
const arrSrc = [
	{name: 'cat'},
	{name: 'cat'},
	{name: 'cat', age: 1},
	{name: 'cat', age: 2}
];

// 各要素を普通に文字列化すると「[object Object]」になり、
// 全て同じと見なされます
const arrDst0 = unique(arrSrc);
console.log(JSON.stringify(arrDst0, null, '  '));

console.log('\n----------\n');

// 文字列化する関数を別途用意すると、正しくユニーク化されます
const arrDst1 = unique(arrSrc, JSON.stringify);
console.log(JSON.stringify(arrDst1, null, '  '));
	`.trim());

	// コンソール ログのラップ
	const log = console.log;
	console.log = function() {
		for (let i = 0; i < arguments.length; i ++) {
			$dst.append(arguments[i] + ' ');
		}
		$dst.append('\n');
		log.apply(console, arguments);
	};

	// クリック時の処理
	$exec.click(() => {
		$dst.empty();
		eval($src.val());
	});
});

sample/main.css

html,
body {
	margin: 0;
	padding: 0;
}

textarea,
button {
	box-sizing: border-box;
	width: 100%;
	vertical-align:bottom;
}
textarea {
	height: 19em;
}

sample/node_sample.js

// モジュールの読み込み
const unique = require('./unique.js').unique;

// 実行
const arrSrc = [
	{name: 'cat'},
	{name: 'cat'},
	{name: 'cat', age: 1},
	{name: 'cat', age: 2}
];

// 各要素を普通に文字列化すると「[object Object]」になり、
// 全て同じと見なされます
const arrDst0 = unique(arrSrc);
console.log(JSON.stringify(arrDst0, null, '  '));

console.log('\n----------\n');

// 文字列化する関数を別途用意すると、正しくユニーク化されます
const arrDst1 = unique(arrSrc, JSON.stringify);
console.log(JSON.stringify(arrDst1, null, '  '));

紹介

Steamでゲームをリリースした時の経験をマニュアル的にまとめた本です。
8bit風RTS「TinyWar」のアルゴリズムを、コード付きで解説した本です。
JavaScriptから手軽に扱える形態素解析器『kuromoji.js』を使い、日本語を分解して遊ぶ本です。
node.jsを使い、「Google Chrome」のユーザーデータを、自動処理でメンテナンスするプログラムを開発する本です。
HTML5でローカルアプリが作れるNW.jsで、同人ゲームを作るための基礎知識の本です。
シミュレーションRPG「TinySRPG」のアルゴリズムを、コード付きで解説した本です。
JavaScriptの実行時エラーを分類して、ワンライナーのソースコードとエラーメッセージを収録した本です。
禁止文字つきコードゴルフを1年以上出題して、その解答ノウハウをまとめた本です。
2011年の春ごろに、Javaで「NyARToolKit」互換のARマーカー認識プログラムを書いた時のレポートです。