シンプルなポーカー ゲームで理解する ECMAScript 5

本記事は、マイクロソフト本社の IE チームのブログから記事を抜粋し、翻訳したものです。 

【元記事】Exploring ECMAScript 5 with a Simple Game of Poker (2011/4/19 1:08 AM)

     

あらゆるブラウザーで同じマークアップ同じスクリプトが使えるようにすることを目指す私たちにとって、JavaScript として広く知られる言語の新しい標準である ECMAScript 5 のサポートは欠かせない取り組みの 1 つです。この数か月の間に、ブログでも ECMAScript 5 に関する記事をいくつも公開してきました (以下を参照)。

IE9 の ECMAScript 5 対応がもたらすあらゆるメリットを説明することを目的として、IE9 RC のリリースに合わせて Test Drive デモを追加で公開しました。

この Texas Hold’Em ゲーム (レドモンドでは親しみを込めて「T’ECMAScript Hold’Em」と呼んでいます) も、再利用可能なクラス ライブラリの記述方法を説明するために作成されました。

ES5 が提供する機能を数多く活用することで、開発者による制御範囲を拡張すると共に、よりオブジェクト指向なデザイン パターンのモデリングが可能になっています。

Web 標準ベース アプリケーションの普及と複雑化が進む現在、この 2 つは必要不可欠な要素と言えます。

はじめに: 機能を検出する

この Texas Hold’Em ゲームを使って ECMAScript 5 の最先端の活用方法を説明していきましょう。

まず機能の検出を行い、このアプリケーションが必要な機能をサポートするブラウザーで常に実行されるようにします。

FeatureDetectES5Properties.js ファイルによって、ECMAScript 5 の重要な機能である defineProperties がサポートされていることが検出されれば、ランタイム エラーが発生することなくアプリケーションの残りの部分が読み込まれます。

これは ES5 サポートに対する包括的な適合性テストにはなりませんが、最新ブラウザーでアプリケーションの実行が可能かどうかをチェックする簡易的な手段としては十分有用です。

 if (!Object.defineProperties || !Object.defineProperty) {
    $FeatureDetect.fail("This demo requires ECMAScript 5 properties API support.");
}

開発者による制御範囲を拡張する

属性

ES5 で行われた最も重要な変更の 1 つとして、開発者がプロパティ属性を直接制御できるようになったことがあります。これにより、ライブラリの完成後に、ユーザーによってキー メソッドが誤って上書きされるという心配がなくなりました。

属性によって ECMAScript オブジェクトを完全に制御できるようになったことで、開発者は必要とされる保護の内容に応じて、適切かつきめ細かにキー メソッドや値を保護できるようになりました。

属性は ”プロパティ記述子” を使って割り当てられます。

 new_card_desc = {
    value: {
        configurable: false,
        enumerable: false,
        writable: true,
        value: value
    },
    binval: {
        configurable: false,
        enumerable: false,
        writable: true,
        value: binval
    }
};

この記述子は、このポーカー ゲームに関連付けられたライブラリにあります。

value はプレーン テキスト文字列で、普通のトランプのスーツ (絵柄) とランク (数字) を表しています。

binval は同じ情報をバイナリ表記したもので、これによってどのような 2 枚のカードでも必ず比較できるようになります。

多くの内部メソッドがこれらのプロパティを利用しているため、削除されてしまわないようこれらを保護する必要があります。

また Card が作成された後には、これらが変更されないようにしなければなりません。それには writable:false を設定して新しい値が割り当てられないようにし、configurable:false を設定して値が永続的に保持されるようにします。

enumerable 属性は、オブジェクトが for-in によって検査されている間、プロパティを隠れた状態にします。

これらのプロパティに対するすべての参照はその他のメソッドの内側に配置されるため、これらを他のライブラリに公開する必要がなくなります。

enumerable:false を設定してオブジェクトをクリーンアップし、その他のライブラリによって有用なプロパティだけが公開されるようにします。

この ES5 の追加事項に詳しい開発者であれば、記述子に含まれるすべての属性の既定値が false であることに気付くのではないでしょうか。これを踏まえると、記述子は以下のように書き換えることができます。

 new_card_desc = {
    value: {
        value: value
    },
    binval: {
        value: binval
    }
};

この例は非常に単純に書かれたものです。プロパティの value 属性にはどのような JavaScript オブジェクトでも設定できるため、メソッド全体を保護するには非常に使いやすい方法ということになります。

