prototype.jsを読み解く

第2回 Prototypeライブラリ(198~639行目)

この記事を読むのに必要な時間:およそ 19.5 分

0289:   toQueryParams: function(separator) {
0290:     var match = this.strip().match(/([^?#]*)(#.*)?$/);
0291:     if (!match) return {};
0292: 
0293:     return match[1].split(separator || '&').inject({}, function(hash, pair) {
0294:       if ((pair = pair.split('='))[0]) {
0295:         var key = decodeURIComponent(pair.shift());
0296:         var value = pair.length > 1 ? pair.join('=') : pair[0];
0297:         if (value != undefined) value = decodeURIComponent(value);
0298: 
0299:         if (key in hash) {
0300:           if (hash[key].constructor != Array) hash[key] = [hash[key]];
0301:           hash[key].push(value);
0302:         }
0303:         else hash[key] = value;
0304:       }
0305:       return hash;
0306:     });
0307:   },
0308: 

toQueryParams()は,URIなどで使われるクエリ文字列のようなもの(key1=value1&key2=value2のような形式)を,プロパティと値を持つJavaScriptオブジェクトの形式に変換してくれる関数です。

引数のseparatorを省略すると,293行目の(separator || '&')にあるようにデフォルトで&が使われます。

まず290,291行目で,#文字以降とそれ以前に分けています。thisである文字列に#という文字があるとそれ以降は使われません。291行目が終わった段階で,match[1]に正規表現のグループ化の一つ目が入るので,ここが処理対象となります(match[0]はマッチした文字列全体⁠⁠。

そのmatch[1]に入っている文字列をseparator || '&'でsplit()して配列にします。それに対して{}という空オブジェクトを初期値としてinject()を実行します(inject()はEnumerable.injectです。後述します⁠⁠。

実行されるのが294~305行目のコード部分です。split()された結果がpairとして渡されるので,その文字列を=でさらにsplit()して再度pairに代入します。

そのpair[0]に値があるかどうかをif文で確認しています。ここが偽になるのは,'=value'のように'='文字の左側に文字列が無い場合です。その場合は次のペアに進みます。

295行目の段階でpairは配列で,pair[0]には=の左側,すなわち変数名にあたるもの,pair[1]には値にあたるものが入ります。

toQueryParams()は適切にURIエンコードされていることが前提なので,変数名にあたるものをデコードして変数keyに入れておきます。

値に'='という文字が入ってしまっていると,294行目でsplit()した際に分割された状態で配列になってしまうので,もしpair.shift()後のpair.lengthが1より大きければjoin()する,という形でvalueに単なる文字列として正規化された状態の値を保存します。

さらに,valueがundefined値でなければこちらもURIデコードしておきます。といいつつ,ここでvalueがundefined になる条件が思いつきません。

299行目で,返すhashに既にプロパティが存在しているかどうかを確認します。まだ存在しなければ単純にそのプロパティkeyに値valueを代入するだけですが,すでに存在する場合にはプロパティ値が配列になるように細工をします。300~301行目の処理により,同じ変数名が複数渡ってきた場合には,返されるオブジェクトのプロパティ値には配列が入るようになっています。

最終的に,ハッシュ形式のオブジェクトが返されます。

0309:   toArray: function() {
0310:     return this.split('');
0311:   },
0312: 
0313:   succ: function() {
0314:     return this.slice(0, this.length - 1) +
0315:       String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
0316:   },
0317: 
0318:   times: function(count) {
0319:     var result = '';
0320:     for (var i = 0; i < count; i++) result += this;
0321:     return result;
0322:   },
0323: 

toArray()は,単純にsplit('')を呼び出して,文字列を文字ごとの配列に変換します。

succ()は,Number.succ()と同様に「次の値」を返す関数ですが,Stringの場合は最後の文字の文字コードを一つ増やす,という挙動となります。特に繰り上がりのようなことは考慮していないようです。

たとえば"abc".succ()は"abd"という文字列を返します。

times()は,countという整数を引数に取り,指定された回数だけ文字列を繰り返し,それを結合したものを返します。Enumerable.inject()を使った方がきれいに書けそうですが,ここではfor文を使ったわかりやすい実装になっています。

0324:   camelize: function() {
0325:     var parts = this.split('-'), len = parts.length;
0326:     if (len == 1) return parts[0];
0327: 
0328:     var camelized = this.charAt(0) == '-'
0329:       ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
0330:       : parts[0];
0331: 
0332:     for (var i = 1; i < len; i++)
0333:       camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
0334: 
0335:     return camelized;
0336:   },
0337: 

camelize()は,単語を'-'でつないだ文字列をキャメルケース(camelCaseのような大文字小文字の使い方)に変換します。

まず'-'でsplit()して,326行目で'-'が含まれていなければそのままの文字列を返します。

328~330行目では,先頭が'-'であった場合の例外処理を行っています。通常のキャメルケースは小文字で始まりますが,ここでは'-abc-def'のように '-'で始まっていた文字列の場合には,先頭を大文字にしています。

332~333行目において,配列の残りの部分を先頭だけ大文字にして返り値となる camelized 変数に追加しています。各単語の先頭の文字以外はそのまま結合しているだけなので,"abc-dEf"などとすでに大文字になっているものは特にいじらず,"abcDEf"というようにそのまま返します。

あとはcamelized変数に入っている結果をreturnするだけです。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。