【JavaScript】privateなプロパティやメソッドを定義する

JavaScriptでprivateなプロパティやメソッド(以下メンバ)を定義する方法を書いていく。

ES6未満では、privateなメンバを定義するために色々な工夫が重ねられていた。それでも全くprivateなプロパティを作るのは難しかった。しかし、ES6ではWeakMapというオブジェクトが使えるようになり、完全にprivateなメンバを定義できるようになった。

ここではまず、ES6未満でprivateなメンバを定義するための工夫の数々を紹介し、その後、WeakMapを使ったprivateなメンバを定義する方法を書く。そして、最後にES6でのClass定義の自分の書き方パターンを紹介したい。

ES6未満でのprivateなメンバの定義方法

まず、1つの工夫としてprivateなメンバには先頭に_(アンダーバー)をつけるという方法があるが、これは外部からアクセスできるためprivateでもなんでもない。

もう1つの方法としてメンバを全てconstructor内に記述するという方法もある。

function Foo() {
	this.prop = 1;
	this.method = function() {};
	var privateProp = 2;
	privateMethod = function() {};
}

これは確かにprivateなメンバを作れるが、PrototypeベースというJavaScriptの利点をなくしてしまう他、インスタンス化する度にpublicなメソッドも定義するためメモリも余計に食う。

現実的な方法としては、以下のような書き方がある。

var Foo = (function() {
	var privates = {};
	var privateId = 0;

	function Foo() {
		Object.defineProperty(this, '_id', { value: privateId++ });
		privates[this._id] = {};

		privates[this._id].prop = 1;
	}

	Foo.prototype.method = function() { return privates[this._id].prop };

	return Foo;
})();

この方法はthis._idというプロパティを作ってしまうことさえ許容できれば、かなりgoodな方法である。しかし、WeakMapを使えばこのようなプロパティを作る必要もなくなる。

ES6のWeakMapを使ってprivateなメンバを定義する

まず、WeakMapを知らない方へWeakMapとは何なのか簡単に説明するが、普通のObjectはkeyに文字列を使うのに対して、WeakMapはkeyにオブジェクトを使う。つまり、プリミティブ型でなければObjectでもArrayでもFunctionでも何でもkeyにできる

var a = {};
var b = [];
var c = () => {};

var weakmap = new WeakMap();
weakmap.set(a, 1);
weakmap.set(b, 2);
weakmap.set(c, 3);

console.log(weakmap.get(b)); // -> 2;

このようにオブジェクトをkeyとして使えるため、Classをインスタンス化する際、そのインスタンス(this)をWeakMapにsetすればWeakMap.get(this)でメンバにアクセスできるのだ。百聞は一見にしかずなので例示する。

var Foo = (function() {
	var privates = new WeakMap();

	function Foo() {
		privates.set(this, {});

		privates.get(this).prop = 1;
	}

	Foo.prototype.method = function() { return privates.get(this).prop };

	return Foo;
})();

ES6のClass定義パターン

上のように、WeakMapの中のプロパティにアクセスするためにはgetやsetというメソッドを使う必要があり、それは面倒なので以下のようなnamespaceという関数を作る。

'use strict';
export default function namespace() {
	let map = new WeakMap();

	return function(object) {
		if(! map.has(object)) {
			map.set(object, {});
		}
		return map.get(object);
	};
};

これをClassファイルにimportしてprivateなメンバを定義する方法を以下に例示する。

'use strict';
import ns from './namespace';

const Foo = (() => {
	let privates = ns();

	return class Foo {
		constructor() {
			let self = privates(this);
			self.privateProp = 1;
			this.prop = 2;

			self.privateMethod1 = privateMethod1.bind(this);
			self.privateMethod2 = privateMethod2.bind(this);
		}

		method() {
			let self = privates(this);
			return self.privateMethod1();
		}
	}

	function privateMethod1() {
		let self = privates(this);
		return self.privateProp + this.prop;
	}
	function privateMethod2() {
		return this.method();
	}
})();

ここでは、privates(this)で返ってくるオブジェクトにprivateなメンバをセットする。取得するときもprivates(this)から取得できる。privates(this)をいちいち呼び出すより、selfなどの変数に入れておくほうがよい。

