Hatena::ブログ(Diary)

mizchi log

@mizchiの雑記帳

2013-03-19

javascriptのprototype拡張はどこまで許されるか


孫引きなんだけどちょっと気になった点

404 Blog Not Found:javascript - es2pi はじめました http://blog.livedoor.jp/dankogai/archives/51859796.html

本来の JavaScript の利用方法(Prototype 拡張)に立ち返り、Array.prototype, String.prototype, Number.prototype 等を拡張しています 

最近のJSは基本的に肥大化するし、DOMはグローバルな状態であり、またprototypeもどこからでもアクセスできるという点ではグローバルである。
手癖が悪い人はprototype経由で値の受け渡ししはじめる。JSではスコープチェーンなりで値の受け渡しをするように気をつけたい。

経験上、方針を決めずにprototypeを触りまくると遅かれ早かれ破綻する。が、元々プロトタイプ指向な言語だけに、うまく活用すると言語としての自由度がグンと上がる。そのバランスを取らないといけない。


以下にprototype拡張が許されると思しきケースを書く。

1. polyfill系ライブラリ


JS5未満の標準ライブラリは貧弱であり、それを補完する系のもの。ECMA Harmony もしくは Rubyインスパイアのものが多い。
このライブラリを使っている限りは、ライブラリが提供するコードを使うべきで、よくわからないからと自分で定義してしまうと、ロジックが重複して見通しが悪くなる。

2. オブジェクト宣言, prototype継承


JavaScriptでオブジェクト宣言する際は勿論prototype経由で振る舞いを定義することになる。

coffee-scriptで継承を実現する際は、次のようなスニペットが挿入される。

// Generated by CoffeeScript 1.6.1
(function() {
  var List,
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) {
      for (var key in parent) {
        if (__hasProp.call(parent, key)) child[key] = parent[key];
      }
      function ctor() {
        this.constructor = child;
      }
      ctor.prototype = parent.prototype;
      child.prototype = new ctor();
      child.__super__ = parent.prototype; return child;
  };

  List = (function(_super) {

    __extends(List, _super);

    function List() {
      return List.__super__.constructor.apply(this, arguments);
    }

    return List;

  })(Array);
}).call(this);

このコードはArrayを継承したListクラスを作る。
まあ普通のprototype活用だとは思う。

3. 実装(not ライブラリ)


自分がフロントエンドを作る側で、かつ自分自身を継承する人がいない場合、独自の挙動を追加することは許されるとは思っている。
ただ、その場合1つのファイルを指定するなどの明示的なアプローチをとる必要がある。たとえば array_proto.js でArrayの拡張を書き、それ以外ではArray.prototypeは決して触れない、とか。

そしてpolyfill系ライブラリを使っている場合はある程度任せるべきで、かつ上書きしてはいけない。それでいてアプリケーションロジックの実行前に全部読み込んでおくべきだと思う。
プロトタイプはアプリケーション実行前後に同じものであってほしい。コード中で動的にprototypeオブジェクトを変更されるのは怖くて仕方ない。


まあ、便利だけど明示的に、ってのが自分の信条です。

2013-02-16

最近のJSの開発環境について知っておくべきライブラリ10個


ほんとに10個だと思った?(無意味に煽っていくスタイル)

最近JSだけのリポジトリで無益なゲームを大量に作っては破棄しているのだけど、割とストレスなく出来上がってきたので書く。

長々と書くが、要は次のリポジトリに概要が詰まってる。
mizchi/mizchi_client_boilerplate · GitHub https://github.com/mizchi/mizchi_client_boilerplate

前提として、最近はCSJSでもnodeのインストールを前提とする環境が多い。必須といってもいい。

grunt


gruntjs/grunt · GitHub https://github.com/gruntjs/grunt

ビルドタスクを簡単に作れる。make、rake、などに相当するが、node製らしくファイルシステムの監視でアクションを作れるのが特長。$ grunt serverで簡易サーバーも立てられる。

  • coffeescript/typescriptの自動コンパイル、結合、minify
  • sass/compassの自動生成

プラグインが大量にあって追加も簡単。ググればだいたい見つかる。

watchタスクにcoffeeのコンパイル手順を書いて、$ grunt watch server、みたいな使い方するとlocalhost:8000に簡易サーバーを立てつつ裏でファイルシステム監視しつつcoffeeの自動コンパイルしてくれて便利。

bower


twitter/bower · GitHub https://github.com/twitter/bower

