Underscore.php で Underscore.js と同じ書き方をしたら挙動が違った

2014年10月19日 01:34

今回は「ハマりどころ」とかいう次元ではなく、JavaScript と PHP の変数スコープの違いを忘れていただけの話ではあるのだが、一応解説しておく。

まず、ツイート中で「本家」と言っている Underscore.js について。enja-oss による日本語訳の「イントロダクション」によれば、こう紹介されている。

Undersocre は JavaScript 用のユーティリティベルトライブラリで、Prototype.js(または Ruby)のような沢山の関数型プログラミングサポートを提供しますが、JavaScript の組み込みオブジェクトは一切拡張していません。jQuery 製のタキシードと、Backbone.js 製のサスペンダーと一緒に着るネクタイであります。

たとえば _.find() の場合は以下のような感じになる。

var fluits = [
    {
        name: 'apple',
        color: 'red'
    },
    {
        name: 'orange',
        color: 'orange'
    },
    {
        name: 'banana',
        color: 'yellow'
    }
];

var red_fluit = _.find(fluits, function(fluit) {
    return fluit.color == 'red';
});

Underscore.js をご存知ない方にもなんとなくお分かりいただけると思うが、この _.find() は $fluits の要素を順にイテレータに渡し、最初に返り値が true となった要素を返す関数である。

そして以下が、PHP 移植版の Underscore.php で書いた場合。

$fluits = array(
    array(
        'name' => 'apple',
        'color' => 'red'
    ),
    array(
        'name' => 'orange',
        'color' => 'orange'
    ),
    array(
        'name' => 'banana',
        'color' => 'yellow'
    )
);

$red_fluit = __::find($fluits, function($fluit) {
    return $fluit['color'] == 'red';
});

が、これを定義済み変数と比較すると、思いどおりの結果にはならない。

$target_color = 'red';

$red_fluit = __::find($fluits, function($fluit) {
    return $fluit['color'] == $target_color;
});

イテレータの返り値がいつまで経っても true にならず、$red_fluit には何も代入されない。ところが、これが JavaScript なら正常に動作する。

var target_color = 'red';

var red_fluit = _.find(fluits, function(fluit) {
    return fluit.color == target_color;
});

前述したとおり、これは単に変数のスコープの問題である。PHP の場合は関数(イテレータ)内の $target_color がローカルスコープとなり、未定義であるからだ。関数外で宣言された(グローバル)変数を関数内で参照した場合、JavaScript では関数外で代入された値が得られるが、PHP の場合は基本的に関数内でしかスコープが有効ではないため未定義となる。よって、PHP において関数の内部でグローバル変数を参照したい場合は、関数の内部でグローバルとして宣言する必要がある。なんだかややこしいが、ここはそういうものだと思うしかない。

ツイートにも書いたが、以下のように書けば問題なく動作する。

$target_color = 'red';

$red_fluit = __::find($fluits, function($fluit) {
    global $target_color;
    return $fluit['color'] == $target_color;
});

もしくは、こんな書き方もあるにはある。

$target_color = 'red';

$red_fluit = __::find($fluits, function($fluit) {
    return $fluit['color'] == $GLOBALS['target_color'];
});