Posts Object.defineProperty〜TypeScriptのDecoratorまで
Post
Cancel

Object.defineProperty〜TypeScriptのDecoratorまで

ちょっぴり前置き

そもそもAngularJSの2を試そうと思ったら、のっけから@のアノテーション(JavaScriptではデコレータと言うらしい)が出てきて、これを理解するには「defineProperty」を理解していないとダメだという人がいるので、とりあえず、その辺から勉強してみることにしました。

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor 関数 (JavaScript)では

指定されたオブジェクトの own プロパティ記述子を取得します。own プロパティ記述子は、オブジェクトで直接定義され、オブジェクトのプロトタイプから継承されない記述子です。

と書かれています。「プロパティ記述子」というのが、たぶん、英語的には「Descriptor」なので、この「Descriptor」を理解しないと駄目そうです。

その「Descriptor」を取得するメソッドがgetOwnPropertyDescriptorなので、とりあえず実行してみましょう。次のソースをnodeあたりで実行すると、コメントのようにコンソールに出力されます。

1
2
3
4
5
var obj = {x: 1};

console.log(Object.getOwnPropertyDescriptor(obj, 'x'));
//->{ value: 1, writable: true, enumerable: true, configurable: true }

{x: 1}というオブジェクトは、xというプロパティしかありませんが、そのxの「Descriptor」をObject.getOwnPropertyDescriptor({x:1}, ‘x’)で見たのが上記のコメントです。

1
2
3
4
   value:1
   writable: true
   enumerable: true
   configurable: true

というプロパティがxというプロパティに対して自動で作成されているということです。ちなみに、だいたいの意味は次のようです。

  • writable: true (書き込み可→falseだと書き込んでも値が変わらない)
  • enumerable: true (列挙可→for .. in文で現れる)
  • configurable: true (再定義可→値という観点で言うと、「writable: false, configurable: true」でも「writable: true, configurable: false」でも、書き込めるみたいですが、一度「configurable: false」にしたものを、すぐ下で触れる再定義を行おうとすると例外がスローされるそうです)

つまりJavaScriptのプロパティには、そのプロパティの性質などを表すプロパティ(属性)が設定されており、保持されているということになります。

また、上記の例は「Data descriptor」と呼ばれているもので、別に、「Accessor descriptor」というものがあります(後述)。

Object.defineProperty

さて、上記の「Descriptor」を、そのオブジェクト生成後に変更することができるのがdefinePropertyです。

下の例は、writableを変更して、値を代入しても値に変化がないことを確認しています。

>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var obj = {x: 2};

console.log(Object.getOwnPropertyDescriptor(obj, 'x'));
//->{ value: 2, writable: true, enumerable: true, configurable: true }
console.log(obj);
//->{ x: 2 }

Object.defineProperty(obj, 'x', {
    value        : 1,
    writable     : false,   // 書き込み不可
    enumerable   : true,
    configurable : true
});
console.log(Object.getOwnPropertyDescriptor(obj, 'x'));
//->{ value: 1, writable: false, enumerable: true, configurable: true }

console.log(obj);
//->{ x: 1 }

// 値を変更
obj.x = 100;
console.log(obj);
//->{ x: 1 }

Accessor descriptor

先ほどのdefinePropertyで「Accessor descriptor」も設定できます。

設定にはFactoryで指定するようです。つまり、setやgetというキーを持っているオブジェクトを返すFunctionを指定します。

>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var obj = {x: undefined};
console.log(Object.getOwnPropertyDescriptor(obj, 'x'));
//->{ value: undefined, writable: true, enumerable: true, configurable: true }

Object.defineProperty( obj, "x", (function() {
    var value;
    return {
        set: function( v ) {
            if( 0 <= v && v <= 100 ) { value = v }
            else { throw new Error( "値は 0 から 100 までの範囲で指定してください" ) }
        },
        get: function() { return value }
    }
})());

obj.x = 1;
console.log(obj.x);
//->1

obj.x = 1000;
//->src03.js:10
//->           else { throw new Error( "値は 0 から 100 までの範囲で指定してください" ) }
//->......

上の20行目で1000を代入しようとして、例外が起こり、意図した通りになっていることが確認できます。

Decorator

先ほどの「defineProperty」を使うと、すでにあるオブジェクトに対して、色々なものを追加したりできそうな感じですね。

そして、TypeScriptはそれを使って、Decoratorを実現しているようなんです。

まずは簡単な例を見てみます(TypeScript)。下のソースはTypeScript Decoratorsにあった例(一部変更)です。

Personというクラスがあって、2つのフィールドがあります(nameとisAdmin)。このクラスに@readonlyというDecoratorが付いています。

8行目から11行目で新しくコンストラクタを定義していて、引数のTargetが元々のコンストラクタ自体らしいので9行目で元々のコンストラクタを実行し、10行目でfreezeしています。そして、

>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@readonly
class Person {
    name: string = "hoge";
    isAdmin: boolean = false;
}

function readonly<TFunction extends Function>(Target: TFunction): TFunction {
    let newConstructor = function () {
        Target.apply(this);
        Object.freeze(this);
    };
 
    newConstructor.prototype = Object.create(Target.prototype);
    newConstructor.prototype.constructor = Target;
 
    return <any> newConstructor;
}

var p = new Person();

console.log(p);

p.name = "denn";

console.log(p);
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Person = (function () {
    function Person() {
        this.name = "hoge";
        this.isAdmin = false;
    }
    Person = __decorate([
        readonly
    ], Person);
    return Person;
}());
function readonly(Target) {
    var newConstructor = function () {
        Target.apply(this);
        Object.freeze(this);
    };
    newConstructor.prototype = Object.create(Target.prototype);
    newConstructor.prototype.constructor = Target;
    return newConstructor;
}
var p = new Person();
console.log(p);
p.name = "denn";
console.log(p);

How to implement a typescript decorator?から拾ってきた例です。

シェア
#内容発言者

cordova+ionicでAndroidアプリを(ちょっぴりtypescript、angularjs、gulp)

非同期はなかなか手強い(特にループが絡むと!)

坂井和郎