tombfixやtaberarelooのpatchでよく見かけるaddAroundの挙動について

経緯

ということで割と真面目にtombfix/taberarelooのユーティリティであるaddAroundについて理解しておきたかった&参照しやすいところに記事が欲しかったので書く。前は解説もどっかにあった気はするんだけどナー

[追記]前あった解説

ということで前あったaddAroundの解説、大元たるtomblooid:brazilさんのところにありました。

跡地 d:id:brazil:20080721:1216579832
Tombloo Patch、はてなブックマークでタイトルを修正しないようにする、アスペクト - 実用

[/追記]

around advice

さてこのaddAround、モノとしてはアスペクト指向プログラミング(Aspect-Oriented Programming;AOP)の流儀におけるaround adviceというものらしく、around adviceで検索(google:around advice)するとそれっぽい記事がちゃんと引っかかるようになる。AspectJの記事とか。
参考になりそうだなと思ったのだとこれ(AspectJ ŠÂ‹«‚ð\’z‚µ‚悤 - advice)。

おもむろにコード読み

さて、話題の発端であるtaberarelootwitterポスト用パッチ(Taberareloo Patch: Fix Twitter.update 2018.04 · GitHub)でも使われているaddAroundの挙動を確認するのが目的なので、コードを追う。
現行のtaberarelooリポジトリからaddAroundの定義箇所を抜粋。
taberareloo/utils.js at ac61c596c565f8e381dfa806d141ad8822c9d0f9 · taberareloo/taberareloo · GitHub

  /**
   * メソッドへアラウンドアドバイスを追加する。
   * 処理を置きかえ、引数の変形や、返り値の加工をできるようにする。
   *
   * @param {Object} target 対象オブジェクト。
   * @param {String || Array} methodNames
   *        メソッド名。複数指定することもできる。
   *        set*のようにワイルドカートを使ってもよい。
   * @param {Function} advice
   *        アドバイス。proceed、args、target、methodNameの4つの引数が渡される。
   *        proceedは対象オブジェクトにバインド済みのオリジナルのメソッド。
   */
  function addAround(target, methodNames, advice) {
    methodNames = [].concat(methodNames);

    // ワイルドカードの展開
    for (var i = 0; i < methodNames.length; i++) {
      if (methodNames[i].indexOf('*') === -1) {
        continue;
      }

      var hint = methodNames.splice(i, 1)[0];
      hint = new RegExp('^' + hint.replace(/\*/g, '.*'));
      for (var prop in target) {
        if (hint.test(prop) && typeof(target[prop]) === 'function') {
          methodNames.push(prop);
        }
      }
    }

    methodNames.forEach(function (methodName) {
      var method = target[methodName];
      target[methodName] = function () {
        var self = this;
        return advice(
          function (args) {
            return method.apply(self, args);
          },
          arguments, self, methodName);
      };
      target[methodName].overwrite = (method.overwrite || 0) + 1;
    });
  }

  exports.addAround = addAround;

(あとで修正すること前提で大雑把に)*1

ここで注目すべきは advice(proceed, args, target, methodName) のかたちで第3引数の adivice が呼び出されるということ。
proceedはオリジナルの、本来呼び出されるはずのメソッドであるため、例えば advice を

function advice(proceed, args, target, methodName){
  return proceed(args);
}

のように定義してaddAroundに渡すと、元々のメソッドをそのまま実行して結果を返すことになる。
ここで元々のメソッドをそのまま実行することも可能というところがミソで、
・事前処理

function advice(proceed, args, target, methodName){
  // 何か処理を加えて args を改変
  return proceed(args);
}

・事後処理

function advice(proceed, args, target, methodName){
  var result = proceed(args);
  // 何か処理を加えて result を改変
  return result;
}

のような処理の追加が簡単にできる。

もちろん元の処理を呼び出さずに完全にカスタムしたメソッドを実行しても良いし、事前処理・事後処理の両方を実行したって良い。基本的に何でもできる。

*1:修正しました