2013年03月25日 11:45 [Edit]

javascript - introducing wrap.js

What?

なるべく多くの種類のオブジェクトを、なるべく直感的に、限りなく透明に近く wrap するためのシステムです。

こんな感じ。

var _ = Object.Wrap;    /* for convenience */
try {
  log(
    _(42)
        .learn('square', function() { return this*this })
        .square() * 1   /* 1764 */
  );
  log(
    (42).square()       /* TypeError: Object 42 has no method 'square' */
  );
}catch(e){
    log(e.message);
};
/* class method without changing Number */
_.Number.prototype
  .learn('times', function(f) { for (var i = 0; i < this; i++) f(i) });
try {
  _(4).times(function(n){ log(n) });
   (4).times(function(n){ console.log(n) });  /* TypeError: Object 4 has no method 'times' */
}catch(e){
    log(e.message);
};

見ての通り、全てはObject.Wrapの中にあり、それ以外のグローバルプロパティには指一本触れません。ここまでは Underscore.js なども同様ですが、見ての通りプリミティブ型もwrapできます。

es2piにイカんのイを表明しそうなメンテナブルJavaScriptの支持者も、これならヨいのヨを表明してくださるのではないでしょうか。

*/ 11.3.3 Facadeパターン
ファサードは、インターフェース越しに完全に制御できる点から、メンテナンス可能な JavaScript にうまく適しています。舞台裏にあるオブジェクトへのアクセスを効率よくフィルタリングすることで、そのオブジェクトのプロパティやメソッドに対するアクセスを許可および禁止できます。

ところで同書のサンプルコードが buggy なんだが…

P. 119
DOMWrapper.prototype.addClass = function(className) {
    element.className += " " + className;   /* where is this? */
};

Object.Wrap()

閑話休題。

実は関数としてのObject(o)は、oがプリミティブなら wrap されたオブジェクトを、そうでなければoをそのまま返す wrapper function です。Object.Wrap(o)もそれを規範としています。

Object(o) === o;        /* true if o is already an object */
Object.Wrap(o) === o;   /* true if o is already wrapped */

auto(un)?wrap

現時点でNumber, String, Array, Object は autowrap されます。

var assert = log;       /* for convenience; */
var n = 0,  wn = Object.Wrap(n),
    s = '', ws = Object.Wrap(s),
    o = {}, wo = Object.Wrap(o),
    a = [], wa = Object.Wrap(a);
assert(wn !== n && wn.value === n);
assert(ws !== s && ws.value === s);
assert(wo !== n && wo.value === o);
assert(wa !== n && wa.value === a);

コレクション型はとにかく、プリミティブ型もwrapできるところが特長ですが、しかし Null, Undefined, Boolean はそのままでは wrap しません。理由は、 Boolean 演算子 が coerce しないから。

var _ = Object.Wrap;
log( Object(21)  + Object(21)   );  /* 42 */
log( Object('4') + Object('2')  );  /* '42' */
log( !!Object(false)            );  /* surprisingly true */
log( _(21)  + _(21)             );  /* 42 */
log( _('4') + _('2')            );  /* '42' */
log( !!_(false)                 );  /* natually false */
log( !!_(null)                  );  /* false */
log( !!_(undefined)             );  /* false */
log( !!_(false, 1)              );  /* true because it is wrapped */
log( !!_(false, 1).value        );  /* false because it is unwrapped explicitly */

wrap by request

ただ前述の例を見てのとおり、第二引数にtruthyな値を入れるとwrapしてくれるようになります。

var assert = log;       /* for convenience; */
var z = null,                   wz = Object.Wrap(z, 1),
    u = undefined,              wu = Object.Wrap(u, 1),
    b = false,                  wb = Object.Wrap(b, 1),
    f = function(a){return a},  wf = Object.Wrap(f, 1),
    r = /^.*$/,                 wr = Object.Wrap(r, 1),
    d = new Date(0),            wd = Object.Wrap(d, 1);
assert(Object.Wrap(z) === z && wz !== z && wz.value === z);
assert(Object.Wrap(u) === u && wu !== u && wu.value === u);
assert(Object.Wrap(b) === b && wb !== b && wb.value === b);
assert(Object.Wrap(f) === f && wf !== f && wf.value === f);
assert(Object.Wrap(r) === r && wr !== r && wr.value === r);
assert(Object.Wrap(d) === d && wd !== d && wd.value === d);

