javascript
Up one levelJavaScript の for イディオム
var array = [...]; for (var i = 0, len = array.length; i < len; i++) { var elem = array[i]; ... }
は有名ですが、もう少し JavaScript っぽい書き方があります。
var array = [...]; for (var i = 0, elem; elem = array[i]; i++) { ... }
コードが一行へるし若干こちらのほうが高速です。配列の要素に数値などが入る場合は (elem = array[i]) != null という条件にしないとまずいです。
蛇足ですが、
var self = this; var array = $R(0, 100).map(function(a) { return a + self.offset });
って
var array = $R(0, 100).map((function(a) { return a + this.offset }).bind(this));
って書けばいいんですね。気づきませんでした。
- Category(s)
- program
- javascript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/javascript-for-idiom/tbping
Ajax 最適化 Tips - Prototype.js のパフォーマンス
prototype.js 1.6.0 RC で、カスタムイベントが使えるようになったり、ネイティブの forEach が使われるようになったり、簡単に DOM エレメントを構築できるようになったり、 isNumber で数値型かチェックできるようになったり、すばらしいバージョンアップであることは間違いないのですが、 prototype.js のソースを見てもわかるとおり、パフォーマンス云々よりも JavaScript 的にスマートな書き方・より単純で意味の取りやすい書き方が優先されており(もちろんこれは正しい方向)、あまり熟知せずにこれらの関数を乱用してしまうとアプリケーションのパフォーマンスに甚大な被害を与えてしまうことがあります。今回は prototype.js を使うにあたって(パフォーマンス的に)注意すべき点を紹介したいと思います。なお参考にする prototype.js のバージョンは 1.5.1 です。
Element.extend
Element.extend 自体それほど重い処理をやっているわけではないのですが、呼び出し関数が多いため自ずと慢性的なパフォーマンスの悪化をもたらします。しかし、 prototype.js ではエレメントであるようなオブジェクトに対しては即刻 $() を適用して間接的に Element.extend を実行しているので、 prototype.js を使う限りこの問題をさけることはできません(現在アリエルで開発しているプロダクトも Firebug でプロファイルを取ると必ず Element.extend が上位に食い込んでくるのです)。個人的には一番最適化してほしい関数です。ぱっと見るかぎりでは最適化の余地は十分ありそうですし。
Element.extend = function(element) { var F = Prototype.BrowserFeatures; if (!element || !element.tagName || element.nodeType == 3 || element._extended || F.SpecificElementExtensions || element == window) return element; var methods = {}, tagName = element.tagName, cache = Element.extend.cache, T = Element.Methods.ByTag; // extend methods for all tags (Safari doesn't need this) if (!F.ElementExtensions) { Object.extend(methods, Element.Methods), Object.extend(methods, Element.Methods.Simulated); } // extend methods for specific tags if (T[tagName]) Object.extend(methods, T[tagName]); for (var property in methods) { var value = methods[property]; if (typeof value == 'function' && !(property in element)) element[property] = cache.findOrStore(value); } element._extended = Prototype.emptyFunction; return element; };
Array.from
var $A = Array.from = function(iterable) { if (!iterable) return []; if (iterable.toArray) { return iterable.toArray(); } else { var results = []; for (var i = 0, length = iterable.length; i < length; i++) results.push(iterable[i]); return results; } }
Array.from は Enumerable を配列にする関数ですが Array のメソッドを使いたいがためについつい以下のようなことをしてしまいます。
var a = $A(arguments).last();
これは本来
var a = Array.last.apply(arguments);
と書くべきです。一般的に破壊的な操作をしない限り Array.from を使うべきではありません。
var args = $A(arguments); var a = args.shift(); return a.apply(this, args);
Array.without
個人的に prototype.js から一番外してほしい関数が Array.without です(関数型言語的に見れば良い関数なのですが)。
without: function() { var values = $A(arguments); return this.select(function(value) { return !values.include(value); }); },
外してほしい理由は主に Array.without が安易に使われてしまうからです。なので本質的には Array.without が悪いわけではありません。
僕が知らないだけなのかもしれませんが、 JavaScript では配列から特定の要素を削除する単純な手段がありません。
一般的な手段は、
var a = [1, 2, 3]; a.splice(a.indexOf(2), 1);
になるのですが、こんな気持ち悪いコード書けない、ということで Array.without を使ってしまうのです。 prototype.js 1.6.1 RC でもそれらしい関数がないですし、もしかしたら何かちゃんとした方法がすでにあって、ただそれを自分が知らないだけじゃないのかという不安にかられてしまうほど、 Array.remove なる関数が存在しないことに疑問を覚えてしまいます。
Element.update
update: function(element, html) { html = typeof html == 'undefined' ? '' : html.toString(); $(element).innerHTML = html.stripScripts(); setTimeout(function() {html.evalScripts()}, 10); return element; },
エレメントの中身を書きかえようとして Element.update を使うのは好ましくありません。ソースを見てもわかるように、 script タグの削除・その script の評価(しかも setTimeout で)をやるからです。 Element.update は script タグが含まれる可能性がある場合のみに使うようにしてください。それ以外は普通に innerHTML への代入で問題ありません。
Element.getElementsBySelector
規模が大きいのでソースは示しませんが、 document.evaluate が使えない環境( IE6 など)で、巨大なエレメントに Element.getElementsBySelector を実行すると大変なことになります。ほとんどの場合、 document.getElementById と document.getElementsByClassName の組み合わせで書き換え可能です。
Element.up
up: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = element.ancestors(); return expression ? Selector.findElement(ancestors, expression, index) : ancestors[index || 0]; },
引数がない場合は問題ありませんが、引数がある場合は、全ての祖先ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、小規模関数の組み合わせで書き換え可能です。
Element.down
down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); var descendants = element.descendants(); return expression ? Selector.findElement(descendants, expression, index) : descendants[index || 0]; },
引数がない場合は問題ありませんが、引数がある場合は、全ての子孫ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、 document.getElementById と document.getElementsByClassName の組み合わせで書き換え可能です。 Element.up よりこちらのほうが断然遅くなる可能性があるので気を付けてください。
Element.previous & Element.next
previous: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); var previousSiblings = element.previousSiblings(); return expression ? Selector.findElement(previousSiblings, expression, index) : previousSiblings[index || 0]; }, next: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); var nextSiblings = element.nextSiblings(); return expression ? Selector.findElement(nextSiblings, expression, index) : nextSiblings[index || 0]; },
Element.up とほとんど同じ。
余談
以上です。生半可なエントリで申し訳ないです。言うまでもありませんが、無駄な最適化はバグを生むだけです。まずは存分に prototype.js の機能を使って、それからプロファイルを取って適切にボトルネックを取り除きましょう。ただ Element.getElementsBySelector なんかは最初から使うべきでないぐらいの代物ですが。
- Category(s)
- program
- javascript
- ajax
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/ajax-tips-prototype-js/tbping
Ajax 最適化 Tips - getElementById する前に
getElementById で要素を手っ取り早く取得するのはとても良いイディオムですが(コードが短かくなる)、パフォーマンスのことを心配する場合は以下のことを念頭に入れておくと良いかもしれません。
Microsoft の DHTML Collections(*) によると、
(*) http://msdn2.microsoft.com/en-us/library/ms533048.aspx
document 以下の特定の種類の要素をフラットにアクセスできるコレクションという形で提供しています。通常ノードツリーをトラバースするよりコレクションにアクセスするほうが速いので、これを使わない手はありません。特に便利だと思われるのは以下のようなものでしょうか(他にも便利なものがありますが、今回は document に対してのコレクションのみに限定したいので省きました)。
- document.anchors
- document.forms
仮に以下のような HTML があるとします。
<a id="button1">Click me!</a> <form id="form1"> ... </form>
この場合、
document.getElementById('button1').innerHTML = 'Do not click me!'; document.getElementById('form1').onsubmit = function() { alert('Submit!') };
と書くより、
document.anchors['button1'].innerHTML = 'Do not click me!'; document.forms['form1'].onsubmit = function() { alert('Sumit!') };
と書くほうが格段に速いです。ただコレクションのインデックスは整数値あるいは id/name を受けつける寛大な仕様になっているので、もう少し厳密にするなら以下のように書くと良いかもしれません。
function getElementByIdWithHint(id, expectedTagName) { expectedTagName = (expectedTagName || '').toLowerCase(); var collection; if (expectedTagName == 'a') collection = document.anchors; else if (expectedTagName == 'form') collection = document.forms; if (collection) { for (var i = 0, len = collection.length; i < len; i++) { var elem = collection[i]; if (elem.id == id) return elem; } return null; } else return document.getElementById(id); } getElementByIdWithHint('button1', 'a').innerHTML = 'Do not click me!'; getElementByIdWithHint('form1', 'form').onsubmit = function() { alert('Sumit!') };
言うまでもありませんが、 getElementById を使わないにこしたことはありません。ほとんどの場合はうまくキャッシュさせたり、もっとオーダーの低い firstChild なりを使って何とかなったりします。その辺りのこともいずれ話したいと思います。
- Category(s)
- javascript
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/before-getElementById/tbping
JavaScriptで外側のスコープでevalする方法
次のコードを見てください。
var Package = (function() { function foo(t) { eval(t); } function bar() { alert('inner bar'); } return { foo: foo } })(); function bar() { alert('outer bar'); } Package.foo('bar()');
このコードをブラウザで実行するとinner barと表示されます。パッケージを使う側としては外側のbarが呼ばれることを想定しているはずですが、パッケージの内部関数が(不当に)呼び出されてしまっています。JavaScriptのevalがレキシカルスコープを考慮する点を思い出すと、こうなる理由は納得できます。が、外側のbarが正しく呼ばれるために、何らかの方法で外側のスコープでevalできる必要があります。とはいってもどうするのが最良なのかわかりません。とりあえず僕が考えた解決策を一つ示しておきます。
var Package = (function(outerEval) { function foo(t) { outerEval(t); } function bar() { alert('inner bar'); } return { foo: foo } })(function(t) {eval(t)});
パッケージを作成するためのブロックスコープに、外側のスコープでevalする関数を渡すようにしています。これにより正しくinner barと表示されるようになります。IE, Firefox, Safariでは正しく動作することを確認しています。もっといい方法をご存知の方はコメント欄で教えていただけると幸いです。
- Category(s)
- javascript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/javascript-eval-scope/tbping
WebアプリケーションをiPhoneに対応させるための技術メモ
はじめに
iPhone 3G DevWikiという開発情報サイトに基本的な情報がまとまっています。まず最初にこちらに目を通してください。
本エントリでは、新規にiPhone対応のWebアプリケーションを作るというより、既存のWebアプリケーションをいかにしてiPhoneに対応させるかを中心にして話を進めます。
どう対応するか
既存のWebアプリケーションをiPhone対応させるには、大きく分けて二つの方法があります。
- 既存のHTML生成コードを利用する
- スクラッチでiPhone専用のHTML生成コードを書く
(1)の方法では、JSPなどのHTML生成コードをそのまま利用しますが、iPhone Safariからリクエストされた場合にのみ、追加でiPhone専用のCSSをロードします。不必要なヘッダやナビゲーションは、CSSで適宜display:noneを指定することで非表示にします。必要ならHTML生成コードにも手を加えますが、その際、UserAgentを見てiPhone Safariかどうか判別して処理を分岐させます。この方法は実装の工数を最小限にできるというメリットがありますが、生成されるHTMLが巨大な場合や高負荷のJavaScriptが評価される場合は、高い確率でパフォーマンスの問題が持ち上がることになります。
(2)の方法では、新規でiPhone専用のHTML生成コードを書きます。既存のHTML生成コードを汚すことなく対応できるためソースコードの可読性や保守性が向上します。さらに刷新されたスリムなHTMLになるのでパフォーマンスの問題も最小限に抑えられます。言うまでもありませんが、この方法のデメリットは工数の肥大化です。実装の工数が膨らむのは当然ですが、(1)の方法と比べてメンテナンスやテストの工数も膨らみます。というのも、(1)の方法では、同じコードベースを利用しているので、機能追加やバグ修正、テストなどの結果はある程度自動的に反映されます。一方、(2)の方法では、元のコードベースに機能追加やバグ修正が入るたびに、iPhone専用のHTML生成コードも修正しなければなりません。
どちらが優れた選択かは一概には言えません。以下を参考にして選択してください。
(1)を選ぶ理由:
- (ある程度)工数を抑えられる
- 実装
- 機能追加
- バグ修正
- テスト
- すばやい対応が必要
(2)を選ぶ理由:
- パフォーマンス
- 分業
- 既存のHTML生成コードが小さいかシンプル
- よりiPhoneに特化
ビューポートの決定
どう対応するか決定したら、つづいてビューポートについて考えなければなりません。ビューポートとは、iPhone Safariの画面表示に関するメタタグで、画面サイズやズームの可不可を指定します。詳しいビューポートに関する解説は上記のWikiを参考してください。
まず考えなければならないのが、ズームが可能かどうかです。一般的にはズームが必要なユーザーインターフェースは劣っていると言えるのですが、前節で(1)の方法を選択している場合はその限りではありません。PCに最適化されたページというのは何らかの方法でうまく表示しない限りどうしてもはみ出てしまうのです。そうした時に応急処置的にズームを可能にしてしまうのはありかもしれません。
また画面サイズに関しても考える必要があります。ズームが不可能で、普通のiPhoneアプリケーションと同様の使い勝手を目指すのなら、幅320pxぐらいで固定するのがベストです。ただ、ズームが可能な場合や、画像のサムネイルなど巨大なビューポートが必要な場合は、その限りではありません。
どちらにしても初めに決定したビューポートを後から変更するのは大変なので慎重に決定してください。
ページ遷移の方法
PCとiPhoneのブラウザで大きく異なるのは画面サイズです。前節で(1)の方法を選択した場合は特にそうなのですが、表示する内容をうまく縮小したり不要な要素を削ったりしなければ、ほとんどのページはiPhoneで表示するのに最適なサイズには収まりません。そこで1ページを複数のページに分割する方法が有効になりますが、その際、どのようにページ遷移させるかが問題になります。おそらく一般的な方法は以下の二つでしょう。
- 通常のページ遷移(単純リンク)
- JavaScriptによる疑似ページ遷移
(1)の方法では遷移先のページのURLをaタグなどでリンクすることでページ遷移を実現します。この方法は非常に簡単ですが、次のデメリットが伴います。
- HTTPの通信が発生(パフォーマンス問題)
- ページの状態が失われる
(2)の方法では、JavaScriptで要素の位置を変更することにより、擬似的なページ遷移を実現をします。この方法は(1)の方法のデメリットが解消されますが、次の新たなデメリットが伴います。
- ブラウザの「戻る」で戻れない(専用のインターフェースが必要)
- コードの複雑化
UiUIKitというライブラリを使えば、(1)の方法に基づいたiPhone風のページを簡単に作成できます。
UiUIKit: http://code.google.com/p/iphone-universal/
また、iUIというライブラリを使えば、(2)の方法に基づいたiPhone風のページを簡単に作成できます。
iUI: http://code.google.com/p/iui/
ただ、この二つを同時に実現するのは現状では独自に対応するしかないようです(*)。
(*) jQTouchなら可能かも… http://www.jqtouch.com/
例えばWebアプリケーションでよくあるのは、トップページから商品一覧のページに遷移するのにはaタグを使って、商品一覧のページで各商品の詳細を閲覧するのには疑似ページ遷移を使いたい、というケースだと思いますが、現状ではこのようなケースに簡単に対応する方法はないようです。
もし、ちゃんとした疑似ページ遷移(*)を使いたいなら、独自に簡単に実装してしまうのをお勧めします。僕もそうしました。ページのdisplayスタイルを変更するコードとナビゲーションスタックさえ実装すれば、結構簡単にそれっぽく動かすことができます。ちなみにページ遷移をアニメーションさせるのはお勧めしません。JavaScriptでやってもCSS Transformを使っても、現状では滑らかにアニメーションされないからです。カクカクするぐらいならdisplayスタイルで表示/非表示を切り替えるほうが素直で、ユーザーにとっても良いでしょう。
(*) iUIは動的にページ遷移を生成することができません
スクロールの対応
iPhone Safariの仕様を注意深く調べれば分かりますが、iPhone Safariではインナーフレームなどの内部要素を一本指でスクロールすることができません。ここで内部要素といっているのは、ページの大きさに関係なく、要素の内部でスクロールを可能にしている要素です。以下に例を示します。
<div style="height: 100px; overflow: scroll"> <p style="height: 300px"> スクロール可能な長い文章… </p> </div>
一般的なブラウザではこのdiv要素に対してスクロールを発行できますが、iPhoen Safariではdiv要素に対するスクロールとは見做されずページ自体のスクロールと見做されます。一応、このような要素でも、二本指でタッチすることでスクロールすることができますが、ほとんどのユーザーはこの方法を知らないと思ったほうが良いでしょう。
このようなケースには現状では以下のように対応するしかありません。
- heightスタイルやoverflowスタイルを取り除いてページの高さに反映させる
- 疑似ページ遷移を使う
- touchイベントを処理する
可能な限り(1)と(2)の方法で対応してください。どうしても内部要素のスクロールが必要な場合には(3)の方法をとってください。その場合はtouchイベントを処理するJavaScriptコードを自分で書く必要があります。(3)に対応するために僕が作った簡単なライブラリを本エントリに添付しておきます。このライブラリを使えば上記の例も対応できます。
<div id="container" style="height: 100px; overflow: scroll"> <p style="height: 300px"> スクロール可能な長い文章… </p> </div> <script type="text/javascript"> Touch.scrollable(document.getElementById('container')); </script>
その他の問題
基本的に上記したことを考慮しておけば案外簡単にちゃんとしたiPhone対応サイトを作ることができます。CSSやLocal StorageなどのWebKit拡張をフルに活用すればさらに効率的に作ることができるでしょう。
本エントリの締め括りとして、僕がはまったiPhone Safari特有の問題を紹介しておきます。
1. offsetWidth, offsetHeightが取得できない問題 iPhone Safariではdisplay:noneな要素のoffsetWidth, offsetHeightが正しく取得できません。一度、display:blockなどに変更してoffsetWidth, offsetHeightを取得した後に、display:noneに戻すなどの対応が必要ですが、prototype.jsを使っているならElement.getDimensions()がその問題を吸収してくれます。
2. onclickの問題 onclick="return false"という字面があるaタグをクリックすると、なぜかはよくわかりませんが、hrefで指定されたページに遷移してしまいます。この問題に対応するために、aタグのhrefにjavascript:void(0)と書くか、onclick="return false"ではなくonclick="return !!false"と書く必要があります。
ちょっと後味が悪いですが参考になれば幸いです。
- Category(s)
- javascript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/iphone-webapp-memo/tbping
UiUIKitはライセンスに注意
これは GNU Affero GPL (AGPLv3) というライセンスのソフトウェアです。
http://ja.wikipedia.org/wiki/Affero_General_Public_License
このライセンスは、Webアプリケーションのユーザーに対しても
完全なソースコードを提供可能にしておくことを要求するものなので、
ビジネスとして使用する場合は注意が必要でしょう。
Re:JavaScriptで外側のスコープでevalする方法
Re:JavaScriptで外側のスコープでevalする方法
Re:JavaScriptで外側のスコープでevalする方法