Underscore.js はもっと評価されていい

Backbone.js が注目されがちですけど、Backbone.js を支えている Underscore.js も、実はかなり便利なライブラリです。公式サイトを見ると、よさげなメソッドが盛りだくさん。

私自身はまだ Backbone.js のついでに使っている段階ですけどね。


Underscore.js が提供するメソッドは数が多いので、今回はよく使ったものに絞って取り上げてみます。それ以外は公式ドキュメントを見てください。

each

test("each", function() {
    var data = [];
    _.each(["foo","bar","hoge","fuga"], function(n){
        data.push(n);
    });
    equals(data.length, 4);
    equals(data[0], "foo");
    equals(data[1], "bar");
    equals(data[2], "hoge");
    equals(data[3], "fuga");
            
    data = [];
    _.each({ firstName:"foo", lastName:"bar", age:18 }, function(v) {
        data.push(v);
    });
    equals(data.length, 3);
    equals(data[0], "foo");
    equals(data[1], "bar");
    equals(data[2], 18);
});

コレクション操作で一番お世話になる each メソッド。説明不要。Backbone.js ではビューの描画や JSON のパースなどで大活躍。

map

test("map", function() {
    var data = _.map([1,2,3,4], function(n) {
        return n * n;
    });

    equals(data.length, 4);
    equals(data[0], 1);
    equals(data[1], 4);
    equals(data[2], 9);
    equals(data[3], 16);
});

コレクションを変換する map メソッド。これ意外と出番ありました。例えば、JSON オブジェクトの配列をパースしてモデルに変換するときとか。

all

test("all", function() {
    var result = _.all([2,4,6,8], function(n) {
        return n % 2 === 0;
    });
    equals(result, true);
});

コレクションの要素すべてが指定した条件を満たすかチェックするメソッド。コレクションの状態によってビューの表示を切り替えたいときに使いました。

any

test("any", function() {
    var result = _.any([1,2,3,4,5], function(n) {
        return n % 2 === 0;
    });
    equals(result, true);
});

コレクションの中に指定した条件をみたすものがあるかチェックするメソッド。これも all 同様、ビューの表示を切り替えるときに使ったかな。確か。

is 系

test("isEmpty", function() {
    var result = _.isEmpty("");
    equals(result, true);

    result = _.isEmpty({});
    equals(result, true);
});
test("isNull", function() {
    var target = null;
    var result = _.isNull(target);
    equals(result, true);
});

isEmpty・isNull の他にも、isArray や isDate や isString など、大量にあります。入力検証に便利。

bind

test("bind", function() {
    var obj = { greet: "Hello" };
    var func = _.bind(function(name) {
        return this.greet + " " + name + ".";
    }, obj);

    var result = func("Ichiro");
    equals(result, "Hello Ichiro.");
});

関数を呼び出すときのコンテキストを設定するメソッド。イベントハンドラの登録やコールバックで使いました。

template

test("template", function() {
    // テンプレートをコンパイル
    var tmpl = _.template('<h2><%-title%></h2><article><%-content%></article>');

    // データをバインド
    var html = tmpl({ title:"foo", content:"bar" });

    equals(html, '<h2>foo</h2><article>bar</article>');
});

Backbone.js でビューを描画するのに必須の template メソッド。テンプレート内では if や for といった JavaScript の構文も使えます。私の場合、if は使ったけど、for は使わなかったかな。


良く使ったのはこれくらいです。あと、面白そうなメソッドもついでに紹介。

defer

test("defer", function() {
    var actual = false;

    // defer に渡した関数はテストが終わった後に実行される
    _.defer(function() {
        equals(actual, true);
        //alert(actual);
    });

    equals(actual, false);
    actual = true;
});

渡した関数をコールスタックの最後に実行する、という面白いメソッド。画面を更新するのに使うといいかも。

mixin

test("mixin", function() {
    _.mixin({
        isNullOrEmpty: function(str) {
            return _.isNull(str) || _.isEmpty(str);
        }
    });

    equals(_(null).isNullOrEmpty(), true);
    equals(_("").isNullOrEmpty(), true);
    equals(_("foo").isNullOrEmpty(), false);
});

