回答編集履歴
3
「プロパティに一定の制約を設ける」を追記
answer
CHANGED
@@ -87,8 +87,86 @@
|
|
87
87
|
※ECMAScript 上で似た性質を持つ関数の仕様を調べてみるといいかもしれません。
|
88
88
|
引数が省略された時の初期値や型の制約など、設計上の考え方で参考になるものがあります。
|
89
89
|
|
90
|
+
### プロパティに一定の制約を設ける
|
91
|
+
|
92
|
+
プロパティに一定の制約を設けるコード事例を書くと、次のようになります。
|
93
|
+
|
94
|
+
```JavaScript
|
95
|
+
'use strict';
|
96
|
+
const Person = (() => {
|
97
|
+
const privateMap = new WeakMap;
|
98
|
+
|
99
|
+
return class Person {
|
100
|
+
constructor (name, age) {
|
101
|
+
privateMap.set(this, Object.create(null));
|
102
|
+
|
103
|
+
this.name = name;
|
104
|
+
this.age = age;
|
105
|
+
}
|
106
|
+
|
107
|
+
get name () {
|
108
|
+
return privateMap.get(this).name;
|
109
|
+
}
|
110
|
+
|
111
|
+
set name (nameString) {
|
112
|
+
return privateMap.get(this).name = String(nameString);
|
113
|
+
}
|
114
|
+
|
115
|
+
get age () {
|
116
|
+
return privateMap.get(this).age;
|
117
|
+
}
|
118
|
+
|
119
|
+
set age (ageNumber) {
|
120
|
+
ageNumber = Number(ageNumber);
|
121
|
+
ageNumber = (Number.isNaN(ageNumber) || ageNumber < 0) ? -1 : Math.floor(ageNumber);
|
122
|
+
|
123
|
+
return privateMap.get(this).age = ageNumber;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
})();
|
127
|
+
|
128
|
+
const p1 = new Person('taro', 10);
|
129
|
+
console.log(p1.name); // "taro"
|
130
|
+
console.log(p1.age); // 10
|
131
|
+
|
132
|
+
p1.name = 1;
|
133
|
+
console.log(p1.name, typeof p1.name); // "1" "string"
|
134
|
+
p1.age = 'fugafuga';
|
135
|
+
console.log(p1.age, typeof p1.age); // -1 "number"
|
136
|
+
|
137
|
+
const p2 = new Person;
|
138
|
+
console.log(p2.name, typeof p2.name); // "undefined" "string"
|
139
|
+
console.log(p2.age, typeof p2.age); // -1 "number"
|
140
|
+
```
|
141
|
+
|
142
|
+
`Person` には次のルールが設けられています。
|
143
|
+
|
144
|
+
- `name` プロパティは必ず String 型である (MUST)
|
145
|
+
- `age` プロパティは -1 以上の整数である (MUST)
|
146
|
+
- `age` プロパティは有効な年齢を設定された場合に 0 以上の整数を返す (MUST)
|
147
|
+
- `age` プロパティは無効な年齢を設定された場合に -1 を返す (MUST)
|
148
|
+
|
149
|
+
このルールは絶対的であり、引数が省略されても、後でプロパティが再設定されても必ず、そうなります。
|
150
|
+
ですので、プロパティの有無を確認する必要がなければ、プロパティに格納された値の型を確認する必要もないのです。
|
151
|
+
面倒な処理は全て、Person クラスがやってくれます。
|
152
|
+
|
153
|
+
**(補足)**
|
154
|
+
|
155
|
+
> Numberは0かnanですか。型によって初期値がどうなるという視点は良くないということでしたが、この辺りは気になるところです。
|
156
|
+
|
157
|
+
`age` プロパティは Number 型ですが、年齢として有効な数値は「0 以上の整数」になります。
|
158
|
+
従って、無効値が設定された場合に「falsy な 0 を返す」という実装は出来ません。
|
159
|
+
`age` プロパティは「整数値を返すプロパティ」なので、`String#indexOf` に倣い、-1 を返すルールとしました。
|
160
|
+
このように、考え方次第では、Number 型でも falsy な 0, NaN 以外を返すルールにする場合があります。
|
161
|
+
|
162
|
+
> 例に挙げて頂いた多くは初期値の話というより、関数の戻り値についてだった気がしますが、
|
163
|
+
|
164
|
+
はい。その通りですが、プロパティの getter/setter に一定のルールを設けるという意味では関数の返り値でも考え方は変わらないと思います。
|
165
|
+
例えば、`get age ()` は `function getAge () {}` に置き換える事が出来ます。
|
166
|
+
|
90
167
|
### 更新履歴
|
91
168
|
|
92
169
|
- 2017/10/08 15:37 「結局、どうするべきなのか」「プロパティの初期化をどうするのか」を追記
|
170
|
+
- 2017/10/18 23:38 「プロパティに一定の制約を設ける」を追記
|
93
171
|
|
94
172
|
Re: hayatomo さん
|
2
typo修正
answer
CHANGED
@@ -76,9 +76,9 @@
|
|
76
76
|
|
77
77
|
```JavaScript
|
78
78
|
{};
|
79
|
-
Object.create(null)
|
79
|
+
Object.create(null);
|
80
80
|
new Map;
|
81
|
-
```
|
81
|
+
```
|
82
82
|
|
83
83
|
各プロパティに何がしかの意味を込め、値にも制約やルールを設ける(「値は~型でなければならない」「プロパティfooが0以上なら、プロパティpiyoは1以上でなければならない」等)のであれば、ユーザ定義のコンストラクタでprototype プロパティに一定のルールを定める事が出来ます。
|
84
84
|
そして、`instanceof` 演算子や `Object.getPrototypeOf` によって、対象のコンストラクタを特定すれば、自身が定めたルール内でプロパティチェックを省略出来ます。
|
1
「結局、どうするべきなのか」「プロパティの初期化をどうするのか」を追記
answer
CHANGED
@@ -48,6 +48,47 @@
|
|
48
48
|
|
49
49
|
### 結局、どうするべきなのか
|
50
50
|
|
51
|
-
|
51
|
+
まず、「オブジェクトが存在しないこと」を表す必要があるのか、を設計段階で考えて下さい。
|
52
52
|
|
53
|
+
- `getElementById` が `null` を返すのは、ドキュメント上に対応するオブジェクトが存在しなかったから、です。「存在しないもの」に対して、new Element を返すのは適切とはいえません。
|
54
|
+
- `document.onclick` が `null` を返すのは、対応するイベントハンドラ関数(Object型)が存在しなかったから、です。「存在しないもの」に対して、new Function を返すのは適切とはいえません。
|
55
|
+
- `String.prototype.match ` が `null` を返すのは、正規表現に対応する文字列が存在しかったから、です。存在判定によって、後述のコードが処理しやすくなるため、でもあります
|
56
|
+
- `Array.prototype.filter` が `[]` を返すのは、`Array.prototype` に存在する関数をメソッドチェーンによって利用する為です。これは `String.prototype.match ` とは異なる設計であり、宗教上の違いもあります。
|
57
|
+
|
58
|
+
Object型が入るべき変数を初期化する事を考えた場合、 「オブジェクトが存在しない」を明示するなら `null` を代入し、そうでないなら、`new Object` や `new Foo` を代入します。
|
59
|
+
|
60
|
+
### プロパティの初期化をどうするのか
|
61
|
+
|
62
|
+
オブジェクトが存在しないのであれば、プロパティも存在しない為、この部分を考える必要はありません。
|
63
|
+
オブジェクトが存在するのであれば、`new Foo` でオブジェクトを初期化するわけですが、プロパティの初期値については設計者の思想に依存しています。
|
64
|
+
例えば、`new Array` の `length` 値は `0` ですが、これは「`length` プロパティが配列の要素数を返す」と設計者が定めているからです。
|
65
|
+
|
66
|
+
```JavaScript
|
67
|
+
new Array().length; // 0
|
68
|
+
```
|
69
|
+
|
70
|
+
`String.prototype.indexOf` が対象の文字列を発見出来なかった時に `-1` を返すのは、「存在した時に 0 以上の自然数を返す事から、存在する時の返り値と重複しない値として適当だったから」です。
|
71
|
+
同じ `Number` 型の値を持つプロパティでも、存在しない時の初期値が異なる事が分かると思います。
|
72
|
+
|
73
|
+
つまり、必要なのは「Object型の初期値とは何か」という視点ではなく、「その変数が持つ論理的な意味は何か」を考え、そこに合理的な理由を付けて初期値を決めることにあると考えます。
|
74
|
+
|
75
|
+
そのオブジェクトがただのデータとしての入れ物であれば、下記3パターンのどれかを採用する事が多いですが、
|
76
|
+
|
77
|
+
```JavaScript
|
78
|
+
{};
|
79
|
+
Object.create(null)l
|
80
|
+
new Map;
|
81
|
+
````
|
82
|
+
|
83
|
+
各プロパティに何がしかの意味を込め、値にも制約やルールを設ける(「値は~型でなければならない」「プロパティfooが0以上なら、プロパティpiyoは1以上でなければならない」等)のであれば、ユーザ定義のコンストラクタでprototype プロパティに一定のルールを定める事が出来ます。
|
84
|
+
そして、`instanceof` 演算子や `Object.getPrototypeOf` によって、対象のコンストラクタを特定すれば、自身が定めたルール内でプロパティチェックを省略出来ます。
|
85
|
+
(例: `new Foo().piyo` は Number 型の自然数が格納されることが保証されている為、チェック不要など)
|
86
|
+
|
87
|
+
※ECMAScript 上で似た性質を持つ関数の仕様を調べてみるといいかもしれません。
|
88
|
+
引数が省略された時の初期値や型の制約など、設計上の考え方で参考になるものがあります。
|
89
|
+
|
90
|
+
### 更新履歴
|
91
|
+
|
92
|
+
- 2017/10/08 15:37 「結局、どうするべきなのか」「プロパティの初期化をどうするのか」を追記
|
93
|
+
|
53
94
|
Re: hayatomo さん
|