【JavaScript】クイックソートを実装してみた

頭の体操がてらクイックソートを実装してみた。現時点ではchromeだとビルトインのArray.sortより速い。FireFoxだとArray.sortの方が約10倍速い。

クイックソート

まず一つめ。アルゴリズムはこの動画とほぼ同じで、軸要素は配列の中心にしている。破壊的な関数。

function quickSort(array) {
	(function sort(start, end) {
		//再帰出口;
		if(start >= end) {
			return;
		}
		var left = start;
		var right = end;
		var reference = array[Math.round((left + right) / 2)];
		//左から値を見ていく;
		//最終的にleftとrightは同じindexになるか交差する;
		//交差した場合はleft > rightになる;
		while(left < right) {
			if(array[left] >= reference) {
				while(right > left) {
					if(array[right] <= reference) {
						var tmp = array[left];
						array[left] = array[right];
						array[right] = tmp;
						right--;
						break;
					}
					right--;
				}
			}
			left++;
		}
		if(array[right] > reference) {
			sort(start, right - 1);
			sort(right, end);
		}else if(array[right] < reference) {
			sort(start, right);
			sort(right + 1, end);
		}else {
			//array[right]がreferenceと同じ値の時の処理;
			sort(start, right - 1);
			sort(right + 1, end);
		}
	})(0, array.length - 1);

	return array;
}

次に二つめ。アルゴリズムはこのサイトを参考にして書いた。一番左を軸要素にしている。破壊的な関数。

function quickSort2(array) {
	(function sort(start, end) {
		if(start >= end) {
			return;
		}
		var reference = array[start];
		var left = start + 1;
		var right = end;

		while(left < right) {
			if(array[left] > reference) {
				while(right > left) {
					if(array[right] <= reference) {
						var tmp = array[left];
						array[left] = array[right];
						array[right] = tmp;
						right--;
						break;
					}
					right--;
				}
			}
			left++;
		}
		var center;
		if(array[right] > reference) {
			center = right - 1;
		}else {
			center = right;
		}
		array[start] = array[center];
		array[center] = reference;

		sort(start, center - 1);
		sort(center + 1, end);
	})(0, array.length - 1);

	return array;
}

速度はchromeだとquickSort2, quickSort, Array.sortの順で、FireFoxだとArray.sort, quickSort2, quickSortだった。

テストコード

以下がテストコード。引数にソートする配列の桁数を指定する。まずは10000あたりで比べてみると良さげ。

function quickSortTest(num) {
	var list = [];
	var list2 =[];
	var list3 =[];
	var i = 0;
	while(i < num) {
		var randomNum = Math.floor(Math.random() * num);
		list.push(randomNum);
		list2.push(randomNum);
		list3.push(randomNum);
		i++;
	}
	console.time('quickSort');
	quickSort(list);
	console.timeEnd('quickSort');

	console.time('quickSort2');
	quickSort2(list2);
	console.timeEnd('quickSort2');

	console.time('Array.sort');
	list3.sort(function(a, b) {
		return a - b;
	});
	console.timeEnd('Array.sort');
}

クイックソート以外にも、簡単なソートアルゴリズムを実装したので以下に載せる。

バブルソート

function bubbleSort(array) {
	for(var i = array.length; i > 0; i--) {
		for(var j = 0; j < i - 1; j++) {
			if(array[j] > array[j+1]) {
				var tmp = array[j];
				array[j] = array[j+1];
				array[j+1] = tmp;
			}
		}
	}
	return array;
}
function bubbleSort2(array) {
	for(var i = 0, len = array.length; i < len; i++) {
		for(var j = i + 1; j < len; j++) {
			if(array[i] > array[j]) {
				var temp = array[i];
				array[i] = array[j];
				array[j] = temp;
			}
		}
	}
	return array;
}

選択ソート

function selectionSort(array) {
	var box = [];
	for(var i = 0; i < array.length; i++) {
		box[0] = array[i];
		box[1] = i;
		for(var j = i + 1; j < array.length; j++) {
			if(box[0] > array[j]) {
				box[0] = array[j];
				box[1] = j;
			}
		}
		var tmp = array[i];
		array[i] = box[0];
		array[box[1]] = tmp;
	}
	return array;
}