.learn()

そもそもwrapしたい理由は、ビルトインプロトタイプ、特にObject.prototypeを拡張したくない、というかするのが怖いからなのですが、だからといってwrapper用のprototypeを生で書くのは大変です。まず元の値をunwrapし、引数もunwrapし、実行してから結果をwrapしなおすというのは骨が折れます。

そのために.learnがあります。ここで普通に普通のオブジェクトのプロトタイプを拡張する時のように関数定義を書けば、あとはそれをwrapper用に変換してインストールしてくれます。

var _ = Object.Wrap;
var wn = _(42);
wn.learn('square', function() { return this*this }, 'Number');
log( wn.square() );
log( '' + wn.square );

もちろんメソッド単体ではなく、プロトタイプ全体も拡張できます。というかプロトタイプ自身が.learn()できるので、こうできます。

var _ = Object.Wrap;
_.Number.prototype.learn({
    times:function(f) { for (var i = 0; i < this; i++) f(i) },
    toThe:function(n) { return Math.pow(this,n) }
});
_(16).times(function(n) {
    log( _(2).toThe(_(n)) );
});

実際プロトタイプにあらかじめ用意してあるメソッドは、ほとんどこの方法で実装しています。

.value

Object.Wrapでwrapしたオブジェクトは、見ての通りプリミティブであれば必要に応じて(つまり演算子適用のタイミングで)unwrapしてくれるすぐれものですが、その手はコレクション型には利きません。

とはいえ、.valueで簡単にunwrapできます。

var _ = Object.Wrap;
log( _([0,1,2,3]).slice(1)[0]      );   /* doesn't work */
log( _([0,1,2,3]).slice(1).value[0]);   /* [1,2,3][0] => 1 */

実はgetterでvalueOf()しているだけですが、getterのおかげで四文字節約できます。

[]の代わりに

コレクション型には.get, .set, .has, .delete も用意されています。

var _ = Object.Wrap;
log( _([0,1,2,3]).has(1)                );  /* true */
log( _([0,1,2,3]).has(4)                );  /* false */
log( _({zero:0,one:1}).has('zero')      );  /* true */
log( _({zero:0,one:1}).has('four')      );  /* false */
log( _([0,1,2,3]).get(1).value          );  /* 1 */
log( _([0,1,2,3]).get(4).value          );  /* undefined */
log( _({zero:0,one:1}).get('zero').value);  /* 0 */
log( _({zero:0,one:1}).get('four').value);  /* undefined */
var wa = _([0,1,2,3]);
log( wa.set(5, 5).value                 );  /* 5 is the return value */
log( wa.value                           );  /* [0,1,2,3,undefined,5] */
var wo = _({zero:0,one:1});
log( wo.set('five', 5).value            );  /* 5 */
log( wo.value                           );  /* {zero:0,one:1,five:5} */
wa = _([0,1,2,3]);
log( wa.delete(0)                       );  /* true */
log( wa.delete(4)                       );  /* false */
log( wa.value                           );  /* [undefined,1,2,3] */
wo = _({zero:0,one:1});
log( wo.delete('zero')                  );  /* true */
log( wo.delete('five')                  );  /* false */
log( wo.value                           );  /* {one:1} */

.methods

Object.Wrapの存在意義は、こういうことをしてドヤ顔をするためです:-)

var _ = Object.Wrap;
log(
_({zero:0,one:1,two:2,three:3})
    .values()                                   /* _([0,1,2,3]) */
    .map(function(x){ return x * x })           /* _([0,1,4,9]) */
    .filter(function(x){ return x % 2 === 0})   /* _([0,4]) */
    .pop()                                      /* _(4) */
    * _(5) * _(2) + 2                           /* 42 */
);

しかしこうなると、それぞれのオブジェクトが何が出来るかわかりにくくなりもします。

オブジェクト自身に答えていただきましょう。

var _ = Object.Wrap;
log( _(null,1).methods      );
log( _(undefined,1).methods );
log( _(false,1).methods     );
log( _(0).methods           );
log( _("").methods          );
log( _([]).methods          );
log( _({}).methods          );

Rubyistのみなさん、おかえりなさいwelcome home!。

さいごに

これを作るためにES5の機能を使いまくったのですが、どう使いまくったかはソースを参照いただくということで。

Enjoy -- without messing the environment!

Dan the JavaScripters with Too Many Objects to Wrap


この記事へのトラックバックURL