ちょっぴり前置き
そもそも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?から拾ってきた例です。