27 November 2010

ECMA Script 5 の Property Descriptor について話してきました

最近の更新はこればかりですが, 月一で行われている仲間内のLT会がありました. 今回は ECMAScript5 の新しい機能である Property Descriptor について簡単に話しました. ほんとうは変更点をひととおりおさらいできればよかったのですが, 時間がなくこれだけに絞りました.

Property Descriptor とは, プログラマがオブジェクトのプロパティのメタデータを触るための仕組みです. プロパティの振る舞いを変更(例えば書き換え不可能にしたり列挙不可能(for/inで出ない)にしたり)することができるようになります. 設定できる項目は以下の6つ. 特に重要なのは下の3項目です.

value プロパティの値
get そのプロパティにアクセスしたときに呼ばれる関数. getter と同じ
set そのプロパティに代入したときに呼ばれる関数. setter と同じ
writable 値を変更可能か (bool)
configurable Property Descripter を変更可能か (bool)
enumerable 列挙可能か (for/in でループされるかどうか) (bool)

これらの項目を, Object.getOwnPropertyDescriptor(obj, prop) で参照, Object.defineProperty(obj, prop, desc) というメソッドで設定できます.

Property Descripter を使うことで, for/in でループされるプロパティを制御できたり, (Object.preventExtentions() と組み合わせることで) 完全に変更不可能なオブジェクトを作成できたりします. これはどちらかというとライブラリ作者側に有用な機能だと思います.

また本題とはずれているのですが, スライドの初めの方で簡単に ECMAScript の歴史についてまとめてあります. 自分は最近 JavaScript に入門した身なので, "なんで ECMA って 3 から 5 に飛んでるんだろう…?" という程度の認識だったのですが, ES4, 5, Harmony の経緯を調べると面白かったです. Wikipedia をちょっと読む程度でも楽しめました.

2010-11-28 追記

以下の内容についてスライドに間違いがあったので修正しました. 現在は修正済みのスライドが見られます.間違いはコメント欄で @ さんに指摘していただきました. ありがとうございました.

Object.preventExtensions(), seal(), freeze() について

Object.preventExtensions() はオブジェクトを引数に取り, そのオブジェクトのプロパティをそれ以降は追加できないようにします(削除は可能).

var o = {foo: 1, bar: 2};

Object.preventExtensions(o);

// 追加しようとすると TypeError
o.baz = 3; // => TypeError: Can't add property baz, object is not extensible

// 削除は可能
delete o.foo;
console.log(o.foo); // => undefined

Object.seal() はこれに加え Property Descripter の変更ができないように, Object.freeze() は seal に加え値の変更もできないようになります.

var o = {foo: 1, bar: 2};
Object.freeze(o);

// extension できない
o.baz = 10; // => TypeError: Can't add property baz, object is not extensible

// non-writable
o.foo = 3; // => strict mode だと例外
console.log(o.foo); // => 1

// non-configurable
Object.defineProperty(o, "foo", {configurable: true}); // => TypeError: Cannot redefine property: defineProperty


Object.getOwnPropertyDescriptor(o, "foo");
// =>
// configurable: false
// enumerable: true
// value: 1
// writable: false

仕様書(PDF)にも次のように書かれています.

15.2.3.8 Object.seal ( O )
When the seal function is called, the following steps are taken:
1. If Type(O) is not Object throw a TypeError exception.
2. For each named own property name P of O,
    a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P.
    b. If desc.[[Configurable]] is true, set desc.[[Configurable]] to false.
    c. Call the [[DefineOwnProperty]] internal method of O with P, desc, and true as arguments.
3. Set the [[Extensible]] internal property of O to false.
4. Return O.
15.2.3.9 Object.freeze ( O )
When the freeze function is called, the following steps are taken:
1. If Type(O) is not Object throw a TypeError exception.
2. For each named own property name P of O,
    a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P.
    b. If IsDataDescriptor(desc) is true, then
        i. If desc.[[Writable]] is true, set desc.[[Writable]] to false.
    c. If desc.[[Configurable]] is true, set desc.[[Configurable]] to false.
    d. Call the [[DefineOwnProperty]] internal method of O with P, desc, and true as arguments.
3. Set the [[Extensible]] internal property of O to false.
4. Return O.

"3. Set the Extensible? internal property of O to false." と書かれています. また extension の可否は内部の "Extensible" というフラグで管理しているということも分かりました.

この関係性をスライド作成時には理解しておらず, メソッドの紹介順も seal -> freeze -> preventExtensions となっていて不親切でした. メソッドを preventExtensions -> seal -> freeze の順にし, 内容についても追記・修正しました.

各ブラウザの対応状況

当初のスライドでは IE8以降, Safari5 は対応としていました. まず IE8 ですが, Object.defineProperty() などは存在してはいるものの, それはDOMオブジェクトに対してしか適用できない不完全なものです.

加えて, preventExtensions, seal, freeze などのメソッドもありません. これを対応済みとするのは不適切なので, 現バージョンでは 9以降 と修正しました.

Safari5 も preventExtensions, seal, freeze などのメソッドに対応していません. スライド中でこれらのメソッドを紹介しておきながら, それが使えないブラウザを対応済みとするのは不親切なので, こちらは注釈を付けました.

参考