Blog, The

1 月 22 日生まれ、ブログ座です。

昔の自分を反面教師として学ぶ JavaScript

ふと昔自分が書いたコードを眺めてました。
あるあるだと思うんですが、「ひどいなー」とか「今ならこうするなー」とか思ったりするわけです。

今日はそんなひどいコードたちを、自分への戒めという意味も込めて、正していこうと思います。どれも基本的なことばかりですが。

ちなみに、ここで挙げている昔のコードは、いずれもバグ予備軍的なコードだったり、ちょっとイケてなかったりするコードです。
そのコードが動く環境では期待通り動いてます。

本当にバグを見つけちゃったら、こんな感じでは書けませんね。


for 文の継続条件式におけるプロパティ参照

昔のコード:

for (var i = 0; i < items.length; i++) {
  // ...
}

今ならこう書く:

for (var i = 0, length = items.length; i < length; i++) {
  // ...
}

JavaScript に限らず基本ですね。
ループの度に items.length が評価されちゃうのでパフォーマンス的に不利です。

初期化式で length を定義して items.length への参照が一度で済むようにします。


undefined の判定

昔のコード:

obj === undefined

今ならこう書く:

// 1
(function(undefined) {
  obj === undefined;
})();

// 2
typeof obj == 'undefined'

// 3
obj === void 0

昔のコードでも、多くの場合、期待通り動きます。

ただ、undefined は予約語ではなく、グローバルオブジェクト (ブラウザなら window) のプロパティなので、他の値で書き換え可能です。
(最近はできなくなりつつある?)

ですので、素直に undefined と比較するのではなく、以下の方法をとるのがベターです。

1. 引数の省略を利用して確実に undefined を取得する
2. typeof 演算子を使う
3. void 演算子を使う


null か undefined の判定

昔のコード:

typeof obj === 'undefined' || obj === null

今ならこう書く:

obj == null

ガード節としてよく用いるものですね。昔のコードでも問題はないと思います。

ただ、obj == null で null か undefined であるかを判定出来るので、今はそちらを使うようにしてます。
「== は使うな」的な風潮がある気がしますが、null との比較はイディオム的に使っていいんじゃないかなーと思います。


Array の判定

昔のコード:

obj instanceof Array

今ならこう書く:

Object.prototype.toString.call(obj) === '[object Array]'

挙動をきちんと理解した上で使うのであれば、昔のコードでも問題ないと思います。

両者の違いとして、instanceof はプロトタイプチェーンを辿るので以下のような場合でも true となってしまいます。

var Foo = function() {};
Foo.prototype = [];
var foo = new Foo();

console.log(foo instanceof Array); //=> true

Object.prototype.toString を利用すれば、組み込みの Array オブジェクトの場合のみ true となります。
これは「今ならこう書く」っていうか使い分けでしょうか。


ラッパーオブジェクト

昔のコード:

var s = new String('foo');

今ならこう書く:

var s = 'foo';

JavaScript にはプリミティブな型と、それに対応するラッパーオブジェクトが存在します。(Number, Boolean, String)
昔のコードではラッパーオブジェクト、今のコードではプリミティブ型を利用しています。

それぞれどう違うのかは割愛しますが、JavaScript において明示的にラッパーオブジェクトが必要な場面ってのは、ほとんど無い気がします。
「typeof で 'object' が返ってきて・・・」とか、いらぬバグを生まない為にも、極力プリミティブ型で統一するようにしましょう。

多分当時は「new を付けた方がオブジェクト指向的でかっこいい」って感じで、意味も分からず使ってたんだと思います。


Array コンストラクタ

昔のコード:

var items = new Array();

今ならこう書く:

var items = [];

いずれも空の Array を生成してます。
Array コンストラクタを使うか、配列リテラルを使うかの問題ですね。

Array コンストラクタは引数が一つで整数の場合、引数の数だけ undefined で初期化した Array が生成されます。
一方、配列リテラルの場合、常にリテラルに渡した値で Array が生成されます。

new Array('foo', 'bar', 'baz'); //=> ['foo', 'bar', 'baz']
['foo', 'bar', 'baz'];          //=> ['foo', 'bar', 'baz']