mixin で Underscore に新しいメソッドを追加できます。ただし、追加したメソッドは _() でラップしたオブジェクトでしか使えません。jQuery プラグインみたいな感じで、Underscore プラグインをそのうち誰かが作るかもしれませんね。


こんなところかな。ちなみに、サンプルコードは QUnit を使ったユニットテストで書いてみました。全体のコードは次の通り。

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8" />
	<title>QUnit Test Suite</title>
	<link rel="stylesheet" href="qunit.css" type="text/css" media="screen">
    <script type="text/javascript" src="qunit.js"></script>
    <script type="text/javascript" src="underscore.js"></script>
    <script type="text/javascript">
test("each", function() {
    var data = [];
    _.each(["foo","bar","hoge","fuga"], function(n){
        data.push(n);
    });
    equals(data.length, 4);
    equals(data[0], "foo");
    equals(data[1], "bar");
    equals(data[2], "hoge");
    equals(data[3], "fuga");
            
    data = [];
    _.each({ firstName:"foo", lastName:"bar", age:18 }, function(v) {
        data.push(v);
    });
    equals(data.length, 3);
    equals(data[0], "foo");
    equals(data[1], "bar");
    equals(data[2], 18);
});

test("map", function() {
    var data = _.map([1,2,3,4], function(n) {
        return n * n;
    });

    equals(data.length, 4);
    equals(data[0], 1);
    equals(data[1], 4);
    equals(data[2], 9);
    equals(data[3], 16);
});

test("all", function() {
    var result = _.all([2,4,6,8], function(n) {
        return n % 2 === 0;
    });
    equals(result, true);
});

test("any", function() {
    var result = _.any([1,2,3,4,5], function(n) {
        return n % 2 === 0;
    });
    equals(result, true);
});

test("isEmpty", function() {
    var result = _.isEmpty("");
    equals(result, true);

    result = _.isEmpty({});
    equals(result, true);
});
test("isNull", function() {
    var target = null;
    var result = _.isNull(target);
    equals(result, true);
});

test("bind", function() {
    var obj = { greet: "Hello" };
    var func = _.bind(function(name) {
        return this.greet + " " + name + ".";
    }, obj);

    var result = func("Ichiro");
    equals(result, "Hello Ichiro.");
});

test("template", function() {
    // テンプレートをコンパイル
    var tmpl = _.template('<h2><%-title%></h2><article><%-content%></article>');

    // データをバインド
    var html = tmpl({ title:"foo", content:"bar" });

    equals(html, '<h2>foo</h2><article>bar</article>');
});

test("defer", function() {
    var actual = false;

    // defer に渡した関数はテストが終わった後に実行される
    _.defer(function() {
        equals(actual, true);
        //alert(actual);
    });

    equals(actual, false);
    actual = true;
});

test("mixin", function() {
    _.mixin({
        isNullOrEmpty: function(str) {
            return _.isNull(str) || _.isEmpty(str);
        }
    });

    equals(_(null).isNullOrEmpty(), true);
    equals(_("").isNullOrEmpty(), true);
    equals(_("foo").isNullOrEmpty(), false);
});
    </script>
</head>
<body>
	<h1 id="qunit-header">QUnit Test Suite</h1>
	<h2 id="qunit-banner"></h2>
	<div id="qunit-testrunner-toolbar"></div>
	<h2 id="qunit-userAgent"></h2>
	<ol id="qunit-tests"></ol>
	<div id="qunit-fixture">test markup</div>
</body>
</html>

エディタにコピペして保存し、同じディレクトリに underscore.js と qunit.js を配置し、ブラウザで表示すればテストが走ります。


私がいうのもなんですが、Underscore.js は単独でも、もっと使われていいライブラリです。Backbone.js は Underscore.js に依存しているので必須ですけど、Underscore.js 自体は他のライブラリに依存していないので、JavaScript MVC や Knockout.js でも使えます。試していませんが、Node や Titanium Mobile でも使えるかも。


クライアント MVC やるなら、コレクション操作は避けられません。表記は独特ですけど、グローバルオブジェクトを汚染しないし、使わない手はないと思うんですけど、どうでしょう。