※早速ですが追記あります。詳細は最後で。
FuelPHPのArrクラスが便利すぎた
ので作りました。PHPがわからなくてもJSわかれば大丈夫、きっと。
この投稿について
JavaScriptにおける階層が深くなったオブジェクトのプロパティの存在チェックができるようになります。例えば下記のようなことができます。
var obj = {a: {b: {c : {d : {}}}}};
isDefined(obj, 'a.b.c.d'); // true
isDefined(obj, 'a.b.c.d.e'); // false
※FuelPHPのArrクラスを参考に(というか丸パクリ)しています。
階層が深いオブジェクト相手に手こずらないように
階層が深いオブジェクトのプロパティの有無のチェックが面倒になった今日このごろ。そもそもそんなことになる使い方するなという話ですが。
下記のようなレベルならいいですが、仮にもう少し深くなるともう面倒ですし、まずコードがひどいことに…。
var obj = {};
// これはOK
var a = obj.asdf || {};
// これはNG!!
var b = obj.asdf.qwer || {}; // TypeError: Cannot read property 'qwer' of undefined
というわけで、階層が深いオブジェクトのプロパティが定義されているかどうかを簡単にチェックしたいなぁ、と思い立ったのがきっかけで考えていたところ
FuelPHPの \Arr::get()
だ!すばらしい!!(錯乱
という考えに至ったわけです。案件で使っていたのが幸いしました。
関数のソースコード
変数が定義されているかどうかも判定できますが、おまけ程度のものです。
というのも、未定義のものは判定可能ですが、そもそも宣言していない変数の確認をしようとした場合、変数を参照しようとした時点で ReferenceError が出てしまいます。
PHPの isset()
のようにはいきません。残念。
/**
* 変数、オブジェクトのプロパティの存在チェック
* 第一引数がnullの場合はfalseを返却する。(PHPのisset()に倣う)
*
* 第二引数にオブジェクトのキーを指定することで、
* オブジェクト内部の値の存在チェックが可能。
* キーは'.'区切りで深い階層にアクセス可能。
*
* @param {mixed} variable 対象の変数
* @param {string} key オブジェクトのキー
* @return {boolean} 定義済みかどうか
*/
function isDefined(variable, key) {
if (variable === null) {
return false;
}
// keyが指定されていないか、variableが配列かオブジェクトでなければここで判定 ※注1
if (typeof key === 'undefined' ||
typeof variable === 'undefined' ||
(Object.prototype.toString.apply(variable) !== '[object Object]' &&
Object.prototype.toString.apply(variable) !== '[object Array]')
) {
return typeof variable !== 'undefined';
}
// keyがstring か numberでなければエラー ※注2
if (typeof key === 'string' ||
typeof key === 'number' ||
Object.prototype.toString.apply(key) === '[object String]' ||
Object.prototype.toString.apply(key) === '[object Number]'
) {
// 階層の中を判定していく
var obj = variable,
keyArr = (key).toString().split('.'), // ※注3
len = keyArr.length; // ※注4
for (var i = 0; i < len; i++) {
obj = obj[keyArr[i]];
if (typeof obj === 'undefined') {
return false;
}
}
return true;
}
throw new TypeError('type of "key" is invalid.');
}
注釈について(ほぼ余談)
if文の判定で
typeof variable === 'undefined'
としていますが、古いIE向けの対応です。古いIEでは null と undefined が Object扱いで、これがないとifブロック内に入らずに先の処理へ進んでしまいます(Object.prototype.toString.apply
の結果が[object Object]
で返ってくる)。この仕様(バグ?)はIE9以降で修正されたようです。typeof
とObject.prototype.toString.apply
どちらも行なっています。
あまりないことかと思いますが、ラッパークラスで引数に渡してくる場合が考えられた為です。Chromeで1000万回ほど判定処理をループさせたら前者のほうが5秒ほど早かったとかそういう些細なレベルの理由で後者は後回しにしています。(key).toString().split('.')
も「1.」と同様にラッパークラスの考慮です。
数値が渡ってきた場合にkey.split('.')
ではエラーになってしまうのでtoString()
で強制的に文字列化しています。new String(key).split('.')
でもいいかなと思いつつ、Chromeで1000万回ほど判定処理をループさせたら(key).toString().split('.')
のほうが3秒ほど早かったとかそういう些細なレベルの理由でnew String(key).split('.')
は見送っています。メモリ等のコストの差までは見ていません...。プロパティ値をキャッシュしています。
この方が早いと見たことがあったような気がしたので一応。ちなみにChromeで長さ1000万の配列を単純にfor文で回すと1秒ほど早かったとかそういう些細なレベr(ryあ、1000万円は欲しいです。
使い方そのいち
// Alias
var log = function (arg) { console.log(arg); };
// 未宣言の変数はダメ
log(isDefined(a)); // ReferenceError: a is not defined
// 第二引数が文字列か数値(オブジェクトのプロパティ名に使用できるもの)でなければエラー
log(isDefined({}, {})); // TypeError: type of "key" is invalid.
// 未定義と null は false を返却
var a, b = null;
log(isDefined(a)); // false
log(isDefined(a, 'asdf')); // false
log(isDefined(b)); // false
log(isDefined(b, 'asdf')); // false
// 空のオブジェクトと配列はキーを指定しなければtrue, キーを指定すれば対応するものがないので当然false
var c = {};
log(isDefined(c)); // true
log(isDefined(c, 'keyC')); // false
var d = [];
log(isDefined(d)); // true
log(isDefined(d, 0)); // false
// 第一引数がオブジェクトか配列でなければ第二引数は意味なし
var e = 0;
log(isDefined(e)); // true
log(isDefined(e, 'asdf')); // true
// Object以外のオブジェクト(おまえは何を言っているんだ)も第二引数は意味なし
var f = new Number(), g = new String(); // ※注1
log(isDefined(f)); // true
log(isDefined(f, 'asdf')); // true
log(isDefined(g)); // true
log(isDefined(g, 'asdf')); // true
注釈について
- JSHintで怒られました...まぁわざわざこういう形式で使う人はいませんね...
Do not use Number as a constructor.
Do not use String as a constructor.
使い方そのに(むしろこちらが肝心)
オブジェクトの深い階層にアクセスしてみます。
// Alias
var log = function (arg) { console.log(arg); };
var obj = {
prop1 : 'asdf',
prop2 : {
prop2_1 : 'qwer',
prop2_2 : {
prop2_2_1 : 'zxcv'
}
},
prop3 : [0, 'tyui', true, {prop3_arr3_1 : 'ghjk'}]
};
// 階層1
log(isDefined(obj, 'prop1')); // true
log(isDefined(obj, 'prop2')); // true
log(isDefined(obj, 'prop3')); // true
log(isDefined(obj, 'prop4')); // false
// 階層2
log(isDefined(obj, 'prop2.prop2_1')); // true
log(isDefined(obj, 'prop2.prop2_2')); // true
log(isDefined(obj, 'prop2.prop2_3')); // false
// 階層3
log(isDefined(obj, 'prop2.prop2_2.prop2_2_1')); // true
log(isDefined(obj, 'prop2.prop2_2.prop2_2_2')); // false
// 配列を含んだ場合
log(isDefined(obj, 'prop3.0')); // true
log(isDefined(obj, 'prop3.1')); // true
log(isDefined(obj, 'prop3.3.prop3_arr3_1')); // true
log(isDefined(obj, 'prop3.3.prop3_arr3_2')); // false
こ、このくらいでいいですか(震
弱点
- 先述の通り、未宣言の変数は言語仕様上判定できません。関数の前後にtry-catchを書くとかで回避できますが…ちょっと考えられません。
- オブジェクトのプロパティ名にドット(" . ")が使われていると事故ります。
(String.split()を使用している為) - 第二引数が数値かつ少数の場合に「2.」と同様の理由で事故ります。
- 僕みたいなへっぽこが書いたので他にもあるかもしれません
上記弱点は各々楽しく解決して、あわよくば共有してもらえると、それはとってもうれしいなって。
おまけ(ひとりごと)
挑戦は続く(訳:後は頼んだ!)
上記コードを使用すれば、 \Arr::get()
の挙動そのままのものを作れるような気がします。PHPわかる方はFuelPHPのソースを覗いてみて、JS化してみるのもいいのではないでしょうか。したらください。
言語の勉強が捗った!
言語の垣根を越えて同じ挙動を再現させてみるというのはできるかできないか含めて結構勉強になります。オススメです。言語自体が初めてだとありえないレベルでググりますが。
最後に
バグとかあったら教えていただけると嬉しいです。
仮に、ありがたいことにもここからコピペして使った方がいらっしゃったとき、ちゃんとここのことを覚えていてくれたらいつかバグに遭ってもなんとかなるかもしれません。ならないかもしれません。
また、FuelPHPやPHPのタグはこの投稿にはつけていません。
PHPに関する投稿とはちょっと違うかなと思いましたので。
以上です。ここまでお読みくださり、ありがとうございました!
追記
やはりというかなんというか、同じことを皆様お考えになるようで、あっさりと関連投稿に出てきていたたまれない感じになりました。2秒くらい。
1. JavaScriptのオブジェクトにドット記法でアクセスする(@keroxp様)
2. .(ドット)つなぎのキー文字列でオブジェクトの値に"かっこよく"アクセスする(@alucky0707様)
ですが、一端のへっぽことしてこのまま引き下がるのもアレなので、 \Arr::get()
をJavaScriptで実装してみました。比べていただければわかりますが、びっくりするくらいそのままです。FuelPHPの中の人に申し訳が立たないレベルでそのままです。
FuelPHPのArr::get()をJavaScriptで実装してみた
参考(リファレンスなど)
JavaScript .(ドット)つなぎのキー文字列でオブジェクトの値にアクセスする : アシアルブログ
PHP: 配列 - Manual
PHP: isset - Manual
Arr - クラス - FuelPHP ドキュメント
※今回の内容は \Arr::get()
のソースから拝借しています。