Twitter製のクライアントサイドjs依存管理ライブラリ。gemとかnpmだと思ってもらえば問題ない。
component.json みたいなファイルを書いて、 $ bower install すると components/以下に保存される。require.jsに対応している。

{
    "name":  "MyProject",
    "version": "0.0.1",
    "dependencies": {
        "underscore": "latest",
        "jquery": "latest",
        "requirejs": "2.1.1"
    }
}

使いたいライブラリがbowerに登録されていないことが多いが、バージョン名のところにgithubもしくはホスティングされているURLを直接参照すれば解決する。

yeoman


yeoman/yeoman · GitHub https://github.com/yeoman/yeoman

CSJS開発環境の総合フレームワーク。日本の紹介記事だとgruntと対になるような紹介をされていることが多いが、実際の中身はgruntのラッパーライブラリである。

angularやemberを試してみたいときに、おもむろに $ yeoman init angularすると雛形が出来上がって、気軽に試したいときにすごい便利。

紹介はするけど、ぶっちゃけ自分ではあまり使ってない。あまりにもyeoman流儀を強制されて、Railsの吐き出したディレクトリを最初に目にしたような目眩を感じる。僕のようにこの流儀が受け付けない人は、何も考えずに他人のboilerplate眺める用途に使おう。

require.js


RequireJS http://requirejs.org/

最近のJSの依存管理のデファクトなのだが、自分はあまり好きではない。
名前空間を決めて使おう!ってのはモダンなJSだと当たり前のことなんだけど、そんな当たり前の糞な規約を守れない糞JSコーダーを、require.jsという枷に入れてどうにかします、みたいな印象を受ける。

未だに自分のローカルの依存解決は gruntのconcat順で解決していてダサい(しデバッグしづらい)のでなんとかしたいのだけど、require.jsの規約はもっとダサい。ライブラリのインスタンスをコールバックで受け取るっていうのも、そのために無駄にネストを深くするのも、ちょっと嫌だ。

でもまあ、TwitterがFlightやBower等で使うことを強制してきてるし、外部ライブラリ間の依存解決もできるし、たぶんこれから標準になるんだろーなといった印象。TypeScriptもmoduleを使うと内部構造はrequire.js準拠になるオプションがある。

peerjs


PeerJS http://peerjs.com

つい先日リリースされたばかりで、WebRTCでクライアントサイドP2Pを実現する、そのAPIを使いやすくしたライブラリ。おそらくsocket.ioに似せたものなので、使ったことがある人にはわかりやすい。
クライアント間でハンドシェイクするためのstunサーバーもpeerjs.comで提供してくれている。(npm で自分で立てても良い)

今現在ChromeDevでしか動作しないが、自分でサーバー立てずにlocalStorageとp2pだけで「ウェブ」アプリを作ることもできる。そんな未来もある。HTML5でゲーム作ってる人はウェブサーバー管理しなくていいってのはすごい利点だと思ってる。

その他MVCフレームワークについて雑感


Backbone

無難に便利なんだけどCollection, Model部分はその他モジュールと綺麗に切り離されているので、そこだけ使いたいケースが多い。Viewはイベントハンドラとして使うと便利。
Backbone使いはよく言うんだけど、このライブラリはコード量が減る方向には貢献しない。

Angular

あんまりガッツリ使ってないんだけど、DOMツリーが名前空間を表象するのは自分にはちょっと受け入れがたい。抽象度が低く規約が多い。HTMLとデータ構造がそのままビューに反映するアプリには良いとは思う。裏で大量のロジックが走るようなアプリには向いてない。

Ember

Railsの規約をそのまま持ってきすぎで、ルーティング制約とそのモデル対応がきつい。シングルページアプリケーションしか作ってない身だと有り難みがなかった。
おそらくRailsとの親和性が高いのでRails組み込みとして使うと真価を発揮するんじゃないだろうか。まだそんな時代じゃない気もするが…。



疲れたのでこのへんで終了。

そうそう、Railsは4.0で遷移アクションがデフォルトpushStateになるらしいので、つまりはJSオブジェクトを破棄しない。グローバル汚染が厳しくなるので、より慎重なJSを書く必要があるし、遷移時のオブジェクトの再生成コストがなくなるのでJSの仕事が多くなると思われる。

HTML5ゲームエンジン、テストフレームワークについてはまた後日。

2012-11-29

JSのMVCについて考えてみた ~ その2 テンプレートエンジンの分業とパフォーマンス


この前の続き。相変わらず思いつきでつらつら書いてて図とかまともなサンプルとかない。

JSのモデルには二種類ある


