本気でやるなら黙読は避けて朗読すべき ― 2008年05月18日 17時40分
「読書百遍義自ら見る」という言葉があります。難解な書物であっても 100 回も読めば自然と意味がわかるようになるという、熟読の大切さを説いた句です。しかし、これは本当のことなのでしょうか? 2000 年もたてば社会も常識もまったく変わってきます。昔の人の言ったことが今も正しいとは限りません。
疑問があれば解明したくなるのが人の性というもの。実際に「読書百遍義自ら見る」は正しいか、確かめて紀要にまとめてくださった方がいます。それによると、女子大生にデカルトの「方法序説」を 30 回読んでもらったところ、ほとんどが内容を理解するにいたったとのこと。この言葉の正しさが見事に証明されたのです。
ただし、一点注意することがあり、それは黙読ではなく朗読するということ。人間は活字を使いだしたのはたかだかこの5千年程度のことであるが、音声を使った情報のやりとりは動物の時代から行ってきたことである
という由緒正しい理由によるものです。
さて、前置きが長くなりましたが、近頃 JavaScript を本気でやるなら何を避けて何をすべきか考えるのが流行っているようです (例 1、例 2、例 3)。ここまでお読みいただいた方にはもうお分かりでしょう。そう、避けるべきは黙読で、すべきは朗読です。文書は声に出して読まなければ身につきません。とはいっても、ソースコードまで音読していたらそれはそれで間抜けです。ソースコードは音読ではなく書き写す、すなわち写経するのがいいでしょう。
では手始めに JavaScript の母体にして標準仕様である ECMAScript Language Specification (HTML 版、日本語版) を朗読し、JavaScript で書かれた JavaScript 実装である Narcissus のソースコードを写経してみましょう。分量を恐れることはありません。前者は中高生でも手軽に読めるライトノベル 1 冊分の約 6 分の 1 のページ数、後者にいたっては 2HD フロッピーディスク 0.047 枚分という驚きの軽さです。30 回も繰り返せば自ずから JavaScript を理解していることでしょう。
それでも分量が多い、とにかく最速コースをという方には Core JavaScript 1.5 ガイドを、こんなのあっという間、もっと書物をという方には、古典として Effective JavaScript と JavaScript 深層 (両者ともインターネットアーカイブより) をお勧めします。
なお、以上はあくまで言語としての JavaScript を本気でやりたいという場合に関してです。クライアントサイドスクリプティングを本気でやりたいのなら、これらに加えてJavaScript 第 5 版、Prototype.js のソースコード、jQuery のソースコードなどはいかがでしょうか。読経と写経の繰り返しが、あなたを JavaScript の悟りの境地へといざなってくれるでしょう。
「本気でやる」ためにはどうすればいいか、はっきりとわかりましたね。この方法にひとつだけ欠点があるとすれば、こんな方法を「本気でや」った人など筆者自身も含めて一人も存在しないであろうことだけです。それでは皆さん功徳を積んで解脱を目指していきましょう。
WEB+DB PRESS 連載 ― 2008年04月25日 00時24分
現在発売中の WEB+DB PRESS Vol. 44 より、「JavaScript + ブラウザ探検」という連載を始めることになりました。初回は「JavaScript + ブラウザ温故知新」として、DOM の基本概念や JavaScript を扱えるブラウザの歴史などをまとめています。次回以降は DOM や HTML 5 といった、ブラウザで利用できる API を中心に話を進めていこうと思っています。
というわけでタイトルに「JavaScript」と入ってる割に、純粋な JavaScript の話はあまりなさそうですが、ブラウザ上で何かを実現するためのヒントとなれれば幸いと思っていますので、よろしくお願いします。
第 11 回 FLOSS 桜山勉強会発表資料 ― 2008年04月02日 01時37分
先日名古屋で開かれた FLOSS 桜山の第 11 回勉強会で、JavaScript 1.7 で導入されたジェネレータに関する話をしてきました。発表資料などは以下の通りです。
- スライド「JavaScript のジェネレータ」
- Haskell のリストを模倣する JavaScript ソース
- 上記の JavaScript ソースをその場で実行できるようにしたもの (Firefox 3 Beta など、JavaScript 1.8 に対応した環境が必要)
補足 (質疑応答で出た内容も含む)
ジェネレータ、配列内包、分割代入は JavaScript 1.7 から、関数の省略表記、ジェネレータ内包、正規表現の先頭固定 (sticky) オプションは JavaScript 1.8 からの対応です。Firefox 2 は JavaScript 1.7 に、Firefox 3 は JavaScript 1.8 に対応していますが、いくつかの機能を使うためには <script type="application/javascript; version=1.7">
のように明示的にバージョン指定をしなくてはいけません。
「Haskell のリストっぽいもの」でやってることは、「無限リストと遅延評価」の「ジェネレータを使った無限リスト」および「JavaScript でカリー化、再び」の続きです。curry 関数を関数オブジェクトのメソッドとし、以下のような cons 関数を使っています。
const cons = function (head, tail) {
yield head;
for (var i in tail)
yield i;
}.curry();
print 関数は、複数の引数を与えるとそれらをスペース区切りで出力する関数です。配列内包との組み合わせは以下のように解釈されます。
var list = cons(1, cons(2, cons(3, nil())));
print.apply(null, [i for (i in list)]);
// ↓
print.apply(null, [1, 2, 3]);
// ↓
print(1, 2, 3)
// => 1 2 3
JavaScript のジェネレータ機能は基本的に Python のそれと同じですが、ジェネレータイテレータオブジェクトの close メソッドの動作が異なります。close メソッドが呼び出されたとき、Python ではジェネレータの実行を再開し、再開地点で GeneratorExit 例外を投げますが、JavaScript ではジェネレータの実行を再開し、再開地点で return 文に出会ったかのようにジェネレータの実行を終了します。また、Pyothon では GeneratorExit、StopIteration 例外が close メソッドの呼び出し元に伝播しませんが、JavaScript ではジェネレータ内で投げられたすべての例外が close メソッドの呼び出し元に伝播します。
# Python 2.5
def gen():
try:
yield
except:
print "in except/catch clause"
finally:
print "in finally clause"
raise StopIteration
g = gen()
g.next()
g.close()
# => in except/catch clause
# => in finally clause
// JavaScript 1.7
function gen() {
try {
yield;
} catch (ex) {
print("in except/catch clause");
} finally {
print("in finally clause");
throw StopIteration;
}
}
var g = gen();
g.next();
g.close();
// => in finally clause
// => uncaught exception: [object StopIteration]
配列内包およびジェネレータ内包の式部分には、関数呼び出しやオブジェクトの生成などを含む任意の式を掛けます。内包表記自体も式なので、入れ子にして使うこともできます。
[[j for (j in enumFromTo(1, i))]
for (i in enumFromTo(1, 4))].toSource();
// => [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]]
// enumFromTo 関数については後述
配列内包もジェネレータ内包も、for 節を重ねたり if 節を付加したりできます。
print.apply(null,
[x.toSource()
for (x in ([i, j] for (i in enumFromTo(1, 4))
for (j in enumFromTo(1, 4))
if (i < j)))]);
// => [1, 2] [1, 3] [1, 4] [2, 3] [2, 4] [3, 4]
正規表現の先頭固定オプションは、字句解析のときなどに使えるんじゃないかと思っています。ほかの言語を使ったりほかの手法を使ったりした場合に比べて、速度的にどうこうというのは調べていません。
現時点での JavaScript 1.7/1.8 の使いどころは、Firefox の拡張機能内部などに限られてくると思います。Web 上で気軽に使えるようになるのは果たしていつのことやら。
おまけ (関数合成)
本題からずれているような気がしたのと時間の都合で発表からははずしましたが、Haskell の .
演算子のようなことをする compose 関数も作ってみました。
const compose = (function (f, g)
(function () f(g.apply(null, arguments))).lengthIs(g.length).curry()
).curry();
lengthIs メソッドがやっていることは「JavaScript でカリー化、再び」の setParameterLength 関数と同じです。引数 i と j が与えられると、i から j までの値を収めたリストを返す enumFromTo 関数なども定義して、Haskell での階乗を求める関数の書き方を模倣してみます。
-- Haskell
fact = product . enumFromTo 1
const enumFromTo = function (from, to) {
if (from > to) return;
yield from;
for (var i in enumFromTo(from + 1, to))
yield i;
}.curry();
const foldl = function (folder, initial, list) {
try {
return foldl(folder, folder(initial, list.next()), list);
} catch (ex if ex instanceof StopIteration) {
return initial;
}
}.curry();
const mul = (function (a, b) a * b).curry();
const product = foldl(mul, 1);
const fact = compose(product, enumFromTo(1));
print.apply(null, [fact(i) for (i in enumFromTo(1, 5))]);
// => 1 2 6 24 120
別の書き方も試してみます。
-- Haskell
fact n = (foldl (.) id [\x -> x * k | k <- [1..n]]) 1
const id = (function (x) x).curry();
const fact = function (n)
foldl(compose, id, (mul(i) for (i in enumFromTo(1, n))))(1);
print.apply(null, [fact(i) for (i in enumFromTo(1, 5))]);
// => 1 2 6 24 120
ちなみにこの enumFromTo 関数で無限リストを作ることもできます。
print.apply(null,
[i for (i in take(10, enumFromTo(1, Infinity)))]);
// => 1 2 3 4 5 6 7 8 9 10
Roppongi.JS #1 ― 2008年03月21日 21時52分
さる 3 月 15 日に開かれた Roppongi.JS の jQuery コードリーディングにオンラインで一部参加しました。途中参加に途中退室、一部こちらの都合で音声なしでの中継視聴とあわただしかったものの、jQuery のソースコードをじっくり読むいい機会になりました。
jQuery オブジェクト
今回個人的にわかったのは jQuery オブジェクト、すなわち jQuery(...)
と jQuery 関数 (または $ 関数) を呼び出したときに返ってくるオブジェクトは何なのかということ。jQuery オブジェクトは配列のようなオブジェクトであり、length プロパティと 0 ~ n (n は length - 1) までの数値的な名前のプロパティを持ちます。配列としての各要素 (数値的プロパティ) は DOM ノードオブジェクトを指しています。
jQuery オブジェクト自身はは Array オブジェクトではないものの JavaScript のオブジェクト (ECMAScript でいうネイティブオブジェクト) であり、DOM ノードオブジェクト (ECMAScript でいうホストオブジェクト) ではありません。
DOM では document.getElementById("id")
と document.getElementsByTagName("element-name")
で返ってくるオブジェクトはそれぞれノードとノードリストであり、まったく性質の異なるものです。それに対して、jQuery では jQuery("#id")
と jQuery("element-name")
で返ってくるのはいずれも同じ性質の jQuery オブジェクトであり、違いといえば前者の (配列としての) 要素数が最大で 1 であるのに対し、後者の要素数、すなわち length プロパティの値は 2 以上になりうることぐらいです。
この、単一のノードを要素数が1の配列またはリストとして扱うという発想は、E4X の思想とよく似ています。E4X では、ある XML オブジェクトと、その XML オブジェクトのみを含む XMLList オブジェクトは、基本的にまったく同様に振舞うからです。
jQuery オブジェクトのメソッド
jQuery オブジェクトのメソッドは、何を返すかによって以下の3種類に分けられます。
- 自分自身 (メソッドを呼び出した jQuery オブジェクト) を返すもの。
- 新たに作成した jQuery オブジェクトを返すもの。
- jQuery オブジェクト以外の値を返すもの。
このうち、2 で作られた jQuery オブジェクトに関しては、通常の jQuery オブジェクトが持つプロパティに加え、メソッドを呼び出した jQuery オブジェクトを指す prevObject プロパティを持っています。end メソッドでこの prevObject プロパティの値を返すことにより、メソッドチェーンを柔軟につなげていくことが可能となっているのです。
雑感
今回途中参加の身で感じたのは、現在の進行状況がすぐわかるといいなということです。具体的には、現在何行目から何行目までを読んでいるのか、どこを飛ばして読み進めるのかといった情報を、逐一チャットに記録していってほしいと思いました。
また、何人かが jQuery のリファレンスへのリンクを張ってくださったのは非常に助かりました。jQuery では引数の型によって処理を分けることが多いので、どんなときにどういう処理が行われるのかをざっと知っておくだけでもずいぶん楽になります。
jQuery は非常に興味深いライブラリであり、その全貌を積極的に探っていけるこのようなイベントは貴重な存在であると感じています。主催の HolyGrail さんをはじめ、運営に携わった方々、そして参加者の皆さんに深く感謝します。
Kanasan.JS JavaScript 第 5 版読書会 #3 ― 2008年02月27日 04時47分
Kanasan.JS の JavaScript 第 5 版読書会 #3 (当日のチャットログ、参加者のブログ記事一覧) に行ってきました。同じ会場で先立って vim 勉強会があったのですが、私は先日 Meadow に乗り換えたばかりなのでそちらのほうはパスしました。
配列のプロパティ
JavaScript ではすべてのプロパティ名が文字列として扱われます。配列 a
に対して a[-1.23] = true
としても、a["-1.23"] = true
と同じに扱われ、"-1.23" という名前のプロパティができるだけでエラーにはなりません。ただし、配列に関しては非負整数とみなされる名前のプロパティが特別扱い (length プロパティにも影響を及ぼしうる) されます。
Array#join と Array#toString
配列に対して、join メソッドを引数なしで呼び出すのと toString メソッドを呼び出すのとでは同じ結果が返ってきます。ただし、join メソッドは配列のようなオブジェクト (配列ではないが length プロパティを持つオブジェクト) に対しても適用できるのに対し、配列の toString メソッドは配列にしか適用できないという違いがあります。
var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
Array.prototype.join.call(arrayLike);
// => a,b,c
Array.prototype.toString.call(arrayLike);
// => TypeError: Array.prototype.toString called on incompatible Object
そのため、実装によっては toString メソッドのほうが高速化できるかもしれません。ちなみに Firefox などで使われている SpiderMonkey の場合は、join メソッドも toString メソッドも内部的に同じ関数 (array_join_sub 関数) を利用しています。
Array#push、Array#unshift の返り値
push メソッド、unshift メソッドが要素追加後の配列の要素数を返すのはどうしてかという話題。これらのメソッドは Perl 由来なので返り値も Perl に従ったのでしょうが、なぜ Perl がそうなっているのは不明です。個人的には配列自身を返してくれたほうが何かと使いやすいと思うのですが。
Array#slice と Array#splice
slice メソッドと splice メソッドの字面が似ていて間違えやすいという声がありました。Perl や Ruby の場合は slice に相当する構文が別途存在する (@array[1..3]
) ので splice があっても混乱しないんだとか。ECMAScript 4 でもスライス構文 (array[1:3]
) が定義される予定です。
ちなみに ECMAScript では splice の第 2 引数は必須ですが、これを省略すると IE と Firefox とで動作が違ってくるそうです。
配列の各要素の比較
配列の各要素が等しいかを調べようと思って array1 == array2
と等値演算子を使ってもうまくいきません。配列はオブジェクトであり、等値演算子をオブジェクト同士の比較に用いると両者が同一のオブジェクトかどうかを返すからです。
解決策として、配列の要素がすべて文字列だとわかっていれば、join メソッドの結果を比べるといった方法があります。区切り文字を NUL 文字 ("\x00"
) にすれば文字がかぶることも減るでしょう。または、Prototype.js を使っていれば、 toJSON メソッドを使って JSON 形式に変換した文字列を比べるという方法もあるそうです。
関数宣言の構文
関数宣言の最後にセミコロンはいりません。これは入れ子になった関数宣言に対しても同じです。JavaScript にはセミコロンの自動補完があるので、本来セミコロンがいるのかどうか迷ってしまいますね。
function f() {
function g() {
...
}; // ← このセミコロンは不要 (この場合空文と解釈される)。
...
} // ← ここもセミコロンは不要。
関数式、関数リテラル、無名関数
個人的には、関数式と関数リテラルは構文の名前 (同一のものを関数式と呼ぶこともあれば関数リテラルと呼ぶこともある) で、無名関数とは識別子を指定していない関数式 (関数リテラル)、または Function コンストラクタによって作られる関数オブジェクトのことであると理解しています。(私も場合によっては意図的に混同させて使いますが。)
関数式 (関数リテラル) を使って名前つきの関数オブジェクトを作ることもできますが、JScript では関数式に指定した名前が外部に公開されたり、古い JavaScriptCore (Safari 2 以前) ではそもそも名前付き関数式に対応していなかったりするので、Web 上での使用は避けたほうがいいかもしれません。
var f = function g() { ... }; // 名前付き関数式
g(); // JScript ではエラーにならない。
負の無限大
Number.NEGATIVE_INFINITY
、-Infinity
、-1 / 0
はすべて同じ値 (負の無限大) を指します。
関数のオーバーロード
JavaScript では関数のオーバーロードはできないので、同様の機能を実現しようとしたら自分で引数をチェックとして処理を振り分ける必要があります。オーバーロードを実現するためのライブラリも多数公開されているようです (Introducing overload.js、JavaScript Method Overloading など)。
PascalCase
変数などの命名規則について。UpperCamelCase のことを PascalCase ともいうそうです。JavaScript 組み込みのメソッド名は基本的に lowerCamelCase なので、私はそれに合わせているのですが、アンダースコア区切りのほうがスコープが狭い気がするのでローカル変数はアンダースコア区切りという人もいました。
Function#length
関数オブジェクトの length プロパティ (仮引数の数を表す) は読み取り専用なので値を上書きできません。ただし、JavaScript では読み取り専用のプロパティに値を設定しようとしてもエラーは出ません。SpiderMonkey の strict モードでは警告が出ます。
function f(a, b, c) {}
f.length = 42;
// => strict warning: f.length is read-only
// => 42
// 代入式の返り値は右辺の値。
f.length;
// => 3
読み取り専用のプロパティを新しく作ることはできません。SpiderMonkey 1.7 (Firefox 2) までは Object.prototype.eval と const 文を組み合わせることにより実現可能でしたが、SpiderMonkey 1.8 (Firefox 3) で Object.prototype.eval は削除されます。
var o = {};
o.eval("const x = 42;");
o.x = 12;
o.x; // => 42 (o.x は読み取り専用になる)
カリー化と部分適用
個人的な理解では、一部の引数の値を固定するのが部分適用 (n 引数関数と m 個の値を受け取り、n - m 引数関数を返す)、ある関数を部分適用可能な関数に変換する操作がカリー化です。
最近のコメント