選択ソートは一々交換しないのでバブルソートより速い。

SVGで円グラフを描く

SVGのpath要素を使って円グラフを描く方法を書いていく。

円グラフは扇形の集まりなので、まず扇形を描く方法を知る必要がある。扇形を描くためにはSVGのpath要素を使う。(path要素について分からない方はMDNのチュートリアルのPath項目を一読することをオススメする。)

Path要素で扇形を描く手順

扇形を描くために必要なpath要素のコマンドは「M」と「L」と「A」と「Z」である。

Mは”Move to”の略で、指定した座標の位置へ移動する。まずこのコマンドで、円の中心点の座標を指定する。

Lは”Line to”の略で、現在位置の座標から指定した座標へ線を引く。このコマンドで、円の中心点から円弧の描き始めの点、つまり扇形の一つの半径を描く。このコマンドに指定する引数は、円弧の描き始めの座標である。

Aはarc要素で、円弧を描く。このコマンドが一番重要である。半径や円弧の描き終わりの座標など引数に色々と指定しないといけないが、後に詳しく説明する。

Zは”Close path”というコマンドで、現在の座標から最初に指定した座標まで線を引く。このコマンドで円弧の描き終わりの点から中心点までの線、つまり扇形のもう一つの半径を描く。

コマンドはpath要素のd属性にて使う。以下具体例を示す。

<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px">
	<path fill="#6d5450" d="M 150 150
		L 150 0
		A 150 150 0 0 1 300 150
		Z" />
</svg>

See the Pen ZYymJq by shigure (@webkatu) on CodePen.

下はこのコマンドの動きを目で追った画像である。

Aコマンドは以下の7つの引数を指定する。

rx ry x-axis-rotation large-arc-flag sweep-flag x y

第一引数はx軸の半径、第二引数はy軸の半径で、楕円を描かないのであれば同じ値を指定する。第三引数は円弧の回転度を指定できるが、ここでは必要ないので0を指定する。

第四引数と第五引数を飛ばして先に第六引数と第七引数を説明する。第六引数は円弧の描き終わりのx座標、第七引数はy座標である。

Aコマンドは現在位置の点と第六引数と第七引数で指定した座標の点を繋いで円弧を描くが、下の画像のように2つの点の繋ぎ方には4種類あり、どの円弧を描くか指定しないといけない。そこで、第四引数と第五引数を使う。

http://www.hcn.zaq.ne.jp/___/SVG11-2nd/paths.htmlより引用

第四引数は円弧が180度以上かどうかを指定する。180度より小さければ0、大きければ1を指定する。ちなみに180度ちょうどはどちらでもよい。

第五引数は2つの点を反時計回りで繋ぐか時計回りで繋ぐかの違いで、今回は時計回りで統一し常に1を指定する。

ここまで、path要素での扇形の描き方の概要を書いてきた。あくまでもpath要素はコマンドを用意してるだけで、角度によって変わる円弧の描き始めの点や円弧の描き終わりの点の座標は三角関数を使って自分で算出しなければいけない。

三角関数を使って座標を導く

ただ単に下の三角比の定義さえ分かっていればよくて、それをプログラムに落とし込めば座標は導ける。

この定義を円に当てはめると以下のようになる。今回は12時の方向を0度として時計回りに描くためこのようになる。

まず、円の中心Oからの距離XとYを三角関数で求める。座標は左上が(0, 0)なので、X’は円の中心のx座標にXを足せば導ける。Y’は円の中心のy座標にYを引けば導ける。このように半径と中心角さえ分かれば座標は導ける。扇形を描く際、半径はあらかじめ自分で決めるので、後は好きな角度を指定すればいい。具体的にJavaScriptで座標を求めるプログラムを書くとしたら以下のようになる。

//cxは中心点のx座標;
//cyは中心点のy座標;
//rは半径;
//degreeは角度(度数法);
function getCoordinate(cx, cy, r, degree) {
	var x = cx + r * Math.sin(degree / 180 * Math.PI);
	var y = cy - r * Math.cos(degree / 180 * Math.PI);
	return {x: x, y: y};
}

