JavaScriptで関数を宣言するときに、引数の長さや型をある程度柔軟に受け入れるようにしたいことがよくある。いわゆるvariadic関数というやつだ。
”いい感じ”の引数を考えてみる
例えば,
doSomething(value1[, value2,...], options, callback);
のように、
- 複数の文字列
- オプションオブジェクト
- コールバック関数
をとって、以下のパターンどれでもいい感じに解釈するようにしたい。
doSomething('foo')
doSomething('foo','bar')
doSomething('foo','bar', 'baz');
doSomething('foo','bar', {verbose:true});
doSomething('foo','bar', function done(){/*...*/})
doSomething('foo','bar', {verbose:true}, function done(){/*...*/})
JavaScriptの関数内ではarguments
という特殊な変数に全ての引数が入っているから、頑張ってこれを解釈すればなんとかなる。
でも頑張らないとなんとかならない。
function doSomething(val, options, callback){
var lastArg = arguments[arguments.length - 1];
if (typeof(lastArg) !== 'function') {
options = lastArg;
callback = null;
/*...*/
}
}
さすがに毎回これをやるのはめんどくさい。
もっと簡単な方法を考えよう。
とりあえずArrayに変換してみる
arguments
はArray
に近いオブジェクトだ。しかしArray
ではない。.pop()
や.shift()
といったメソッドは使えない。
そこでよく見るのはarguments
をArray
に変換してしまう方法。
function doSomething(val, options, callback){
var args = Array.prototype.slice.call(arguments, 0);
var lastArg = args.pop();
if (typeof(lastArg) === 'function' ) {
}
}
ちょっとだけ楽になる。
引数解釈用のモジュールを設計してみる
だけどArrayに変換するだけだと、まだまだ面倒くさい。
考慮パターンとして
- 最後の引数が関数の場合はコールバックとして扱う
- コールバック以外で最後の関数がオブジェクトの場合はオプションとして扱う
- オプションとコールバック以外の引数は可変長の引数として扱う
みたいなことをしたいのだが、そのためにはいちいち型比較をして、if文を書いて、
みたいなことをしなきゃいけない。
だけどそもそもif文書きたくない。
いっそ、引数解釈専用のモジュールを作ってそいつによしなにやらせるようにしよう。
とりあえず、必要な機能としては
- 引数の先頭・末尾から、型が合致する場合だけ値を取り出す。
- 未処理のやつをまとめて取得する
あたりか。
シグネチャとしてはとして、
var argx = require('argx'); // 引数解釈用のモジュール
function doSomething(values, options, callback) {
var args = argx(arguments);
callback = args.pop('function'); // 末尾が関数の場合のみ取得
options = args.pop('object'); // 残りの末尾がオブジェクトの場合のみ取得
values = args.remain(); // 残りをまとめて取得
/*...*/
}
な感じにしよう。
引数解釈用のモジュールを実装してみる
これを実現するためには
- 未処理の引数を管理する
- 型判定をして一致した場合のみ取り出す
的なクラスを定義すれば良い。
/** 引数解釈クラス */
function Argx(args) {
this.values = Array.prototype.slice.call(args, 0);
}
Argx.prototype = {
values: undefined,
_slice: function (index, type) {
var s = this;
var hit = (!type) || (typeof(s.values[index]) === type);
if (hit) {
return s.values.splice(index, 1).shift();
} else {
return undefined;
}
},
/** 先頭の型が一致した場合のみ取得 */
shift: function (type) {
var s = this;
return s._slice(0, type);
},
/** 末尾の型が一致した場合のみ取得 */
pop: function (type) {
var s = this;
return s._slice(s.values.length - 1, type);
},
/** 残り全部取得 */
remain: function () {
var s = this;
var values = s.values;
s.values = undefined;
return values;
}
};
あとはこいつのインスタンスを作る関数を用意して、
/** 引数解釈関数 */
function argx(args) {
return new Argx(args);
}
こんな感じでつかう。
function doSomething(values, options, callback) {
var args = argx(arguments); //Argxクラスのインスタンス生成
callback = args.pop('function');
options = args.pop('object');
values = args.remain();
console.log({
callback: callback,
options: options,
values: values
}); // -> { callback: [Function: done], options: { verbose: true }, values: [ 'v1', 'v2' ] };
}
doSomething("v1", "v2", {verbose: true}, function done() {
});
引数解釈用のモジュールを実用レベルに実装してみる
上に書いたコードはあくまで簡易版だ。
実用しようと思うと他にもいろいろ考慮するパターンが出てくる
-
typeof
で判定できない独自オブジェクトを判定したい - 複数の型を
or
条件で結びたい - 配列型を判定したい
- 複数まとめてshift/popしたい
など。
実際使ってみると後からあれこれでてきたので、npmのパッケージとしてまとめた
とりあえずNode.js
版だけ。browser用はいずれ気が向いたらやるかも。(やらないかも。)