new Array(3); //=> [undefined, undefined, undefined]
[3];          //=> [3]

思わぬバグを生まない為にも、今は一律 Array リテラルを使うようにしてます。


arguments の扱い

昔のコード:

function foo() {
  var args = [];
  for (var i = 0; i < arguments.length; i++) {
    args.push(arguments[i]);
  }
  // ...
}

今ならこう書く:

function foo() {
  var args = Array.prototype.slice.call(arguments);
  // ...
}

関数の実引数が格納される arguments は「Array のようなオブジェクト」であって Array ではありません。
当時もそのことは知っていたようですが、それを Array に変換するコードが冗長です。

Array.prototype.slice を使えば、「Array のようなオブジェクト」を本物の Array に変換することができます。
同メソッドは意図して汎用的に作られていて、「Array のようなオブジェクト (length を持っててインデックスで要素にアクセスできる)」に対しても呼び出せるようになってます。

Arguments の他にも HTMLCollection や NodeList なんかも変換可能です。


グローバル汚染

昔のコード:

// ここはトップレベル

var foo = 'foo';

function hoge() {
  // ...
}

// ...

今ならこう書く:

(function() {
  var foo = 'foo';

  function hoge() {
    // ...
  }

  // ...
})();

ダメ。ゼッタイ
name や parent など、案外あっさりと window のプロパティ競合しちゃったりするものです。

即時関数を利用してスコープを作りましょう。


プロトタイプ汚染

昔のコード:

Object.prototype.clone = function() {
  // ...
};

今ならこう書く:

function clone(obj) {
  // ...
}

JavaScript ではプロトタイプを拡張することで、組み込みのオブジェクトにメソッドやプロパティを追加できます。

でも、for in で列挙されるようになったりとか、同名のメソッドの間で実装に差異が出たりとか、そこにはいろいろ弊害があります。
もしかしたら ECMAScript 標準で同名のメソッドが定義される可能性もあるわけです。

拡張したいオブジェクトを引数とする、underscore.js 的なアプローチを取りましょう。

Object にメソッドが追加できると知って、ドヤ顔で書いていた当時の自分が目に浮かびます・・・。


その他のひどいコード

Array の判定?
if ('push' in obj && 'pop' in obj) {
  for (var i = 0; i < obj.length; i++) {
    // ...
  }
}

push, pop っていうプロパティを持ってるかどうかで配列であるかを判定しています。
ECMAScript 標準のオブジェクトに限れば、一応これでも判定できます。多分。

このコードのだめなところは、push, pop っていうインターフェースを期待しておきながら、
スタック的な使い方を一切せず、普通に length を使って for ループしてることです。

ダックタイピングの真似事だったんだと思うんですが、全然出来てませんね。


エイリアス
// ここはトップレベル

var Foo = {
  hoge: function() {
    // ...
  },
  fuga: this.hoge
}

これは本当にひどい。

Foo.fuga を Foo.hoge のエイリアスにしたかったんだと思います。
でもこのコードだと this.hoge は window.hoge を参照してしまいますので、意図した挙動はとりません。

この例でいう fuga を使ってなかったようなので、エラーにはならかったようですが・・・使わないならこんな事しちゃいけませんね。

当時の自分の意図を汲んでみると、こんな感じでしょうか。

var Foo = {
  hoge: function() {
    // ...
  }
}
Foo.fuga = Foo.hoge;


振り返って

図らずも雑多な JavaScript Tips みたいになっちゃいました。

JavaScript って、こういうちょっとした落とし穴みたいなのが本当に多いですよね。
なぜ Coffee Script 等が話題になってるのかがよくわかります。

でも僕は好きですよ。JavaScript のこういうところ。

デブサミ関西 2012 に行ってきた

Developers Summit 2012 Kansai に行ってきました。
メモも兼ねて感想を書いておこうと思います。


Chromeのプロジェクトに学ぶAgileでScaleするソフトウェア開発手法

Google 及川さんより。

Chrome における開発手法のお話。
従来のやり方との対比、ブレないことの重要性、徹底した自動化、各人の役割やフロー、などの話がなされました。

ブレないことって難しいですよね。