Math.sinやMath.cosの角度θは度数法ではなく弧度法で指定しなければいけない。弧度法は180度をπと表す。例えば1度の場合は「1 / 180 * π」、30度の場合は「30 / 180 * π」、270度の場合は「270 / 180 * π」で度数法から弧度法に変換できる。上の関数は自動で弧度法に変換してくれるが、度数法から弧度法に変換する方法は押さえておきたい。

さて、座標を出す方法がわかったので、path要素のコマンドに座標の数値を指定することで扇形を作れるようになった。ここで、SVGのpath要素で扇形を描くJavaScriptの関数を書いてみる。

//cxは円の中心のx座標;
//cyは円の中心のy座標;
//rは半径;
//startDegreeは扇形の始まりの中心角(度数法);
//finishDegreeは扇形の終わりの中心角(度数法);
//backColorは扇形の色;
//strokeColorは線の色;
//strokeWidthは線の幅;
function createFanShape(cx, cy, r, startDegree, finishDegree, backColor, strokeColor, strokeWidth) {
	//円弧の始まりの座標;
	var startX = cx + r * Math.sin(startDegree / 180 * Math.PI);
	var startY = cy - r * Math.cos(startDegree / 180 * Math.PI);
	//円弧の終わり座標;
	var finishX = cx + r * Math.sin(finishDegree / 180 * Math.PI);
	var finishY = cy - r * Math.cos(finishDegree / 180 * Math.PI);
	//扇形の角度が180度を超えているか;
	var largeArcFlag = (finishDegree - startDegree <= 180) ? 0 : 1;

	//扇形の中心点へ移動するコマンド;
	var move = 'M' + cx + ' ' + cy + ' ';
	//扇形の中心点から円弧の始まりまで線を結ぶコマンド;
	var line = 'L' + startX + ' ' + startY + ' ';
	//円弧を描くコマンド;
	var arc = 'A' + r + ' ' + r + ' ' + 0 + ' ' + largeArcFlag + ' ' + 1 + ' ' + finishX + ' ' + finishY + ' ';
	//円弧の終おわりから中心点を結ぶコマンド;
	var close = 'Z';

	//path要素を作成;
	var NS = 'http://www.w3.org/2000/svg';
	var path = document.createElementNS(NS, 'path');
	path.setAttributeNS(null, 'd', move + line + arc + close);
	path.setAttributeNS(null, 'fill', backColor);
	path.setAttributeNS(null, 'stroke', strokeColor);
	path.setAttributeNS(null, 'stroke-width', strokeWidth);

	return path;
}

この関数はpath要素を返す。このpath要素をsvg要素に追加すると扇形が表示される。この関数は12時の方角を始まりとする時計回りの円を想定している。例えば、1時から5時の扇形を作りたい場合、関数の第四引数と第五引数に次のように指定する。

createFanShape(100, 100, 100, 30, 150, '#6d5450');

次に、実際にこの扇形を繋げて簡単な円グラフを作ってみる。

円グラフを作ってみる

円グラフは扇形を繋げるだけで描ける。まず最初に円の大きさを決める。円は一周360度なのでそれを100%ととして後はグラフの割合で扇形の角度を決めていけばいい。例えば下のようなネタで円グラフを作ってみる。

Q.神はいると思う?

  • インターネットで見た 82%
  • いない 13%
  • わからない 5%

See the Pen MYmJOR by shigure (@webkatu) on CodePen.

この円グラフは、3つの扇形をつなげている。この円グラフは単に、createFanShapeという扇形を作る関数に半径や角度などの情報を引数で与えて、それを繰り返しているだけだ。

円グラフは、テキストも添える必要がある。ここでは、createPieChartTextという関数でテキストを置いている(正確にはtext要素を作っている。)。この関数に扇形の情報を渡すと、ちょうど扇形の円弧上の中心にテキストを配置するtext要素を作成する。円弧と重なると文字が見づらいため、半径を変えてちょうどいい位置を指定する。ここでは、円の半径に0.7をかけた値を引数で渡している。つまり、円グラフの中にもう一つ円があり、その円周上にテキストが配置されているようなものだ。下を参考にしてほしい。

See the Pen qEjYOa by shigure (@webkatu) on CodePen.