この例は、Deck.js ファイルの Shuffle メソッドで使われています。

 Shuffle: {
    value: function () {
        var i, j, tempi, tempj;
        this.dealt = 0;
        this.discardPile = [];
        for (i = 0; i < this.length; i++) {
            j = Math.floor(Math.random() * (i + 1));
            tempi = this[i];
            tempj = this[j];
            this[i] = tempj;
            this[j] = tempi;
        }
    }
}

writable および configurable 属性を false に設定することで、誤ってあるいは意図に反してロジックが変更されてしまう可能性はほぼなくなります。ポーカーでの不正を禁止するようなものですね。

Deck オブジェクト自体は Card のオブジェクトの配列です。すべてのメソッドを enumerable:false に設定することによって、Deck に対する反復処理は、すべての使用可能なカードを反復処理していることと同じことになります。

これらのメソッドはアクセス可能な状態が維持されますが、メソッドがその他の場所のロジックに影響を与えてしまうことはなくなります。

Setter と Getter

Setter と Getter は、汎用プログラミング言語においてよく使われるプログラミング処理です。これらが提供する強力なメリットの 1 つが、通常 "計算値" と呼ばれるプログラム的に生成されたデータに、簡単に直接アクセスできることです。

Card.js ファイルの Card オブジェクトの表現を例に、これを説明しましょう。

 image: {
    enumerable: true,
    get: function () {
        var iSuit, iRank;        
        switch (this.suit) {
            case "S": iSuit = "Spades"; break;
            case "H": iSuit = "Hearts"; break;
            case "D": iSuit = "Diamonds"; break;
            case "C": iSuit = "Clubs"; break;
            default: throw "No such suit: " + this.suit;
        }
        if (isNaN(parseInt(this.rank, 10))) {
            switch (this.rank) {
                case "T": iRank = "10"; break;
                case "J": iRank = "Jack"; break;
                case "Q": iRank = "Queen"; break;
                case "K": iRank = "King"; break;
                case "A": iRank = "Ace"; break;
                default: throw "No such rank: " + this.rank;
            }
        } else if (this.rank > 1) {
            iRank = this.rank;
        } else {
            throw "No such rank: " + this.rank;
        }
        return iSuit + "_" + iRank + ".png";
    }
}

各 Card オブジェクトには対応する .png がアプリケーション内に存在し、これが表示されています。

すべてのカードに対して手動で反復処理を実行して各 .png の一意名を格納する代わりに、Deck 内のすべての Card が継承する Getter を使用すれば、各カードが既に持つ一意の情報を使って文字列を生成することができます。

これはメソッド呼び出しを使用しても作成できますが、このインスタンスでは Getter が適していると考えられます。Getter を使えば、引数が要求されたり引数が前提になることはなく、ライブラリのユーザーに不要なメソッドが公開されることもありません。ユーザーにとっては単なるデータ プロパティでしかありません。

Object.defineProperty

Object.defineProperty メソッドや Object.defineProperties メソッドを使うと、ES5 の新しい属性を適用できるようになります。

この 2 つのメソッドの違いは、1 回のメソッド呼び出しでユーザーが定義できるプロパティの数にあります。

このポーカー ゲームではすべてのプロパティがこの 2 つのメソッドを使って定義されています。もちろんプロパティを宣言する方法は他にもあるので、開発者は自分が使用したいプロパティ宣言方法を組み合わせることができます。

このことから、これらの新しいメソッドが柔軟性に優れているということがよくわかります。Card.js ファイルにこれを使った例があります。

 var CardPrototype = {};
Object.defineProperties(CardPrototype, card_proto_desc);

Object.seal と Object.freeze

記述子や属性の直接宣言以外にも方法はあります。

Object.seal および Object.freeze を使うと、適切なレベルのプロパティ保護をオブジェクト全体に簡単に拡張できるようになります。これを説明する前に、ES5 によって追加された最新の属性である extensible について説明する必要があります。

extensible はオブジェクトのプロパティではなく、オブジェクト自体に追加されます。Object.preventExtensions を呼び出すことでこのフラグが "永続的" に設定されるようになり、あらゆるプロパティがそのオブジェクト上で定義されることを回避できますが、seal と freeze はこの機能のスーパーセットです。

Object.seal を使うと、オブジェクトの拡張性とオブジェクトのプロパティの構成可能な特性をワンステップでロックダウンできます。書き込み可能なプロパティの変更可能な状態は維持されますが、削除はできなくなります。