また、privateなメソッドはclassの外でfunction文で宣言して(ホイスティング)、constructor内でprivates(this)にthisをbindして代入したほうがネストも浅くて済む。

もし、モジュール化でexportなどを使う場合、privateなメンバのアクセスをファイル全体まで許すこともできる。それは、即時関数でclassを囲うか囲わないかによる。

また、browserifyなどを使って複数のファイルを1つのファイルにあらかじめコンパイルしているのであれば、それらのファイル内のみメンバのアクセスを許可するといったような名前空間を作ることも可能である。

import ns from ./namespace;
export default ns();
import internal from './internal'
export default class Foo {
	conctructor() {
		internal(this).prop = 1;
	}
}
import internal from './internal'
import Foo from ./Foo
let foo = new Foo();
console.log(internal(foo).prop); // -> 1;

1つの方法として覚えておくと使える時がくるかもしれない。

終わりに

今回は、JavaScriptでprivateなメンバを定義する方法を書いてきた。特に、WeakMapを使った方法はかなり有用性があり、途中で示したnamespace.jsと合わせて使うとJavaScriptでもprivateなメンバを簡単に書けるようになる。今後、デフォでJavaScriptでもprivateなメンバを定義できるようになるかもしれないが、それまでは、このWeakMapを使った方法で定義することを推奨したい。

document.getElementsByText()

ユーザースクリプト用に書いた

"use strict";

(function () {
	function walkDOM(node, callback) {
		if (node === null) return;
		callback(node);

		walkDOM(node.firstChild, callback);
		walkDOM(node.nextSibling, callback);
	}

	function getElementsByText(text) {
		var set = new Set();
		walkDOM(this, function (node) {
			if (node.nodeType !== 3) return;
			if (node.nodeValue.indexOf(text) === -1) return;
			set.add(node.parentNode);
		});
		var ret = [];
		set.forEach(function (value) {
			ret.push(value);
		});
		return ret.length === 0 ? null : ret;
	}

	document.getElementsByText = getElementsByText;
	Element.prototype.getElementsByText = getElementsByText;
})();

使い方はこのスクリプトを読み込んで、document.getElementsByText(text)を実行するかElement.getElementsByText(text)を実行する。

引数のtextが含まれている親のエレメント達が配列で返ってくる。何もなければnullを返す。

GitHubに例を載せている。

再帰を使っているためスタックオーバーフローする可能性がある。

document.evaluateでXPathを使ったほうが早い。

あくまでもユーザースクリプト用に書いてみた。

【JavaScript】IEのためにa要素のdownload属性を代替する

a要素のdownload属性を使えばchromeなどのモダンブラウザではコンテンツをダウンロードできるわけだが、IEはそうはいかない。例えば、下の画像をダウンロードする時、IE以外のモダンブラウザではこのように書けばダウンロードされる。

var a = document.createElement('a');
a.href = '/img/1.jpg';
a.download = 'neko';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

IEではダウンロードが実行されずにそのままページ遷移してしまうのがわかる。そこで今日は、代替方法を書く。

その方法とは、xhrでrequestを送り、blobで返してもらいそれをダウンロードするという方法だ。下がそのコードである。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/img/1.png');
xhr.responseType = 'blob';

xhr.onloadend = function() {
	if(xhr.status !== 200) return;
	window.navigator.msSaveBlob(xhr.response, 'neko');
}
xhr.send();

IEにはnavigatorオブジェクトにmsSaveBlob(blob, fileName)というメソッドが用意されているようで、blobのダウンロードにはこれを使う。

もうすでにお気づきの方もいるかもしれないが、これは完全な代替方法にはなり得ない。なぜならxhrはクロスドメイン制限があるからだ。つまり、同一ドメインか、CORS(Cross-Origin Resource Sharing)に対応していないとダウンロードきない。そして、blobのやりとりはIE9以下は対応していない。しかし、使える機会はあると思うので、もしよかったら使ってほしい。

他にいい方法を知っているという方はぜひ教えてください><