テキストは中央に配置するためはみ出す可能性がある。テキストの配置に関してはもっと良い方法があるかもしれない。

このように、SVGで扇形の作り方が分かれば簡単な円グラフも描けるようになる。円グラフを描くためのライブラリは色々とあるが、SVGで直接描く方法を知っておくのは悪くないと思う。記事の最後に、createFanShapeとcreatePieChartTextの関数を載せておくので参考にしてほしい。

計画表を作るアプリ

唐突だが、今回の記事を取り入れたSVGで描く計画表アプリを作ったのでもし機会があれば使ってほしい。LifeLeaderというWebアプリだ。

LifeLeader

記事で使った関数

//cxは円の中心のx座標;
//cyは円の中心のy座標;
//rは半径;
//startDegreeは扇形の始まりの中心角(度数法);
//finishDegreeは扇形の終わりの中心角(度数法);
//backColorは扇形の色;
//strokeColorは線の色;
//strokeWidthは線の幅;
function createFanShape(cx, cy, r, startDegree, finishDegree, backColor, strokeColor, strokeWidth) {
	//円弧の始まりの座標;
	var startX = cx + r * Math.sin(startDegree / 180 * Math.PI);
	var startY = cy - r * Math.cos(startDegree / 180 * Math.PI);
	//円弧の終わり座標;
	var finishX = cx + r * Math.sin(finishDegree / 180 * Math.PI);
	var finishY = cy - r * Math.cos(finishDegree / 180 * Math.PI);
	//扇形の角度が180度を超えているか;
	var largeArcFlag = (finishDegree - startDegree <= 180) ? 0 : 1;

	//扇形の中心点へ移動するコマンド;
	var move = 'M' + cx + ' ' + cy + ' ';
	//扇形の中心点から円弧の始まりまで線を結ぶコマンド;
	var line = 'L' + startX + ' ' + startY + ' ';
	//円弧を描くコマンド;
	var arc = 'A' + r + ' ' + r + ' ' + 0 + ' ' + largeArcFlag + ' ' + 1 + ' ' + finishX + ' ' + finishY + ' ';
	//円弧の終おわりから中心点を結ぶコマンド;
	var close = 'Z';

	//path要素を作成;
	var NS = 'http://www.w3.org/2000/svg';
	var path = document.createElementNS(NS, 'path');
	path.setAttributeNS(null, 'd', move + line + arc + close);
	path.setAttributeNS(null, 'fill', backColor);
	path.setAttributeNS(null, 'stroke', strokeColor);
	path.setAttributeNS(null, 'stroke-width', strokeWidth);

	return path;
}
//cxは円の中心のx座標;
//cyは円の中心のy座標;
//rは半径, 扇形の半径 * xを指定する;
//円グラフの内側にテキストを配置するには 0 < x < 1;
//円グラフの外側にテキストを配置するには 1 < x;
//startDegreeは扇形の始まりの中心角(度数法);
//finishDegreeは扇形の終わりの中心角(度数法);
//backColorは扇形の色;
//fontColorは文字色;
//textは配置するテキスト内容;
function createPieChartText(cx, cy, r, startDegree, finishDegree, fontColor, text) {
	//textを表示する角度(扇形の真ん中);
	var degree = (startDegree + finishDegree) / 2;
	//textの座標;
	var textX = cx + r * Math.sin(degree / 180 * Math.PI);
	var textY = cy - r * Math.cos(degree / 180 * Math.PI);

	//text要素を作成;
	var NS = 'http://www.w3.org/2000/svg';
	var svgText = document.createElementNS(NS, 'text');
	svgText.textContent = text;
	svgText.setAttributeNS(null, 'x', textX);
	svgText.setAttributeNS(null, 'y', textY);
	svgText.setAttributeNS(null, 'fill', fontColor);
	svgText.setAttributeNS(null, 'dominant-baseline', 'middle');
	svgText.setAttributeNS(null, 'text-anchor', 'middle');

	return svgText;
}

【Linux】CentOS6.5にrbenvをsudoで呼び出せるよう環境構築

先日Sassを使うためにRubyをインストールしようと思い、我がCentOS6.5に環境構築をしようとしたところ色々とハマったので備忘録程度に記述。