フロントエンドである以上本質的にすべてビューだとも言える。
であるがゆえにあやふやにしないほうがいい。

ビューモデル

UIの状態を示す属性。選択しているタブとか、開いているダイアログとか、そういうものの状態をDOMから読むのではなく、JSとして一度確定し、その結果をビューに反映すべきだ。激しく画面を組み替える場合はビューというグローバル変数はどこからも汚染される可能性がある。

データベースのローカルキャッシュ

たとえば、a地点からb地点の距離をユークリッド距離を求めるのに、わざわざサーバーに問い合わせるのは無駄。普通に三平方の定理で計算すればいい。アクション性が高いものほど、ここの振る舞いは分厚くなる。いわゆるHTML5アプリはここを重点的にやるほどサーバーの負担が減り、サーバーレスポンスを必要としないことで、表現的な自由度も高くなる。
基本的な考え方は「サーバーで起こってるであろうことをシミュレーションする」。この場合でも、定期的にサーバーにデータを問い合わせて同期をとる必要がある。
このレイヤーは、nodeで汎用的に書いて同じコードが使えたら嬉しかったりする。

クライアントサイドにおけるテンプレートエンジン


「クライアントのテンプレートエンジンの高速化」は、サーバーサイドにおけるテンプレーティングの速度とは全く別の軸で考える必要がある。サーバーサイドでは純粋に文字列操作だが、クライアントにおけるテンプレートエンジンはDOM生成コストが絡んでくる。Mustacheであろうがhaml.jsであろうが、結局のボトルネックはDOM生成部分になる。ネットワークIOの前では動的言語と性的言語の速度差が吹き飛ぶのと同じだ。

もっとも高速なのは、JSの変数に突っ込んでキャッシュしたDOMを直接操作するパターン。だが、よっぽど注意しないとメンテナンスが不可能なコードになる。経験上、自分以外が書いた、id探してデータを突っ込む以上のDOM操作は、まともに読めた試しがない。jQueryのDOM操作が三行以上続いてると思考停止する人も多いだろう。自分もそうだ。

ビューバインド付きのMVCフレームワーク

KnockoutとかAngularとかのこと。
経験として、開発効率を優先すると大きなビューになり、パフォーマンスを求めるとビュー(テンプレート)が分割されていき管理が難しくなる。これもトレードオフ。たとえば、パラメータ一つだけを書き換えたいのに、それが大きな画面に属していた場合はその全部を再描画する必要があったりするケースがある。関連するモデル粒度だったり、ビューの粒度を適切に分割するのは至難の業で、特にプログラミング初期はとくに見積もれないケースが多い。

誰がテンプレートを書くか?


こんな定義があったとして

//ステータス画面更新規約
var Status = {
  hp: 100,
  mp: 25
}

Statusによって展開されるMustacheテンプレート

<div id='hp'>{{hp}}hp</div>
<div id='mp'>{{mp}}mp</div>

これはJSヘヴィなアプリの場合、JSエンジニアが書くべきか、マークアップが書くべきか。
iterableだったり分岐が激しいテンプレートな場合、マークアップ側に負担が大きくなる。この傾向は、Angularのようなリッチなフレームワークを使うとより顕著になる。
フロントエンドはどうしてもマークアップドリブンになりがちだが、画面設計の際に必要なパラメータの洗い出しはJSを書く人も参加しておいた方がいい。テンプレートを展開するパラメータをマークアップ側が用意できるのが理想だが、ロジックを知らないとすぐに出せるデータ、出せないデータの区別ができず、片方だけでは苦しい。うまくコミュニケーションして連携するしかない。

リスナーイベントはプロシージャルな名前であるべき


ただ、ここで問題になるのはDOMに貼られたリスナーイベントをどう扱うかだ。
id名をRPCと捉えてDOMからは極力情報を受け取らない、という方向性が望ましいと思う。
HTML5のリッチなモデルレイヤーをもったJSは、ほとんどビューモデルとモデルが使えれば現在の状態は確定できるはずだし、またそうではない場合そうなるようにする必要があると思う。

ボタンを押したから、「ここでDOM上の数値が10だから…」とjQueryでDOMを読みに行ってはいけない。簡単なDOMならいいが、DOMが情報を持つとグローバル変数のように扱われててどんどんメンテできなくなっていく。

HTML/DOMはグローバル変数だ。可能な限り状態を持つべきではない。リスナーイベントのid/class名はプロシージャルであるべきだ。

$('#button_add_hp').on 'click', => @status.add_hp()

たとえばこんな風に