Webtech Walker

JavaScriptでRuby 2.0のRefinements的なのを実装してみる

Ruby2.0でRefinementsという、クラスの拡張を特定にスコープに限定する仕組みが導入された(一応使えるけどまだ実験的な機能)。

こんな感じで使う

# foo.rb
module Foo
  refine String do
    def greeting
      "hello #{self}!"
    end
  end
end
require './foo'
using Foo

puts 'hokaccha'.greeting #=> hello hokaccha!

refineで拡張したString#greetingメソッドはusingしたファイル内でしか使えない。

このような仕組みがあると、どこでどのようにクラスが拡張されたか、わけがわからなくなるという問題を解決できる。

試しにJavaScriptでも実装してみた。

refinements.js

こんな感じで使う。

// TestRefineという名前でクラスの拡張を定義
Refinements.register('TestRefine', function(refine) {
  // String.prototypeを拡張する
  refine(String.prototype, {
    greeting: function() {
      return 'Hello ' + this + '!';
    }
  });
});

// registerした定義をusingで使う
Refinements.using('TestRefine', function() {
  // この中でのみ String#greeting が利用可能
  'hokkacha'.greeting(); //=> hello hokaccha!
});

'hokkacha'.greeting(); //=> エラー

Refinements.usingのコールバックの中でだけrefineで定義したクラスの拡張が利用できる。

registerの中ではいくつでもrefineできる。

Refinements.register('TestRefine', function(refine) {
  // Array.prototypeを拡張する
  refine(Array.prototype, {
    // ランダムで一個だけ要素を取得するメソッド
    sample: function() {
      var index = Math.floor(Math.random() * this.length);

      return this[index];
    }
  });

  // String.prototypeを拡張する
  refine(String.prototype, {
    greeting: function() {
      return 'Hello ' + this + '!';
    }
  });
});

また、usingは複数のモジュールを一度に利用できる。

Refinements.using('TestRefine', 'TestRefine2', function() {
  // ...
});

これで基本的によくないとされているネイティブオブジェクトのprototype拡張も比較的安全に行うことができるようになる。

ただし(致命的な)欠点にusingの中で非同期処理があった場合、非同期処理の中では拡張が使えないというのがある。

Refinements.using('TestRefine', function() {
  // ここでは使えるけど
  var elem = [1, 2, 3].sample();

  setTimeout(function() {
    // ここでは使えない
    var elem = [1, 2, 3].sample();
  }, 100);
});

doneみたいな関数を引数にとって終了を待ってもいいけどそしたらusingの外でも拡張が有効になる場合がでてきてRefinementsとはなんだったのかということになるので非同期の場合は諦めた。

つまり非同期処理が多用されるJavaScriptにおいてこのモジュールは使いものにならんということですね。

このエントリーをはてなブックマークに追加