Chrome の開発に携わるほどの人だと、各々すごい経験をしてきて、その中で得た自分なりのやり方や考え方ってのがあると思うんです。
そんな状況で、みんながブレずに同じ方向を向くっていうのは、すごく難しいことなんだろうなと、すごく遠い人間なりに思ったりしました。


JavaScript 最新事情 — 開発者なら知っておきたい次世代 JavaScript

Mozilla 浅井さんより。

ECMAScript 5 はほぼ使える状態になっててこんな機能があるよっていう話と、ECMAScript 6 ではこんな機能が導入されるよっていうお話。

ECMAScript 6 の話は大変興味深かったです。正直全然知らなかったです。
「ネイティブの Map はありがたいなー」とか「Direct Proxies すげー」とか、ひっそりとテンションが上がってました。
時間の関係で後半は飛ばし気味でしたが、かえって後で調べてみようという思いが強くなりましたねw

あと typeof null == "object" はもともとバグだったって話が何気に衝撃でしたね。有名な話なんでしょうか?


ヤフーのHTML5対応

Yahoo 是井さんより。

Yahoo における HTML5 に対する取り組みについて。
モバイルを中心として、使えるところから使ってるって話や、会社として HTML5 を使うために、ガイドラインやツールを導入してるというお話でした。

ルール付けの必要性はすごく感じました。
Web の表現力が向上するというところで、いい面ばかりがクローズアップされがちですが、扱える情報 (ローカルストレージなど) が増え、リスクも確実に大きくなりますもんね。

あと、デブサミ直前の あの発言 を引用して、確かにパフォーマンスには課題があるというお話をされていました。


ブラウザテストを自動化することにした ~TestNGSeleniumでやってみる~

フリュー 阪田さんより。

Selenium WebDriver、Selenium Grid、TestNG の機能や使い方の紹介。
それらを使って可能な限りブラウザテストを自動化していきましょうというお話。

まず TestNGJava のテスティングフレームワークだって事を知りませんでした。
タイトルにある言葉ぐらいは予習しとけと反省・・・。

自動化されてると気持ちがいいですね。
Selenium はよく使うんですが、最初は無駄に何回もテストを流したりしてましたw


デスクトップアプリ開発者が押さえておきたいWindows 8 時代の変革

GrapeCity 八巻さんより。

Windows 8 時代では、タブレットとのハイブリッド型 PC、高精細ディスプレイを搭載した PC が主流となることが予想され、
デスクトップアプリ (Metro じゃなくて) でもそれらの事を意識して開発しないといけないよっていう話。
で、実際それらにどのように対応していくか、いくつかのパターンが紹介されていました。

非常に丁寧で聞きやすいセッションでした。

本題ではないですが、冒頭の Windows 8 の互換性の話が印象に残ってます。
Windows 7 で動作していたものがすべて動く」という方針、VB6 が動く (!)、デザインが変われど大きさは 1px も変わってない等、そこにはいつもの Windows があるようで安心しました。


関西に所縁のあるITコミュニティ集まれ!デブサミ名物コミュニティLT大会

関西の IT コミュニティ (勉強会) による LT 大会。
10 以上の勉強会から、活動内容の紹介を主とした LT がなされました。

今まで正直、勉強会に対してあまり積極的では無かったんです。
人見知りなんですね。仲良さそうにしてるところに入れず、ぼっちな感じになった事もあったりして。

でも、楽しそうに話をする方々を見て、そんな下らない理由で二の足を踏んでる自分が馬鹿らしくなりました。
共感出来る人たちが集まって、何かするってのは素晴らしい事ですね。羨ましいです。

内容ももちろん面白かったんですが、「勉強会へ行こう」そういった意味で刺激を受けたセッションでした。


全体の感想

慢心をいい意味で打ち砕いてくれたというか、そんな思いです。
純粋に勉強になりましたし、いい刺激になりました。

スピーカー、スタッフの皆様、ありがとうございました!

Emacs のフォントサイズを変える

使いたい時にいつも忘れてるのでメモ。
いつから使えるのかは分からないけど、とりあえず Emacs 23 では使える。

C-x C-+ or C-x C-=
大きく
C-x C--
小さく
C-x C-0
デフォルトのサイズへ

