JavaScript に新しく導入された accessor property について

ECMAScript の 5th edition では、新しく accessor property (アクセサプロパティ) というものが導入されました。 本記事は、この accessor property について説明します。 accessor property に対応している JavaScript 処理系はまだ多くないと思いますが、少なくとも Firefox 4 と Internet Explorer 9Safari 5 は対応しているようです。

互換性の問題があるので一般の web サイト上で使うにはまだ時期が早いですが、Firefox 4 向けの拡張機能を作るときなど、特定の JavaScript 処理系でのみ動かすコードを書く場合には使っていくといいかもしれません。

accessor (アクセサ) とは

JavaScript では、オブジェクトはプロパティを持っています。 普通は、以下のように単に名前を指定して代入したり参照したりして使用します ((本記事では、出力用の関数として print を使用します。 Rhino では最初から print 関数がありますが、ない場合は各自定義してください。))。

var obj = {};
obj.prop1 = "aaaa"; // prop1 という名前のプロパティに値を代入
print( obj.prop1 ); // prop1 という名前のプロパティの値を参照

しかし、上のような方法だと、予期しない値が設定される可能性があります。 プロパティに値を代入する際に値が期待するものかどうかのチェックをしたい場合はどうしたらいいのでしょうか? 1 つの方法としては setter/getter 関数を使用する、というものがあります。

var obj = {};
(function(o) { // クロージャにより, 外部からアクセスできない変数 (value) を保持する
    var value;
    o.setProp1 = function set(v) {
        if( 0 <= v && v <= 100 ) { value = v }
        else { throw new Error( "値は 0 から 100 までの範囲で指定してください" ) }
    };
    o.getProp1 = function get() { return value };
})(obj);

obj.setProp1( 10 ); // 値の設定
obj.getProp1();     // 値の読み出し
obj.setProp1( 1000 ); // 期待されない値を設定しようとすると例外が投げられる

Java などでは、このような getter/setter を使用する方法が一般に使われています。 一方、Ruby などには accessor と呼ばれるもの *1 があり、代入文形式でプロパティの設定や読み出しを書いて、内部的には getter/setter 関数を呼ぶ、ということができます。 以下は Ruby の例です。

class C
  # 値の設定をするための属性
  def prop1=(v)
    raise "値は 0 から 100 までの範囲で指定してください" unless 0 <= v and v <= 100
    @prop1 = v
  end
  # 値の読み出しをするための属性
  def prop1; @prop1 end
end

obj = C.new
obj.prop1 = 10 # 値の設定
obj.prop1      # 値の読み出し
obj.prop1 = 1000 # 期待されない値を設定しようとすると例外が投げられる

obj.prop1 = 1000 という風に代入文の形式をしていますが、実際には関数 (メソッド) が呼び出されており、値が期待するものかどうかのチェックを行うなどの処理が可能です。 関数形式ではなく単純な代入文や参照のような形式でコードを書き、getter/setter を使う方法と同じように値のチェックができる、というのが accessor です。

ECMAScript *2 にも、5th edition で accessor が導入されましたので、新しい JavaScript 処理系ではこのような accessor が使用できるようになっています。 このような accessor としてのプロパティを、JavaScript では accessor property と呼びます。

ちなみに、今までから使われていたようなプロパティは data property と呼ばれます。

accessor property の定義方法

accessor property を定義するには、オブジェクトリテラルの中に以下のように書きます。 以下では、propertyName という名前の accessor property を設定しています。

// オブジェクトリテラル
var obj = {
    // プロパティの読み出し用
    get propertyName() { /* 関数本体 */ },
    // プロパティの設定用
    set propertyName(v) { /* 関数本体 */ }
}

この方法では、すでに存在するオブジェクトに accessor property を追加することはできません。 すでに存在するオブジェクトに accessor property を追加する場合は、Object.defineProperty メソッドなどを使用する必要があります。 Object.defineProperty メソッドについては 次の記事 を参照してください。

継承した accessor property への代入は?

継承された accessor property と同じ名前のプロパティへの代入文は、新しくプロパティが作られるのか、それとも継承したプロパティの [[Set]] 属性の関数が呼び出されるのか、という疑問が出てくるかもしれません。 結論から言うと、継承された accessor property への値の代入は、継承されたプロパティの [[Set]] 属性が使用されます。

var obj = (function() { // クロージャにより, 外部からアクセスできない変数 (_prop1) を保持する
    var _prop1 = 10;
    return {
        // accessor property を定義
        get prop1() { return _prop1 },
        set prop1(v) {
            if( 0 <= v && v <= 100 ) { _prop1 = v }
            else { throw new Error( "値は 0 から 100 までの範囲で指定してください" ) }
        },
        
        // ただの data property
        prop2: 20
    };
})();

// Object.create は、第 1 引数を prototype に持つ新しいオブジェクトを生成する
// すなわち sub は obj を継承する
var sub = Object.create( obj );

// 継承したプロパティの参照
print( sub.prop1 + ", " + obj.prop1 ); // 10, 10
print( sub.prop2 + ", " + obj.prop2 ); // 20, 20

// 派生オブジェクトの同名のプロパティに値を代入してみる
sub.prop1 = 50;
sub.prop2 = 40;

// 継承したプロパティの参照
print( sub.prop1 + ", " + obj.prop1 ); // 50, 50
print( sub.prop2 + ", " + obj.prop2 ); // 40, 20

data property と比較するとわかりやすいですが、継承元が同名のプロパティを持っていても、それが data property であれば、プロパティへの代入文は自分自身に新しくプロパティを作ります。 よって、継承元には影響を与えません。

一方、継承元が同名の accessor property を持っている場合は、その accessor property が使われますので、継承元にも影響を与えます。

まとめ

  • ECMAScript 5th edition で、accessor property が導入された
  • オブジェクトリテラル内で関数定義をするように accessor property の定義ができる
  • 既に存在するオブジェクトに accessor property を新たに定義するには Object.defineProperty メソッドを使う
  • accessor property が使える JavaScript 処理系はまだ多くない

*1:Ruby では 「属性」 (attribute) と呼ばれています。

*2:JavaScript やそれに類似の言語の統一的な言語仕様