色々と調べたところどうやらrbenvという管理ツールを使ってRubyをインストールする方法がいいらしいので、rbenvを使って環境構築する方法を書いていく。

基本的にこのサイト「rbenvをシステムワイドにインストールする」に書いてあることを忠実にやっていく。ただし、それでもハマる部分がいくつかあるので必要な部分を付け足しながら書いていく。

まず始めに

rbenvのGitHubにある環境構築の方法はHOMEディレクトリにrbenvをインストールするものでユーザーを変えると使えなくなる。面倒なのでどのユーザーでもsudoでrbenvを呼び出せるよう/usr/local/rbenvにインストールする。

sudoersを編集

sudoersはsudoコマンドについての色々な設定が書かれているファイル。sudoコマンドを実行した時にPATHが引き継がれるよう設定を変える。

次のコマンドを打ってsudoers編集画面に行く。

sudo visudo

編集画面にて次のコマンドを打つ(“/”はviの検索コマンド)

/secure_path

次のような行を見つける。

# Defaults	secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

このsecure_pathというのはsudo実行時に通すことのできるPATHのリストである。ここに書かれていない場所にあるPATHはsudo実行時に通らないので、rbenvをインストールする予定のPATHを追加していく。まず、先頭のシャープ「#」を外しコメントアウトする(すでに外されているかもしれない)。コロン「:」でPATHを追加していく。追加するPATHは「/usr/local/rbenv/bin」と「/usr/local/rbenv/shims」だ。これらを実行すると次のような感じになる。

Defaults	secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/rbenv/bin:/usr/local/rbenv/shims"

次にsecure_pathの上の方に「Defaults env_keep」と書かれた行がいくつかあるのでその下に次の行を追加する。

Defaults env_keep += "PATH RBENV_ROOT"

この「PATH」を追加することによってsudo実行時にPATHが引き継がれるようになる(すでに追加されているかもしれない)。「RBENV_ROOT」は後の設定で通すPATHである。

rbenvをインストール

次のコマンド打ってrbenvをGitHubから取得する。なおgitをインストールしていないのであれば、あらかじめ「sudo yum -y install git」でgitをインストールしておく。

sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv

次のコマンドを打ち、rbenvの初期設定をする。

sudo rbenv init -

PATHを設定する

ログイン時にPATHが自動で通るようにホームディレクトリにある「.bash_profile」にPATHを通すコマンドを書く。「.bash_profile」は直接編集せず、次のコマンドを打ち「.bash_profile」にPATHを通すコマンドが書かれるようにする。

bash -c 'cat <<\__EOT__ >> ~/.bash_profile
export RBENV_ROOT="/usr/local/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"
__EOT__'

exportというのはPATHを通すコマンドで、これが「.bash_profile」に書かかれることによって、ログイン時にrbenvへのPATHが自動で通るようになる。

次のコマンドでシェルを再起動する

exec ${SHELL} -l

これでrbenvを使えるようになる

ruby-buildをインストール

Rubyをインストールするためにruby-buildというプラグインを使うので、次のコマンドを打ってruby-buildをGitHubから取得する。

sudo git clone git://github.com/sstephenson/ruby-build.git /usr/local/rbenv/plugins/ruby-build

OpenSSLをインストール

rbenvでRubyインストールする際、OpenSSLがインストールされていないとRubyのインストールに失敗する。次のコマンドを打ちOpenSSLをインストールする。

sudo yum -y install openssl-devel

Rubyをインストール

ここまででRubyをインストールする準備が整ったのでRubyをインストールする。しかし、インストールするRubyのバージョンによっては失敗する。Rubyのバグのようである。修正されたruby 2.0.0-p353以降は失敗しないようなのでこれを次のコマンドでインストールする。

sudo rbenv install -v 2.0.0-p353

インストールされたら、次のコマンドでrubyのバージョンを設定する。

sudo rbenv global 2.0.0-p353

最後に次のコマンドを打つ

sudo rbenv rehash

これでgemコマンドもsudoを通して使えるようになる。「rbenv rehash」はgemを使って何かコマンドをインストールしたりする度に実行する必要があるようだ。例えば「sudo gem install sass」でsassをインストールした後など「sudo rbenv rehash」とコマンドを打つ必要がある。