一回打てば +, =, -, 0 の連打で調節出来る。
M-x text-scale-adjust でも OK。

最初「= で大きくなるって何なん?」って思ってたけど、英語配列のキーボード使って納得した。

じんわりとテキストを表示するだけの Web サービスを作った

Web サービスを作りました。こんなのを Web サービスって言ってしまっていいのか分からないけど。

The Text Show

URL で渡されたテキストをじんわり表示する・・・だけです。
なんかワンクッション置いて伝えたい事があるときに、URL を渡して見てもらうっていう感じで使ってください。

たとえばこんな感じです。

http://the-text-show.com/%E3%81%82%E3%82%8A%E3%81%8C%E3%81%A8%E3%81%86

英字だと URL をそのまま渡すとバレバレなので、goo.gl とか bitly とかで URL を短縮してから渡すといいと思います。

deck.js とうまくやっていくための gem を作った

deck.js は便利です。

HTML でスライドを書くツールは色々あるけど、デフォルトのテーマでも見栄えが良かったり、プログラマブルなところが気に入ってます。

ただその HTML っていうのが結構大変で・・・

(deck.js に付いてるテンプレート)
deck.js/boilerplate.html at master · imakewebthings/deck.js · GitHub

JS や CSS を読み込んだり、HTML (Snippet) をコピペしたりと、スライド意外の部分でやる事が多いです。そもそも HTML でがっつりスライドを書くってのもしんどいですね。

そんなこともあって、「slim とか軽量なテンプレート言語で書けたらなー」とか「おいしいとこだけ書けたらなー」とか思いながら Ruby でスクリプト作ってたら、「これ意外と有用なんじゃね?」ってレベルになったので gem として公開してみる事にしました。

荒削りかつ不親切な部分もありますが、よければ使ってやってください。

ソースは以下から。

tkmsaoi/deckr · GitHub

できること

  • deck.js に特化した Sinatra アプリケーションの生成
  • スライド作成支援
    • テーマやら Extension やらの面倒を見てくれます
    • お気に入りのテンプレート言語が使えます

インストール

Ruby が入ってれば gem でインストールできます。
後で Bundler も使うので、入ってなければインストールしておいてください。

$ gem install deckr
$ gem install bundler # 入ってなければ

Sinatra アプリケーションの作成

適当なディレクトリで以下のコマンドを実行します。(presentations は任意の名前でOK)

$ deckr new presentations

実行すると、指定したディレクトリ以下に Sinatra アプリケーションが作られます。

presentations
├── Gemfile
├── config.ru
├── deck
│   └── ...
└── views
    ├── index.slim
    └── layout.slim

deck には deck.js のファイル群、views にはスライドのテンプレートが入ってます。Gemfile や config.ru はおなじみのファイルですね。

加えて以下のコマンドを実行すれば Web サーバーが起動します。

$ cd presentations
$ bundle install --path vendor/bundle
$ bundle exec rackup

ブラウザで http://localhost:9292/ にアクセスすれば、サンプル的なやつが表示されると思います。

テンプレートの書き方

以下はデフォルトの views/index.slim の内容です。
deck っていうヘルパーが使える事を除いては普通のテンプレートです。

- @title = "Your deck.js Presentation"
- deck.style "web-2.0"
- deck.transition "horizontal-slide"
- deck.enable "goto", "menu", "navigation", "status", "hash", "scale"

.slide
  h1 Slide

.slide
  h1 Content

.slide
  h1 Here

最初の 4 行ではスライドを設定しています。こう書いとけば views/layout.slim の方でよろしくやってくれるようになってます。

1 行目は単純に変数を設定してるだけですね。@title が設定されていれば title タグに出力されるようになってます。

2~3 行目の deck.style, deck.transition でテーマを指定しています。それぞれ deck/themes/style, deck/themes/transition 以下のファイルに対応します。

4 行目の deck.enable では Extension を有効にしています。deck/extensions 以下のディレクトリに対応します。

それ以降にはスライドの内容を書いていきます。
.slide (<div class="slide">...</div>) の内容がスライド 1 ページに対応します。
たとえばアジェンダだとこんな感じでしょうか。

.slide
  h2 Agenda
  ol
    li foo
    li bar
    li baz

