携帯からの投稿で失礼します。
配列を代入する変数の初期値は[]にしているのですが、オブジェクトの初期値は、{}にすべきでしょうか?それともnullにすべきでしょうか?
質問の経緯としては、配列の場合、例えば、list.map(item => item.cards)のような処理を行う前に、list.lengthが0ではないことを確認してから実行すれば、list内にcardsプロパティを持ったオブジェクトが存在してる時にのみ実行出来るのでエラーにはなりませんが、オブジェクトの場合は初期値を{}にしてしまうと、obj.cardsが存在してるかどうかを確認してから、obj.cardsに対する処理を行わないといけなくなって、いちいち、cardsというキーがオブジェクトに存在してるかどうかをキー名指定してチェックするのは煩わしく感じてしまいます。ちなみにcardsは配列と仮定しています。
なので、オブジェクトの初期値はnullの方が良いのかなと思ったのですが、どういう考え方が良いのでしょうか?
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答3件
0
ベストアンサー
オブジェクトの初期値
初期値はオブジェクト毎に独立しているので、画一的な定義は存在しないと思います。
例えば、new Object
が「Object
のインスタンスの初期値」であり、new Array
が「Array
のインスタンスの初期値」ですが、定義されるプロパティには違いがあります。
JavaScript
1var obj = new Object, array = new Array; 2 3console.log(Object.keys(obj)); // [] 4console.log(Object.keys(array)); // [] 5console.log(Object.getOwnPropertyNames(obj)); // [] 6console.log(Object.getOwnPropertyNames(array)); // ["length"] 7 8console.log(Object.keys(obj.__proto__)); // [] 9console.log(Object.keys(array.__proto__)); // [] 10console.log(Object.getOwnPropertyNames(obj.__proto__)); // ["constructor", "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", "toString", "valueOf", "__proto__", "toLocaleString"] 11console.log(Object.getOwnPropertyNames(array.__proto__)); // ["length", "constructor", "concat", "pop", "push", "shift", "unshift", "slice", "splice", "includes", "indexOf", "keys", "entries", "forEach", "filter", "map", "every", "some", "reduce", "reduceRight", "toString", "toLocaleString", "join", "reverse", "sort", "lastIndexOf", "copyWithin", "find", "findIndex", "fill"]
ただし、オブジェクトの初期値は Object 型のみであり、それ以外は存在しません。
Null 型
Null 型は「オブジェクトが存在しない状態」を表します。
getElementById
は要素ノードを発見できなかった時に null
を返しますが、その理由は「new Element
の初期値がnull だから」ではなく、オブジェクトが存在しなかったからです。
オブジェクトが存在しないということは「初期化もされない」ということなので、null は new Element の初期値ではないと考える事が出来ます。
document.onclick
プロパティで click
イベントハンドラが未定義の場合に null
で初期化されているのも「オブジェクトが存在しないから」であって、new Function
の初期値が null
ではないと考えることが出来ます。
配列の初期値
前述の通り、new Array
の初期値は []
です。
配列を代入する変数の初期値は[]にしているのですが、
この設計は一つの解ですが、ある関数が配列を返す時、「存在しない事」を表す為に []
を返すべきかどうか、というのは別の問題になります。
String.prototype.match
はマッチした時に配列を返し、マッチしなかった時に null
を返します。
JavaScript
1console.log(Array.isArray('abc'.match(/a/))); // true 2console.log('abc'.match(/d/)); // null
querySelectorAll
や Array.prototype.filter
等では決められた配列形式のオブジェクトを返す設計ですが、String.prototype.match
は g
フラグ等のオプションによって返り値が変化します。
また、「マッチしたか否か」は重要な判断要素になる為、「存在しなかったこと」を明示する為に null
を返す設計にしているものと考えられます。
結局、どうするべきなのか
まず、「オブジェクトが存在しないこと」を表す必要があるのか、を設計段階で考えて下さい。
getElementById
がnull
を返すのは、ドキュメント上に対応するオブジェクトが存在しなかったから、です。「存在しないもの」に対して、new Element を返すのは適切とはいえません。document.onclick
がnull
を返すのは、対応するイベントハンドラ関数(Object型)が存在しなかったから、です。「存在しないもの」に対して、new Function を返すのは適切とはいえません。String.prototype.match
がnull
を返すのは、正規表現に対応する文字列が存在しかったから、です。存在判定によって、後述のコードが処理しやすくなるため、でもありますArray.prototype.filter
が[]
を返すのは、Array.prototype
に存在する関数をメソッドチェーンによって利用する為です。これはString.prototype.match
とは異なる設計であり、宗教上の違いもあります。
Object型が入るべき変数を初期化する事を考えた場合、 「オブジェクトが存在しない」を明示するなら null
を代入し、そうでないなら、new Object
や new Foo
を代入します。
プロパティの初期化をどうするのか
オブジェクトが存在しないのであれば、プロパティも存在しない為、この部分を考える必要はありません。
オブジェクトが存在するのであれば、new Foo
でオブジェクトを初期化するわけですが、プロパティの初期値については設計者の思想に依存しています。
例えば、new Array
の length
値は 0
ですが、これは「length
プロパティが配列の要素数を返す」と設計者が定めているからです。
JavaScript
1new Array().length; // 0
String.prototype.indexOf
が対象の文字列を発見出来なかった時に -1
を返すのは、「存在した時に 0 以上の自然数を返す事から、存在する時の返り値と重複しない値として適当だったから」です。
同じ Number
型の値を持つプロパティでも、存在しない時の初期値が異なる事が分かると思います。
つまり、必要なのは「Object型の初期値とは何か」という視点ではなく、「その変数が持つ論理的な意味は何か」を考え、そこに合理的な理由を付けて初期値を決めることにあると考えます。
そのオブジェクトがただのデータとしての入れ物であれば、下記3パターンのどれかを採用する事が多いですが、
JavaScript
1{}; 2Object.create(null); 3new Map;
各プロパティに何がしかの意味を込め、値にも制約やルールを設ける(「値は~型でなければならない」「プロパティfooが0以上なら、プロパティpiyoは1以上でなければならない」等)のであれば、ユーザ定義のコンストラクタでprototype プロパティに一定のルールを定める事が出来ます。
そして、instanceof
演算子や Object.getPrototypeOf
によって、対象のコンストラクタを特定すれば、自身が定めたルール内でプロパティチェックを省略出来ます。
(例: new Foo().piyo
は Number 型の自然数が格納されることが保証されている為、チェック不要など)
※ECMAScript 上で似た性質を持つ関数の仕様を調べてみるといいかもしれません。
引数が省略された時の初期値や型の制約など、設計上の考え方で参考になるものがあります。
プロパティに一定の制約を設ける
プロパティに一定の制約を設けるコード事例を書くと、次のようになります。
JavaScript
1'use strict'; 2const Person = (() => { 3 const privateMap = new WeakMap; 4 5 return class Person { 6 constructor (name, age) { 7 privateMap.set(this, Object.create(null)); 8 9 this.name = name; 10 this.age = age; 11 } 12 13 get name () { 14 return privateMap.get(this).name; 15 } 16 17 set name (nameString) { 18 return privateMap.get(this).name = String(nameString); 19 } 20 21 get age () { 22 return privateMap.get(this).age; 23 } 24 25 set age (ageNumber) { 26 ageNumber = Number(ageNumber); 27 ageNumber = (Number.isNaN(ageNumber) || ageNumber < 0) ? -1 : Math.floor(ageNumber); 28 29 return privateMap.get(this).age = ageNumber; 30 } 31 } 32})(); 33 34const p1 = new Person('taro', 10); 35console.log(p1.name); // "taro" 36console.log(p1.age); // 10 37 38p1.name = 1; 39console.log(p1.name, typeof p1.name); // "1" "string" 40p1.age = 'fugafuga'; 41console.log(p1.age, typeof p1.age); // -1 "number" 42 43const p2 = new Person; 44console.log(p2.name, typeof p2.name); // "undefined" "string" 45console.log(p2.age, typeof p2.age); // -1 "number"
Person
には次のルールが設けられています。
name
プロパティは必ず String 型である (MUST)age
プロパティは -1 以上の整数である (MUST)age
プロパティは有効な年齢を設定された場合に 0 以上の整数を返す (MUST)age
プロパティは無効な年齢を設定された場合に -1 を返す (MUST)
このルールは絶対的であり、引数が省略されても、後でプロパティが再設定されても必ず、そうなります。
ですので、プロパティの有無を確認する必要がなければ、プロパティに格納された値の型を確認する必要もないのです。
面倒な処理は全て、Person クラスがやってくれます。
(補足)
Numberは0かnanですか。型によって初期値がどうなるという視点は良くないということでしたが、この辺りは気になるところです。
age
プロパティは Number 型ですが、年齢として有効な数値は「0 以上の整数」になります。
従って、無効値が設定された場合に「falsy な 0 を返す」という実装は出来ません。
age
プロパティは「整数値を返すプロパティ」なので、String#indexOf
に倣い、-1 を返すルールとしました。
このように、考え方次第では、Number 型でも falsy な 0, NaN 以外を返すルールにする場合があります。
例に挙げて頂いた多くは初期値の話というより、関数の戻り値についてだった気がしますが、
はい。その通りですが、プロパティの getter/setter に一定のルールを設けるという意味では関数の返り値でも考え方は変わらないと思います。
例えば、get age ()
は function getAge () {}
に置き換える事が出来ます。
更新履歴
- 2017/10/08 15:37 「結局、どうするべきなのか」「プロパティの初期化をどうするのか」を追記
- 2017/10/18 23:38 「プロパティに一定の制約を設ける」を追記
Re: hayatomo さん
投稿2017/10/08 04:54
編集2017/10/18 14:28総合スコア18156
0
私が(素の)Objectを使う場合はどんなプロパティ(キー)が入るかが決まっていることを前提にしています。ですので、それぞれのプロパティの初期値を入れますが、undefined
が初期値の場合は省略する場合があります。
JavaScript
1const obj = { 2 name: '名無し' 3}; 4 5obj.name = '山田太郎'; 6obj.age = 20;
TypeScriptやFlowを使っていれば、型アノテーションでエラーチェックもできます。(下記はFlowでの例)
JavaScript
1// @flow 2const obj /*: { 3 name: string, 4 age: ?number 5} */ = { 6 name: '名無し', 7 age: undefined 8}; 9 10obj.name = '山田太郎'; 11obj.age = 20;
指定のプロパティ名が存在確認は、そのプロパティ値がnullまたはundefinedでないかで判断します。
JavaScript
1if (obj.age != null) { 2 console.log(`${obj.name}は${obj.age}才です。`); 3} else { 4 console.log(`${obj.name}は年齢不詳です。`); 5} 6
なお、不定なキーを扱う場合はObjectではなくMapを使用します。
投稿2017/10/08 02:55
総合スコア21733
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/10/08 03:28
退会済みユーザー
2017/10/08 03:40
0
オブジェクトの初期値をnullとした場合、null参照の可能性があるのであれば、オブジェクトがnullかどうかを確認してから使用する必要があると思いますので手間は同じなのではないかと思います。
JSON等で取得する場合も含め、オブジェクトを利用する場合はある程度決まったデータ構造があるから使用しているものと思います。
そのため、必要なプロパティを定義したオブジェクトを作成し、返却するような関数を用意して初期値として使用するという対応も考えられます。
ES6ではクラスが使用可能となりましたので、ES6が利用可能であれば、このような場合はクラスとして定義して利用されるとよいのではないでしょうか。
投稿2017/10/08 02:22
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/08 02:41
退会済みユーザー
2017/10/08 02:45
退会済みユーザー
2017/10/08 03:15
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/08 05:01
2017/10/08 07:01
退会済みユーザー
2017/10/08 19:29
退会済みユーザー
2017/10/08 19:31
退会済みユーザー
2017/10/08 19:34 編集
退会済みユーザー
2017/10/08 19:36
2017/10/11 02:46
退会済みユーザー
2017/10/11 03:56
2017/10/18 14:29