インストールしたrbenvを消す方法

もし何か失敗してrbenvを再インストールしたりすることになった時のためにその方法も書いておく。rbenvを消すにはrbenvをインストールしたディレクトリを削除すればいい。今回は/usr/local/rbenvにインストールしたのでこれを次のコマンドで消す。ただし、gemでインストールしたコマンドも丸ごと消える。

sudo rm -rf /usr/local/rbenv

これでrbenvは消えるが、「.bash_profile」に書いたコマンドは消えないので、ログインする度にrbenvが呼ばれて、「command not found rbenv」みたいな表示が出る。これも消したい場合は、「vi ~/.bash_profile」で「PATHを設定する」の項で書いた設定を消せばいい。つまり、rbenvを再インストールする際は、再び「.bash_profile」にPATHを通すコマンドを書く必要はない。

【JavaScript】オブジェクトをDeepCopyするclone関数を書いた

JavaScriptのオブジェクトは参照渡しなので複製したい時は自分で同じオブジェクトを作らないといけない。そこでオブジェクトをディープコピーするclone関数を作ってみた。コードは以下。GitHubにもある。

See the Pen PqRWmw by shigure (@webkatu) on CodePen.

使い方と引数の説明

var cloneObject = clone(object, homogeneity, excludedPrototypes, excludedObjects);
  • 第一引数「object」

    第一引数はコピーしたいオブジェクトを指定する。ただ単にディープコピーしたいのであれば第一引数だけ使えばいい。

    			var a = {a: function() {}, b: [], c: {}};
    			var b = clone(a);
    
    			//参照渡しではなく全てコピーされている;
    			console.log(b === a); //false;
    			console.log(b.a === a.a); //false;
    			console.log(b.b === a.b); //false;
    			console.log(b.c === a.c); //false;
    			
  • 第二引数「homogeneity」

    第二引数はオブジェクトの拡張可属性(Object.preventExtensible, isSealed, isFrozen)や、プロパティのディスクリプタ(enumerableなど)を元のオブジェクトと同一にするかどうかをオブジェクトで指定する。なぜ、この引数を用意したかというと、JavaScriptでは一度拡張不可にしたオブジェクトを再び拡張可にする方法はないからだ。しかし、オブジェクトをクローンすれば中身が同じオブジェクトを拡張可で作り直せるためこの引数を用意した。

    {extensible: false}と引数に指定すると拡張可属性は同一にされず、全て拡張可のcloneObjectが返る。

    {descriptor: false}と指定するとプロパティのディスクリプタはget, set, valueを除いて(enumerable, configurable, writable)全てtrueになる。

    何も指定しないと、全て元のオブジェクトと同一のオブジェクトが返る

    			var a = {};
    			//ディスクリプタ全てfalse;
    			Object.defineProperty(a, 'prop', {
    				get: function() {},
    				enumerable: false,
    				configurable: false,
    			});
    			//オブジェクトを凍結;
    			Object.freeze(a);
    
    			//拡張可属性とディスクリプタは元のオブジェクトと同一にしない;
    			var b = clone(a, {extensible: false, descriptor: false});
    			var descriptor = Object.getOwnPropertyDescriptor(b, 'prop');
    
    			console.log(descriptor.enumerable); //true → デフォルトのディスクリプタ;
    			console.log(descriptor.configurable); //true → デフォルトのディスクリプタ;;
    			console.log(Object.isFrozen(b)); //false → オブジェクトは凍結されていない;
    			
  • 第三引数「excludedPrototypes」

    第三引数はコピーせずにそのまま参照で渡したいオブジェクトの型を配列で指定する。ここでいうオブジェクトの型とは親のprototypeのことで、Object.getPrototypeOf(object)で得られるprototypeのことだ。例えば、関数オブジェクトをコピーしたくない場合は、[Function.prototype]と指定すればいい。後述するが、自作クラスのインスタンスもクラスのprototypeを指定すれば参照渡しされるようになる。

    			var a = {a: function() {}, b: [], c: {}};
    			//Function型とArray型はコピーしない;
    			var b = clone(a, null, [Function.prototype, Array.prototype]);
    
    			console.log(b.a === a.a); //true → 参照で渡された;
    			console.log(b.b === a.b); //true → 参照で渡された;
    			console.log(b.c === a.c); //false → 第三引数で指定していないObject型はコピーされた;
    			

    ちなみにprototypeチェーンは辿らない。あくまでも直接の親のprototypeしか見ない。

  • 第四引数「excludedObjects」

    第四引数はコピーしたくないオブジェクトを直接配列で指定する。

    			var a = {a: function() {}, b: [], c: {}};
    			var b = clone(a, null, null, [a.a, a.b]);
    
    			console.log(b.a === a.a); //true → 第四引数にあるオブジェクトなので参照で渡された;
    			console.log(b.b === a.b); //true → 第四引数にあるオブジェクトなので参照で渡された;
    			console.log(b.c === a.c); //false → 通常どおりコピーされた;
    			

