この記事(JavaScript .(ドット)つなぎのキー文字列でオブジェクトの値にアクセスする)の最後に、
ただ、どうすれば
obj['world.japan.greeting']
でウィッス!出来るのかが分かりませんでした。
プロパティへのアクセスについてカスタマイズする方法をご存知の方がいたら教えてもらえると嬉しいです(・ω<)
とあったので書いてみます。
まぁ、Proxies APIを使うだけなんですが。
##Proxies API
Proxies APIというのは、
- オブジェクトにアクセスしたり (
obj.a
) - in演算子を呼び出したり (
'a' in obj
) - deleteを呼び出したり (
delete obj.a
)
したとき、予め設定しておいた関数を実行して挙動を乗っ取れるというECMScript 6で予定されている機能で、僕が好きなものです。
まだ策定中の仕様の中の機能の一つなのですが、JavaScriptの実験機能を有効にしたChromeやFirefoxで実装されています。Node.jsでも--harmony
オプションを付けて起動すれば実行出来ます。
どんなものなのかはMDNのここらへんが一番詳しい気がします。
今回の obj['world.japan.greeting']
はオブジェクトにアクセスするのにあたります。
だって、JavaScriptではobj.a
とobj['a']
は同義ですから。
##作ってみる
それでは、JavaScript .(ドット)つなぎのキー文字列でオブジェクトの値にアクセスするで書かれているようなものを実装してみることにします。
なんとなくNode.jsにしました。
var
//for dynamic proxies API
Proxy = require('harmony-reflect').Proxy;
var
Accessable = (function () {
var
debug = false;
/**
* @privare
*/
function defaultValue(val, def) {
return typeof val === 'undefined' ? def : val;
}
/**
* 実際にゲッターとして呼び出される関数
* @private
* @param {Object} obj アクセスされるオブジェクト
* @param {String} key アクセスするプロパティを表す文字列
* @param {String} [dsv='.'] プロパティを区切る文字列
* @param {Any} [def=undefined] プロパティが存在しなかった場合の返り値
* @return {Any} プロパティが存在した場合はその値を、無ければ<code>def</code>の値
*/
function getValueByDsvKey(obj, key, dsv, def) {
//initialize arguments
obj = Object(obj);
key = ''+key;
dsv = defaultValue(dsv, '.');
def = defaultValue(def, undefined);
var
//見つからなかった場合にとりあえず使う値
//何もプロパティを持たないオブジェクトなので安全
noobj = Object.create(null),
//access property
val = key.split(dsv).reduce(function (obj, k) {
if(debug) console.log(obj);
return defaultValue(obj[k], noobj);
}, obj);
return noobj === val ? def : val;
}
/**
* 実際にセッターとして呼び出される関数
* @private
* @param {Object} obj アクセスされるオブジェクト
* @param {String} key アクセスするプロパティを表す文字列
* @param {Any} val セットする値
* @param {String} [dsv='.'] プロパティを区切る文字列
*/
function setValueByDsvKey(obj, key, val, dsv) {
dsv = defaultValue(dsv, '.');
var
keys = key.split(dsv), k = keys.pop();
getValueByDsvKey(obj, keys.join(dsv), dsv, {})[k] = val;
}
/**
* 実際に存在確認に呼び出される関数
* @private
* @param {Object} obj アクセスされるオブジェクト
* @param {String} key アクセスするプロパティを表す文字列
* @param {String} dsv プロパティを区切る文字列
* @return {Boolean} プロパティが存在するかどうか?
*/
function hasValueByDsvKey(obj, key, dsv) {
dsv = defaultValue(dsv, '.');
var
noobj = Object.create(null),
keys = key.split(dsv), k = keys.pop(),
hasObj = getValueByDsvKey(obj, keys.join(dsv), dsv, noobj);
return hasObj == noobj ? false : k in hasObj;
}
/**
* 実際に削除に呼び出される関数
* @private
* @param {Object} obj アクセスされるオブジェクト
* @param {String} key アクセスするプロパティを表す文字列
* @param {String} [dsv='.'] プロパティを区切る文字列
* @return {Boolean} プロパティを削除できたか?(正確には違う)
*/
function deleteValueByDsvKey(obj, key, dsv) {
dsv = defaultValue(dsv, '.');
var
keys = key.split(dsv), k = keys.pop(),
delObj = getValueByDsvKey(obj, keys.join(dsv), dsv);
return delObj == null ? false : delete delObj[k];
}
var
/**
* Proxyに渡すハンドラ
*/
handler = {
get: function getHandler(config, key) {
return getValueByDsvKey(config.obj, key, config.dsv, config.def);
},
set: function setHandler(config, key, val) {
setValueByDsvKey(config.obj, key, val, config.dsv);
return true;
},
has: function hasHandler(config, key) {
return hasValueByDsvKey(config.obj, key, config.dsv);
},
deleteProperty: function deleteHandler(config, key) {
return deleteValueByDsvKey(config.obj, key, config.dsv);
},
};
/**
* オブジェクトをobj['aaa.bbb.ccc']形式でプロパティにアクセスできるようにする
* @param {Object} obj ラップするオブジェクト
* @param {String} dsv プロパティを区切る文字列
* @param {Any} def プロパティが存在しなかった場合の返り値
*/
function Accessable(obj, dsv, def) {
return new Proxy({
obj: obj,
dsv: dsv, def: def,
}, handler);
}
//test
if (debug) (function () {
var
assert = require('assert'),
obj = {
a: {
b: {
c: 1,
},
},
},
acc = new Accessable(obj, '->', true);
assert.equal(acc['a->b->c'], obj.a.b.c, 'get');
assert.equal(acc['a->b->c->d'], true, 'get def');
acc['a->b->c'] = 2;
acc['a->b->d'] = 3;
assert.equal(acc['a->b->c'], 2, 'set1');
assert.equal(acc['a->b->d'], 3, 'set2');
assert.equal('a->b->c' in acc, true, 'has');
delete acc['a->b->c'];
console.log(obj);
assert.equal('a->b->c' in acc, false, 'delete');
})();
return Accessable;
})();
module.exports = Accessable;
予想以上に長くなってしまったのはdelete
やらin
やらにも対応していたからです。
あとharmony-reflectというモジュールに依存しています。あしからず。
で、これの使い方の解説は上のコードの中にテストも仕込んだのであまり必要無さそうですが、こんなふうになります。
#!/usr/bin/env node --harmony
var
Accessable = require('./accessable.js');
var
obj = {
world: {
japan: {
greeting: 'ウィッス!',
prefecturesCount: 47,
},
america: {
greeting: 'What\'s up?',
},
},
noworld: 'ウェーイ!!',
},
acc = new Accessable(obj);
accessKeys = [
'world.japan.greeting',
'world.america.greeting',
'world.japan.prefecturesCount',
'noworld',
];
accessKeys.forEach(function (key) {
console.log(acc[key]);
});
obj['world.japan.greeting']
でウィッス!出来ました(拍手)
うまい例が思いつかなかったのでJavaScript .(ドット)つなぎのキー文字列でオブジェクトの値にアクセスするの例をこれ向けに書きなおしてみました。
現状では間違いなく動くのはサーバーサイドぐらいなので忘れ去られ気味なProxies AOIですが、どうか可愛がってやってください(何様だ!