さらに保護を強化したい場合は Object.freeze を使います。すると値の割り当てを含む、オブジェクトとそのプロパティに対するすべての変更が禁止されます。

これはたとえば以下のような、スーツやランクを保持する配列などの定数を宣言する場合に使うと便利です。

 /* すべての使用可能なカード スーツ */
var Suits = {    
      "S" : 3, // スペード
    "H" : 2, //ハート
    "D" : 1, //ダイアモンド
    "C" : 0  //クローバー
};
Object.freeze(Suits);
 
/* すべての使用可能なカード ランク */
var Ranks = {
    "2" : 2,
    "3" : 3,
    "4" : 4,
    "5" : 5,
    "6" : 6,
    "7" : 7,
    "8" : 8,
    "9" : 9,
    "T" : 10, // 10
    "J" : 11, // ジャック
    "Q" : 12, // クイーン
    "K" : 13, // キング
    "A" : 14  // エース
};
Object.freeze(Ranks);

オブジェクト指向のアプローチ

Object.create

「このプロトタイプを持つ新しいオブジェクトが必要」という場合は、Object.create を使うと簡単です。以前はこれを以下のように記述していました。

 <new_object>.prototype = <proto_object>;
 return new <new_object>;

このように宣言するのではなく、Object.create を使えば、新しいプロパティを <new_object> に正確に追加して、共有プロトタイプから継承されながらもインスタンスごとに一意の値を持つ新しいオブジェクト インスタンスを作成することができます。この簡単な例が Card.js の末尾にあります。

 /*コンストラクター*/
var NewCard = function (suit, rank) {
 
  // 各インスタンス用の一意の値をセットアップ
    var value, binval, new_card_desc;
    value = suit.concat(rank);
    binval = Suits[suit];
    binval = binval << 4;
    binval = binval + Ranks[rank];
    new_card_desc = {
        value: {
            writable: true,
            value: value,
        },
        binval: {
            writable: true,
            value: binval
        }
    };
// Card オブジェクトを拡張する一意のインスタンスを返す
    return Object.create(CardPrototype, new_card_desc);
};

この CardPrototype は共有メソッド (Comparison メソッドなど) を格納し、新しい記述子はカードの各インスタンス用の一意な値を格納します。

Object.keys

Object.keys は、オブジェクト上の ”列挙可能な” プロパティの配列を返します。 Deck コンストラクターから簡単な例を紹介します。

 // Deck のビルド
suits = Object.keys(Suits);
ranks = Object.keys(Ranks);
for (var s in suits) {
    for (var r in ranks) {
        deckArray[deckArray.length] = NewCard(suits[s], ranks[r]);
    }
}

定数の 2 つの配列上のすべてのプロパティに対する反復処理を実行することで、ランクとスーツの実現可能な組み合わせがすべて検出され、Deck 全体がコンパイルされます。

Object.getOwnPropertyNames

Object.getOwnPropertyNames は、すべてのメソッドを “列挙可能”(または列挙不可能)にする場合に使用します。ただし、これによって返されるのは “独自” のプロパティ名だけであることに注意してください。つまり、プロトタイプから継承されたプロパティはスキップされます。これによってこの 2 種類を簡単に区別することができます。

アプリケーションを完成させる

ES5 の機能の多くについて、またそれらの機能がこの CardGame ライブラリの実装をどのように支えているかについて、ここまで簡単に説明してきました。(Card および Deck ファイルに含まれる) ライブラリ自体は、アプリケーション独自の要素 (ゲーム ボードのレイアウトを指定する Poker、Player、および Game ファイル) と組み合わせて使用されています。

私はアプリケーションの完成にあたってアプリケーションを JSLint にかけ、if と後続の条件の間にあるスペース、後置形式の単項演算子の使い方、格納するスコープの先頭で単一行ではなく複数行で var が宣言されていることなど、いくつかのエラーを見つけました。

このサンプルを使って ES5 を理解する

このシンプルなポーカー ゲームを IE9 の ES5 に対する幅広いサポートを理解するきっかけとしてぜひご活用ください。このコードを使うことで、ES5 をベースとした独自のカード ゲームを記述することができます。ECMAScript 5 に掛けて、ファイブ カード ドローなどはいかがですか?

IE9 をダウンロードして、コードを書く楽しさをさらに広げてください。

—Jared Straub、Test、JavaScript チーム、ソフトウェア開発エンジニア