対応している型とオブジェクト

clone関数が対応している型を以下に書いていく。オブジェクトの型はObject.prototype.toString().call(object)で返ってきた文字列で判別している。

  • Object

    ユーザーが「new Object()」や「{}」リテラルで作成したObjectや自作クラスからnewでインスタンス化したオブジェクト、Object.create()で継承したオブジェクト全てに対応している。

    			var a = {a: {}, b: [{}]};
    			var b = clone(a);
    
    			console.log(a.a === b.a); //false;
    			console.log(a.b === b.b); //false;
    			console.log(a.b[0] === b.b[0]); //false;
    			
  • Array

    Arrayオブジェクトも同様。ユーザーが作ったArrayオブジェクトはコピーされる。

    			var a = [[{}], {a: []}];
    			a.a = {};
    			var b = clone(a);
    
    			console.log(a[0] === b[0]); //false;
    			console.log(a[0][0] === b[0][0]); //false;
    			console.log(a[1] === b[1]); //false;
    			console.log(a[1].a === b[1].a); //false;
    			console.log(a.a === b.a); //false;
    			
  • Function

    ユーザーが作成したfunctionオブジェクトはコピーされる。

    			var a = function() {};
    			a.a = {a: function f() {}};
    			var b = clone(a);
    
    			console.log(a === b); //false;
    			console.log(a.a === b.a); //false;
    			console.log(a.a.a === b.a.a); //false;
    			

    ビルトイン関数やビルトインメソッドはコピーされず参照で渡される。

    後述するが、クロージャを使ってる関数オブジェクトは、変数に外からアクセスできないため同じ動作にはならない。

  • Error, Date, RegExp, Boolean, String, Number

    いずれもコピーされる

  • Node

    NodeはcloneNodeを使ってコピーされる。しかし、通常のcloneNode(true)ではNodeを入れ替えた時にscript要素が実行されないのでscript要素はcloneScriptを使ってコピーしている。つまり、cloneされたNodeを代入するとscript要素内のスクリプトも再実行される。

    Nodeのクローン機能は削除した(2017/06/07)

対応していないオブジェクト

基本的に対応しているオブジェクト以外はコピーされず参照で渡される。Math、JSON、arguments、Event、screen、navigator、window、Node、ブラウザオブジェクトなどなどは参照で渡される。

また、ビルトイン関数、標準で用意されているメソッドも参照で渡される。setTimeout、eval、Array.prototype.sliceなどは参照で渡される。