テンプレートの追加とか

テンプレートを追加する時は、ちょっと面倒ですが config.ru にルートを追加してください。
たとえば views/foo.slim を追加する場合、以下のような感じです。

get "/foo" do
  slim :foo
end

また、以下のようにすれば slim 以外のテンプレート言語も利用出来ます。

get "/bar" do
  erb :bar, :layout_engine => :slim
end

やるかわからない TODO

  • 静的 HTML ファイルのビルド
  • Extension のビルド (Coffee Script とか SCSS とかをコンパイル)
  • deck.js を github から落としてくるようにする (今は gem に添付してるやつだけ)

IE の console.log がよくわからない

きっかけ

デバッグ用に console をラップしたオブジェクトを作ってて、Sinon.js を使ってテストをしてたら、なんか IE でエラーが出る。

調べてみると Sinon.js 内にこんなコードがあって・・・

function isFunction(obj) {
  return !!(obj && obj.constructor && obj.call && obj.apply);
}

これが console.log に対して false を返してるのが原因らしい。
true を返しそうなもんだけど・・・


どうなってんの?

どうも納得がいかなかったので、IE8 と IE9 のコンソールで色々試してみた。

  • typeof console.log は "object" を返す
  • Object.prototype.toString.call(console.log) は "[object Object]" を返す
  • 関数じゃないらしいので、apply や call は未定義
  • でも、コンソールで console.log を評価すると "function log() { [native code] }" って言ってる
  • ていうか、 toString や hasOwnProperty なんかも未定義
  • console.log.foo = "bar" としたら "オブジェクトでサポートされていないプロパティまたはメソッドです。" って怒られた


なるほど。詳しいことはよく分からんが特別な存在と言うことですね。

なんというか、自分が JavaScript という中で積み上げてきたものを、ことごとく打ち砕かれた。そんな思いです。


どうしよう?

冒頭のコードで false を返してた理由はわかりましたが、どうしたものか・・・

true を返すようにプロパティを追加するという、場当たり的な対応もできませんし、テスト対象のコードも console.log.apply(console, [...]) てな感じなので、このままでは動きません。

なんとかしたい。
調べてたら、なんとかなりそうなのが見つかりました。


javascript - console.log.apply not working in IE9 - Stack Overflow

一番 vote されてる回答の中で、解決策らしきコードが示されてます。

if (Function.prototype.bind && console && typeof console.log == "object") {
    [
      "log","info","warn","error","assert","dir","clear","profile","profileEnd"
    ].forEach(function (method) {
        console[method] = this.bind(console[method], console);
    }, Function.prototype.call);
}

console の各種メソッドを、「Function#call に元のメソッドをバインドし、第一引数に console を適用した Function オブジェクト」に置き換えています。

書いてて意味が分からないんですが、console.log("foo", "bar", "baz") としたときに、こんなコードが実行されるイメージでしょうか。

Function.prototype.call.apply(console.log, [console, "foo", "bar", "baz"]);

特別な存在である console.log ですが、Function.prototype のメソッドは使えるんですね。
内部的に関数として呼び出せるインターフェースは持ってるってことですかね?


なにはともあれ、これで結果は変えずに、console のメソッドを真の Function オブジェクトに置き換えることができそうです。

IE9 ではこのままで問題無いんですが、Function#bind, Array#forEach は IE8 じゃ使えないので、自分なりにアレンジして、最終的にはこんな感じになりました。

if (typeof console !== "undefined" && typeof console.log === "object") {
  (function() {
    var slice = Array.prototype.slice;
    var bind = Function.prototype.bind || function(context) {
      var args = slice.call(arguments, 1);
      var self = this;
      return function() {
        return self.apply(context, args.concat(slice.call(arguments)));
      };
    };
    var methods = ["log", "info", "warn", "error", "assert", "dir", "clear", "profile", "profileEnd"];
    for (var i = 0, length = methods.length; i < length; i++) {
      console[methods[i]] =
        bind.call(Function.prototype.call, console[methods[i]], console);
    }
  })();
}

コンソールでざっと試した感じだと動いてるっぽい。

JavaScript は長いことやってても、未だにこういう発見があったりして面白いですね。