15
15

More than 5 years have passed since last update.

.(ドット)つなぎのキー文字列でオブジェクトの値に"かっこよく"アクセスする

Last updated at Posted at 2013-07-21

この記事(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.aobj['a']は同義ですから。

作ってみる

それでは、JavaScript .(ドット)つなぎのキー文字列でオブジェクトの値にアクセスするで書かれているようなものを実装してみることにします。

なんとなくNode.jsにしました。

accessable.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というモジュールに依存しています。あしからず。

で、これの使い方の解説は上のコードの中にテストも仕込んだのであまり必要無さそうですが、こんなふうになります。

accessable_usage.js
#!/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ですが、どうか可愛がってやってください(何様だ!

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15