その他

  • 循環参照、内部参照

    循環参照や内部参照はcloneObjectでの循環参照や内部参照に切り替えられる。循環参照のオブジェクトが渡されてもスタックオーバーフローはしない。

    			var a = {};
    			a.a = a;
    			var b = clone(a); //スタックオーバーフローしない;
    
    			console.log(a === b) //false;
    			console.log(a.a === b.a) //false; →循環参照先が適切に切り替えられる;
    			console.log(b === b.a) //true → b内で循環参照している;
    			
  • 自作クラス(コンストラクタ)などからprototype継承しているインスタンスオブジェクト

    これもコピーされる。コピーされるcloneObjectはコピー元のインスタンスオブジェクト同様、同じ親からprototype継承をする。正確に言えば、Object.getPrototypeOf()でコピー元のオブジェクトのprototypeを取得し、それを継承する。また、Object.getOwnPropertyNamesで列挙属性関係なく自分で持つプロパティはすべてコピーされる。

    			//newでインスタンスを作る例;
    			var Class = function() {};
    			var instance = new Class();
    			instance.prop = 1;
    			var cloneIns = clone(instance);
    
    			console.log(cloneIns === instance); //false → インスタンスは別;
    			console.log(cloneIns.__proto__ === instance.__proto__); //true → 両方共Class.prototypeを参照している;
    			console.log(cloneIns.prop); //1 → インスタンスプロパティは通常どおりコピーされる;
    
    			//Object.createでインスタンスを作る例;
    			var Person = {};
    			var child = Object.create(Person);
    			child.age = 12;
    			var brother = clone(child);
    
    			console.log(brother === child); //false;
    			console.log(brother.__proto__ === child.__proto__); //true → 両方共Personを参照している;
    			console.log(brother.age); //12 → インスタンスプロパティは通常どおりコピーされる;
    			

    以上のようにほぼ同じインスタンスが作成される。ほぼと書いたのはコンストラクタの隠蔽されている変数にはアクセスできないからで全く同じインスタンスが作成されるわけではないのだ。もしインスタンスをコピーせずに参照で渡したいのであれば、使い方と引数のセクションにも書いたように、cloneの第三引数に、継承しているprototypeを配列で指定する。

    			var Class = function() {};
    			var instance = new Class();
    
    			var cloneIns = clone(instance, null, [Class.prototype]);
    			console.log(cloneIns === instance); //true → コピーされず参照で渡された;
    			

    この第三引数は、主に自作クラスのインスタンスを個別で扱うために用意したが、Functionオブジェクトなど大抵の場合コピーする必要性があまりないオブジェクトも指定できるようになっている。

  • アクセサプロパティ(getterとsetter)

    アクセサプロパティはgetterもsetterも関数オブジェクトとしてコピーされる。

  • 関数オブジェクトのコピーについて

    clone関数は関数オブジェクトもコピーするが、クロージャを使ったプライベートな変数は外から参照できないため、クロージャを利用している関数は不完全にコピーされる。具体的に言うと、関数内のクロージャ変数の参照先がグローバルになる。そのため、特に関数オブジェクトをコピーする必要がないなら、clone関数の第三引数に[Function.prototype]と指定してFunctionオブジェクトは参照渡しすることを推奨する。

終わりに

基本的にJSON.parse(JSON.stringify(object))が可能であればそれでコピーした方がよさそうだ。このclone関数の特徴は自作クラスのprototype継承をしているオブジェクトを継承含め真似る点、Functionオブジェクトまでコピーできる点と循環参照対策されている点。もし、バグがあれば教えてほしい。

GitHub

【JavaScript】注釈・脚注を補完する「AutocompleteAnnotations」

注釈・脚注を補完するスクリプト「autocompleteAnnotations」を書いた。

ブログを書いていると【1】のように注釈や脚注を付けたい時がある。しかし、一々ハッシュリンク貼ったりa要素のtitle属性に内容を書いたりするのは面倒くさいので自動化する。以下サンプル

See the Pen AutocompleteAnnotations by shigure (@webkatu) on CodePen.

使い方

「autocompleteAnnotations.js」をdefer属性をつけて呼び出し、注釈部分の要素に「class=”annotations”」という属性をつけて、説明部分の要素に「class=”footnotes”」という属性をつければOK。

<head>
	<!-- defer属性をつけてscriptを呼び出す -->
	<script defer src="autocompleteAnnotations.js"></script>
</head>
<body>
	<!-- 注釈部分を"annotations"というclass名の要素で囲む -->
	<p>hogehoge<span class="annotations">【1】</span></p>
	<p>fugafuga</p>
	<!-- 説明部分(脚注)を"footnotes"というクラス名の要素で囲む -->
	<p class="footnotes">piyopiyo</p>
</body>
<head>
	<script defer src="autocompleteAnnotations.js"></script>
</head>
<body>
	<p>hogehoge<span class="annotations"><a id="annotation0" href="#footnote0" title="piyopiyo">【1】</a></span></p>
	<p>fugafuga</p>
	<p class="footnotes"><a id="footnote0" href="#annotation0">【1】</a>: piyopiyo</p>
</body>

GitHub