導入
なぜ 「再」入門 (re-introduction) なのか? なぜなら JavaScript は世界で最も誤解されたプログラミング言語【訳注: 日本語訳】であると言える合理的な理由があるからです。しばしばおもちゃだと馬鹿にされながら、しかしその人を欺くような単純さの下に、強力な言語機能が隠されているのです。2005 年は数々の高い注目を集める JavaScript アプリケーションが発表され、この技術の深い知識がどんなウェブ開発者にとっても重要なスキルだということが示されました。
この言語の歴史について理解することから始めるのが役立つでしょう。JavaScript は 1995 年、Netscape の技術者ブレンダン・アイク (Brendan Eich)によって創られ、1996 年初頭に Netscape 2 で初めてリリースされました。当初は LiveScript と呼ばれる予定でしたが、Sun Microsystems の Java 言語の人気にあやかろうという不運なるマーケティング上の決定により改名されました ― 2 つの言語が共通点をほとんど持っていないにもかかわらず。それ以来、このことは未だに混同の元となっています。
Microsoft はその数か月後、IE3 とともに JScript というほとんど互換性のある言語をリリースしました。Netscape はこの言語をヨーロッパの標準化団体 Ecma International に提出し、その結果 1997 年に ECMAScript という標準の第 1 版が生まれました。この標準は重要なアップデートを受けて 1999 年に ECMAScript 第 3 版となり、その後しばらくほとんど安定してきました。言語の複雑化に関する政治的な隔たりから、第 4 版は放棄されたものの、その多くのパーツは 2009 年 12 月に発行された新しい ECMAScript 第 5 版の基礎となりました。
第 3 版からのこの安定性は、様々な実装系が標準に追い付くのに十分な時間が与えられたという意味で、開発者にとってうれしいニュースです。私はほとんどもっぱら第 3 版の方言に焦点を当てていきます。ただ、なじみの深さから JavaScript という語は最後まで使い続けるつもりです。
大部分のプログラミング言語と違って、JavaScript という言語には入出力の概念がありません。この言語はあるホスト環境でのスクリプト言語として実行されるよう設計されており、外部の世界とコミュニケーションするための機構はそのホスト環境が提供するものとしているのです。もっとも一般的なホスト環境はブラウザですが、JavaScript のインタプリタは Adobe Acrobat や Photoshop、Yahoo! ウィジェットエンジン、さらにはサーバサイド環境にも見つけることができます。
概要
JavaScript はオブジェクト指向の動的言語であり、型や演算子、コアオブジェクト、メソッドがあります。その構文は Java や C 言語に由来するので、それらの言語の多くの構造が JavaScript にも同様に適用できます。重要な違いの一つに、JavaScript にはクラスがありません。その代わり、クラスの機能はオブジェクトプロトタイプによって達成されています。その他の主な違いは、関数もオブジェクトであるということです。それゆれ、関数に実行可能なコードを持たせて、他のオブジェクトと同じように受け渡しさせることができます。
まずはあらゆる言語の構成要素、「型」を見ることから始めましょう。JavaScript のプログラムは値を操作し、それらの値はすべてある型に属しています。JavaScript の型は:
…ああ、あと Undefined と Null、これらはちょっと変わっています。そして Array (配列)、これは特殊なオブジェクトの一種。さらに Date と RegExp (正規表現)、これらは自由に使えるオブジェクトです。あと技術的に正確なことを言うと、関数は単に特殊なオブジェクトの一種です。したがってこの型の図はより正確にはこうなります:
- 数値
- 文字列
- 真偽値
- オブジェクト
- 関数
- Array (配列)
- Date
- RegExp (正規表現)
- Null
- Undefined
さらにいくつかの組み込み Error 型もあります。しかし最初の図のままでいく方が、物事はとても簡単になるでしょう。
数値
JavaScript における数値は、仕様によると「倍精度 64 ビットフォーマット IEEE 754 値 (double-precision 64-bit format IEEE 754 values)」です。このことはいくつかの興味深い結果をもたらします。JavaScript には整数に当たるものがないので、あなたが C や Java の数学に慣れている場合は、数値計算に少し気を付ける必要があります。次のようなものに注意しましょう:
0.1 + 0.2 == 0.30000000000000004
実のところ、整数値は 32 ビット int 型として扱われます (一部のブラウザの実装では実際にそのように記憶されます) 。これはビット演算を行う際に重要なことです。詳しくは、The Complete JavaScript Number Reference をご覧ください。
足し算、引き算、モジュロ (剰余) など、標準的な算術演算がサポートされています。さらに、これは先ほど言及し忘れたのですが、より高度な数学関数や定数を扱う Math という組み込みオブジェクトもあります:
Math.sin(3.5); var d = Math.PI * r * r;
あなたは組み込みの parseInt()
関数を使うことで、文字列を整数に変換することができます。この関数は省略可能な第 2 引数として変換の基数を取りますが、あなたは常にこの引数を与えるべきです:
> parseInt("123", 10) 123 > parseInt("010", 10) 10
もし基数を与えなかったら、あなたは驚くような結果を得ることでしょう:
> parseInt("010") 8
こうなったのは、parseInt
がこの文字列を先頭の 0 から 8 進数とみなすべきと判断したためです。
もし2進数を整数に変換したいなら、単純に基数を変えましょう:
> parseInt("11", 2) 3
同様に浮動小数点数への変換を行う、組み込みの parseFloat()
関数があります。こちらは parseInt()
と異なり、基数は常に 10 が用いられます。
また、単項演算子 +
を使って値を数値に変換する:
> + "42" 42
もし文字列が数でない場合、NaN
(非数、"Not a Number" の略) と呼ばれる特別な値が返ります:
> parseInt("hello", 10) NaN
NaN
には毒性があります: これを入力としてどの算術演算に与えても、その結果は同様に NaN
になるのです:
> NaN + 5 NaN
組み込みの isNaN()
関数を使えば、NaN
であるかを検査することができます:
> isNaN(NaN) true
JavaScriptはまた、特別な値 Infinity
と -Infinity
を持っています:
> 1 / 0 Infinity > -1 / 0 -Infinity
組み込みの isFinite()
関数を使えば、Infinity
、-Infinity
、NaN
であるかを検査することができます。:
> isFinite(1/0) false > isFinite(-Infinity) false > isFinite(NaN) false
parseInt()
および parseFloat()
関数は文字列を、規定の数値書式に該当しない文字が現れるまで解析し、その箇所までの数値を返します。一方、"+" 演算子は適切でない文字を含む文字列を NaN
に変換します。ご自身でコンソールを用いて、文字列 "10.2abc" をそれぞれの方法で解析させるとその違いがよくわかるでしょう。文字列
JavaScript における文字列は、文字の連なったもの (sequences)です。より正確に言えば、文字列は Unicode 文字の連なったものであり、つまりそれぞれの文字は 16 ビットの整数で表されます。これは国際化 (internationalisation)に対処しなければならない誰もが歓迎するニュースでしょう。
もし単一文字を表したいなら、単純に長さ 1 の文字列を使います。
文字列の長さを知るには、その文字列の length
プロパティにアクセスしましょう:
> "hello".length 5
JavaScript のオブジェクトとの初めての接触です! 私、文字列もオブジェクトであると言いましたっけ? (Did I mention that strings are objects too?) 文字列はまた、メソッドも持っています:
> "hello".charAt(0) h > "hello, world".replace("hello", "goodbye") goodbye, world > "hello".toUpperCase() HELLO
その他の型
JavaScript は null
と undefined
を区別します。null
は、意図的に「値がない」ということを指し示す object 型のオブジェクトです。対して undefined
とは、初期化されていない値 ― すなわち、「まだ値が代入されていない」ということを指し示す undefined 型のオブジェクトです。変数については後で話しますが、JavaScript では値を代入しないで変数を宣言することができるのです。そのようにした場合、その変数の型は undefined
です。
JavaScript は true
と false
(これらはともにキーワードです) を取りうる値とする真偽値型を持っています。どんな値でも以下のルールに基づいて真偽値に変換できます:
false
、0
、空文字列 (""
)、NaN
、null
、およびundefined
は、すべてfalse
になる- その他の値はすべて
true
になる
Boolean()
関数を使うことで、明示的にこの変換を行うことができます:
> Boolean("") false > Boolean(234) true
しかしながら、これはほとんど必要ありません。なぜなら JavaScript は、if
文 (下記参照) の中といった真偽値が期待されるときに、無言でこの変換を行うからです。このような理由から、私たちは時々、真偽値に変換されるとき true
または false
になる値という意味で、それぞれ "true values" または "false values" と言うことがあります。あるいは、そのような値はそれぞれ "truthy" または "falsy" と呼ばれます。
&&
(論理 AND) や ||
(論理 OR) 、!
(論理 NOT) などの真偽値演算がサポートされています (下記参照)。
変数
JavaScript における新しい変数は var
キーワードを使って宣言されます:
var a; var name = "simon";
もし値を代入しないで変数を宣言すると、その型は undefined
になります。
Java など他の言語との重要な相違点は、JavaScript ではブロックがスコープを持たず、関数のみがスコープを持つことです。よって、変数が複合文内 (例えば if
制御構造の内部) で var
を用いて定義された場合でも、その変数は関数全体でアクセス可能です。
演算子
JavaScript の算術演算子は、+
、-
、*
、/
、そして剰余演算子の %
です。値は =
を使って代入されます。また +=
や -=
のような複合代入文もあります。これらは x = x 演算子 y
と展開できるものです。
x += 5 x = x + 5
++
や --
を使ってインクリメントやデクリメントをすることができます。これらは前置あるいは後置演算子として使うことができます。
+
演算子は文字列の結合もします:
> "hello" + " world" hello world
文字列を数字 (や他の値) に足すと、全てのものはまず最初に文字列に変換されます。このことはミスを誘うかもしれません:
> "3" + 4 + 5 345 > 3 + 4 + "5" 75
空文字列を足すのは、何かを文字列に変換する便利な方法です。
JavaScript における比較は、<
や >
、<=
、>=
を使ってすることができます。これらは文字列と数値のどちらでも機能します。等価性はちょっと明快ではありません。二重イコール演算子は、異なる型を与えると型強制 (type coercion)を行います。これは時に面白い結果を返します:
> "dog" == "dog" true > 1 == true true
型強制を防ぐには、三重イコール演算子を使います:
> 1 === true false > true === true true
!=
と !==
演算子もあります。
JavaScript はビット演算子も持っています。使いたいなら、ちゃんとありますよ。
制御構造
JavaScript は C 言語ファミリーの他の言語とよく似た制御構造セットを持っています。条件文は if
と else
でサポートされています。必要ならこれらを連鎖させることもできます:
var name = "kittens"; if (name == "puppies") { name += "!"; } else if (name == "kittens") { name += "!!"; } else { name = "!" + name; } name == "kittens!!"
JavaScript は while
ループと do-while
ループを持っています。1 つ目は普通のループ処理に適しており、2 つ目はループの本体が少なくとも 1 回は実行されるようにしたいときのループです:
while (true) { // 無限ループ! } var input; do { input = get_input(); } while (inputIsNotValid(input))
JavaScript の for
ループは C や Java のと同じです。これはループの制御情報を 1 行で与えることができます。
for (var i = 0; i < 5; i++) { // 5 回実行されます }
&&
と ||
演算子は、1 つ目のオペランドによって 2 つ目のオペランドを評価するか否かが決まる短絡論理 (short-circuit logic)を用いています。これはあるオブジェクトの属性にアクセスする前に、それが null オブジェクトかをチェックするのに便利です:
var name = o && o.getName();
あるいはデフォルト値をセットするのにも便利です:
var name = otherName || "default";
JavaScript はワンライン条件文のための三項演算子を持っています:
var allowed = (age > 18) ? "yes" : "no";
switch 文はある数値や文字列を元にした複数分岐に使われます:
switch(action) { case 'draw': drawit(); break; case 'eat': eatit(); break; default: donothing(); }
もし break
文を入れなければ、処理は次の段階へフォールスルー (fall through)します。この動作が望むものであることは非常にまれでしょう ― 事実、もしそれが本当に意図するものならば、デバッグの補助として故意のフォールスルーをコメントで明確にラベリングするだけの価値があるでしょう:
switch(a) { case 1: // フォールスルー case 2: eatit(); break; default: donothing(); }
default 節は省略できます。必要ならば、switch 部と case 部のどちらにも式を置くことができます。比較はこれら2つの間で ===
演算子を使って行われます:
switch(1 + 3) { case 2 + 2: yay(); break; default: neverhappens(); }
オブジェクト
JavaScript のオブジェクトは、名前と値のペアの単純なコレクションです。これは以下のものに似ています:
- Python の辞書型
- Perl や Ruby のハッシュ
- C や C++ のハッシュテーブル
- Java の HashMap クラス
- PHP の連想配列
このデータ構造が幅広く使われているという事実は、この構造の万能性の証拠でしょう。JavaScript において (コアデータ型を除いた) すべてのものはオブジェクトなので、どんな JavaScript プログラムも自然と非常に多くのハッシュテーブルのルックアップ (検索) を伴います。良いことにそれがとても速いのです!
「名前」部は JavaScript における文字列であるのに対し、値は JavaScript のどんな値でも ― さらなるオブジェクトでも ― 構いません。この仕様が任意の複雑なデータ構造を作ることを可能にしています。
空のオブジェクトを生成する 2 つの基本的な方法があります:
var obj = new Object();
そして:
var obj = {};
これらは意味的に等価です。2 つ目はオブジェクトリテラル構文と呼ばれ、こちらの方がより便利です。オブジェクトリテラル構文は JSON 書式の中核でもあり、こちらを採用するべきです。
一度作ってしまえば、オブジェクトのプロパティには 2 つの方法のいずれかで再びアクセスすることができます:
obj.name = "Simon"; var name = obj.name;
そして...
obj["name"] = "Simon"; var name = obj["name"];
これらもまた意味的に等価です。2 つ目の方法はプロパティの名前を文字列として与えるという利点があり、つまりその名前を実行時に計算できることを意味します。ただ、この方法を用いると JavaScript エンジンや minifier による最適化が適用されなくなります。またこの方法は、予約語と同じ名前のプロパティを設定したり取得したりするのに使うことができます:
obj.for = "Simon"; // 構文エラー。'for' が予約語であるため obj["for"] = "Simon"; // うまく動きます
オブジェクトリテラル記法はオブジェクトをそっくりそのまま初期化するのに使えます:
var obj = { name: "Carrot", "for": "Max", details: { color: "orange", size: 12 } }
属性へのアクセスは同時に連鎖させることができます:
> obj.details.color orange > obj["details"]["size"] 12
配列
JavaScript における配列は、実は特別なタイプのオブジェクトです。普通のオブジェクトとほとんど同じように働きます (数字のプロパティは当然 [] の構文でのみアクセスできます) が、しかし配列は 'length
' という魔法のプロパティを持っています。これは常に配列の一番大きな添字より 1 大きい値を取ります。
配列を生成する古いやり方は以下の通り:
> var a = new Array(); > a[0] = "dog"; > a[1] = "cat"; > a[2] = "hen"; > a.length 3
より便利な書き方は配列リテラルを使うことです:
> var a = ["dog", "cat", "hen"]; > a.length 3
配列リテラルの最後にカンマを残しておくのは、ブラウザ間で対応非対応があるので、してはいけません。
array.length
は必ずしも配列中の要素の数ではないことに注意してください。以下の例を考えてみましょう:
> var a = ["dog", "cat", "hen"]; > a[100] = "fox"; > a.length 101
思い出してください ― 配列の長さは一番大きな添字より 1 大きい値です。
もし存在しない配列の添字を要求すると、undefined
が得られます:
> typeof a[90] undefined
上記のことを考慮に入れれば、以下の方法を使って配列をイテレートする (各要素を順に巡る) ことができます:
for (var i = 0; i < a.length; i++) { // a[i] について何かする }
これは毎回 length プロパティをルックアップしているため少し非効率です。改善したのがこれ:
for (var i = 0, len = a.length; i < len; i++) { // a[i] について何かする }
よりすてきな形式は:
for (var i = 0, item; item = a[i++];) { // item について何かする }
ここでは 2 つの変数を準備しています。for
ループの中央部の代入文は、同時にそれが真であるか検査しています ― もしこれが真なら、ループは継続します。i
は毎回インクリメントされるので、配列の要素は順番に item に代入されます。ループは "falsy" な要素 (例えば undefined
) が見つかったとき止まります。
このトリックは "falsy" な値が含まれないとわかっている配列 (例えばオブジェクトや DOM ノードの配列) に対してのみ使われるべきであることに注意してください。もし 0 が含まれるかもしれない数値データや、空文字列が含まれるかもしれない文字列データをイテレートしようとするならば、代わりに i, len
の形式を使うべきです。
イテレートするもう 1 つの方法は for...in
ループを使うことです。ただし、もし誰かが Array.prototype
に新しいプロパティを追加していたら、それらもこのループでイテレートされてしまうので注意してください:
for (var i in a) { // a[i] について何かする }
配列に要素を追加したいなら、このようにするのが最も安全です:
a[a.length] = item; // a.push(item); と同じ
a.length
は一番大きな添字より 1 大きい値なので、このコードは配列の末尾にある空の場所に代入しようとすることを保証します。
配列には多くのメソッドが付いてきます:
メソッド名 | 説明 |
---|---|
a.toString() |
|
a.toLocaleString() |
|
a.concat(item[, itemN]) |
配列に要素を追加した、新しい配列を返します。 |
a.join(sep) |
|
a.pop() |
最後の要素を取り除いて、その要素を返します。 |
a.push(item[, itemN]) |
Push は配列の末尾に 1 つ以上の要素を追加します。 |
a.reverse() |
|
a.shift() |
|
a.slice(start, end) |
部分配列を返します。 |
a.sort([cmpfn]) |
オプションとして比較関数をとります。 |
a.splice(start, delcount[, itemN]) |
ある部分を削除して他の要素に置き換えることで、配列を修正することができます。 |
a.unshift([item]) |
要素を配列の先頭に挿入します。 |
関数
オブジェクトと共に、関数は JavaScript を理解するうえで核となる構成要素です。最も基本的な関数は極めてシンプルです:
function add(x, y) { var total = x + y; return total; }
これは基本的な関数について知るためのすべてを例示しています。JavaScript の関数は 0 以上の名前の付いた引数を取ることができます。関数の本体は好きなだけたくさんの文を含ませることができ、またその関数内で局所的な変数を宣言することができます。return
文は好きな時に関数を終了し値を返すために使うことができます。もし return
文が使われなかったら (あるいは値を付けない空の return が使われたら) 、JavaScript は undefined
を返します。
実のところ、名前の付いた引数はガイドラインのようなもの以上の何物でもありません。あなたは期待された引数を渡さずに関数を呼ぶことができます。その場合引数には undefined
がセットされます。
> add() NaN // You can't perform addition on undefined
あなたはまた、関数が期待しているより多くの引数を渡すこともできます:
> add(2, 3, 4) 5 // added the first two; 4 was ignored
これは少し馬鹿げているように見えるかもしれませんが、関数はその本体の中で arguments
と呼ばれる追加の変数を利用することができます。これはその関数へ渡された全ての値を保持する配列のようなオブジェクトです。さあ、add 関数を好きなだけたくさんの値をとれるよう書き直してみましょう:
function add() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum; } > add(2, 3, 4, 5) 14
しかしこれは 2 + 3 + 4 + 5
と書くより使い勝手がいいものでは全くありません。平均を取る関数を作ってみましょう:
function avg() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; } > avg(2, 3, 4, 5) 3.5
この関数はかなり便利ですが、新たな問題を提示しています。この avg()
関数はコンマ区切りのリストを引数に取りますが、もし配列の平均を知りたいときにはどうしたらいいでしょう? あなたは単純に関数を以下のように書き直すこともできます:
function avgArray(arr) { var sum = 0; for (var i = 0, j = arr.length; i < j; i++) { sum += arr[i]; } return sum / arr.length; } > avgArray([2, 3, 4, 5]) 3.5
しかし私たちがすでに作った関数を再利用できた方がいいですよね。幸運なことに、JavaScript は関数オブジェクトの apply()
メソッドを使うことで関数を呼ぶ、あるいは引数に任意の配列を付けて呼ぶことができます。
> avg.apply(null, [2, 3, 4, 5]) 3.5
apply()
の第 2 引数は引数として使う配列です。第 1 引数は後で論じます。このようなことは関数もまたオブジェクトであるという事実を強調します。
JavaScript では無名関数 (anonymous functions)を作ることができます。
var avg = function() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; }
これは意味的には function avg()
形式と等価です。これは非常に強力です。あなたは普通は式を置くところならどこにでも完全な関数定義を置くことができるのです。これはあらゆる巧妙なトリックを可能にしています。ここではいくつかの局所変数を ― C のブロックスコープのように ― 「隠す」方法を示します:
> var a = 1; > var b = 2; > (function() { var b = 3; a += b; })(); > a 4 > b 2
JavaScript では関数を再帰的に呼び出すことができます。これは特にブラウザの DOM などから得られる木構造を取り扱うときに便利でしょう。
function countChars(elm) { if (elm.nodeType == 3) { // TEXT_NODE return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += countChars(child); } return count; }
この例は無名関数に関するある潜在的な問題を際立たせます: 名前を持たない関数を再帰呼び出しさせるにはどうしたらよいのでしょう? その答えは arguments
オブジェクトに隠されています。このオブジェクトは引数のリストとして振舞うのに加えて、arguments.callee
と呼ばれるプロパティを提供しています。arguments.callee
の使用は非推奨になり、strict mode では使用不可になりました。これに代わり、以下のように "名前付き無名関数" を用いてください。:
var charsInBody = (function counter(elm) { if (elm.nodeType == 3) { // TEXT_NODE return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += counter(child); } return count; })(document.body);
上記のように無名関数に与えられた名前は、(少なくとも) 関数自身のスコープ内でのみ有効です。これはエンジンによる高度な最適化と可読性の高いコードの両方を実現します。
カスタムオブジェクト
古典的なオブジェクト指向プログラミングにおいて、オブジェクトとはデータとそのデータを操作するメソッドの集まりです。JavaScript は、C++ や Java に見られる class 文を持たない、プロトタイプベースの言語です。(これは、class 文を持つ言語に慣れたプログラマを混乱させることでしょう。) 代わりに、JavaScript は関数をクラスとして用います。ファーストネームとラストネームのフィールドを持つ人物オブジェクトを考えてみましょう。その名前を表示させる方法には2種類が考えられます: "first last" と "last, first" です。ここまでで論じた関数とオブジェクトを使ってみると、次のようなやり方があります:
function makePerson(first, last) { return { first: first, last: last } } function personFullName(person) { return person.first + ' ' + person.last; } function personFullNameReversed(person) { return person.last + ', ' + person.first } > s = makePerson("Simon", "Willison"); > personFullName(s) Simon Willison > personFullNameReversed(s) Willison, Simon
これはこれでうまく行きますが、かなり見苦しいですね。グローバルな名前空間にいくつもの関数を作ることになってしまいます。本当にしたいことは関数をオブジェクトにくっつけることです。関数はオブジェクトなので、簡単にできます:
function makePerson(first, last) { return { first: first, last: last, fullName: function() { return this.first + ' ' + this.last; }, fullNameReversed: function() { return this.last + ', ' + this.first; } } } > s = makePerson("Simon", "Willison") > s.fullName() Simon Willison > s.fullNameReversed() Willison, Simon
おや、まだ見たことがないものがありますね: 'this
' キーワードです。関数内で使われると、'this
' は現在のオブジェクトを参照します。実際に意味するところは関数の呼ばれ方によります。オブジェクト上で ドットの記法や角カッコの記法 を使って呼び出すと、そのオブジェクトが 'this
' になります。ドット記法を使わずに呼び出すと、'this
' はグローバルオブジェクトを参照します。このことはよくある失敗の原因になります。例えば:
> s = makePerson("Simon", "Willison") > var fullName = s.fullName; > fullName() undefined undefined
fullName()
を呼び出すとき、'this
' はグローバルオブジェクトに結び付けられます。first
や last
というグローバル変数はありませんので、それぞれに対して undefined
が得られます。
この 'this
' キーワードを活用することで、makePerson
関数を改良することができます:
function Person(first, last) { this.first = first; this.last = last; this.fullName = function() { return this.first + ' ' + this.last; } this.fullNameReversed = function() { return this.last + ', ' + this.first; } } var s = new Person("Simon", "Willison");
もう 1 つのキーワード 'new
' が出てきました。new
は 'this
' と強い関連があります。これが何をするかというと、新しい空のオブジェクトを作り、'this
' にその新しいオブジェクトをセットして、後に続く関数を呼びます。'new
' によって呼ばれるよう設計された関数はコンストラクタ関数と呼ばれます。new
によって呼ばれるということがわかるよう、先頭を大文字にすることがよく行われています。
person オブジェクトはだいぶ良くなりましたが、まだ改善の余地があります。person オブジェクトを作るたびに、その中に 2 つの新しい関数オブジェクトを作っています。関数のコードは共有されたほうがいいですよね?
function personFullName() { return this.first + ' ' + this.last; } function personFullNameReversed() { return this.last + ', ' + this.first; } function Person(first, last) { this.first = first; this.last = last; this.fullName = personFullName; this.fullNameReversed = personFullNameReversed; }
このほうが良いですね。メソッド関数を一度だけ作って、コンストラクタの中でそれへの参照を代入しています。もっとよくなりませんかね? 答えは yes です:
function Person(first, last) { this.first = first; this.last = last; } Person.prototype.fullName = function() { return this.first + ' ' + this.last; } Person.prototype.fullNameReversed = function() { return this.last + ', ' + this.first; }
Person.prototype
は Person
のすべてのインスタンスで共有されるオブジェクトです。これは (「プロトタイプチェーン」という特別な名前を持った) ルックアップチェーンの一部を構成します。Person
の何もセットされていないプロパティにアクセスしようとするときはいつでも、JavaScript は Person.prototype
が代わりのプロパティを持っているか確認します。結果として、Person.prototype
に割り当てられたプロパティはすべて this
オブジェクトを通じてコンストラクタのすべてのインスタンスで利用できるようになります。
これはとても強力です。JavaScript では、プログラム上でいつでもどれかのプロトタイプを変更することができます。ということは、実行時に既存のオブジェクトに対して追加のメソッドを加えることができるのです:
> s = new Person("Simon", "Willison"); > s.firstNameCaps(); TypeError on line 1: s.firstNameCaps is not a function > Person.prototype.firstNameCaps = function() { return this.first.toUpperCase() } > s.firstNameCaps() SIMON
興味深いことに、JavaScript の組み込みオブジェクトのプロトタイプにも差し込むことができます。String
オブジェクトに文字列を逆さにして返すメソッドを加えてみましょう:
> var s = "Simon"; > s.reversed() TypeError on line 1: s.reversed is not a function > String.prototype.reversed = function() { var r = ""; for (var i = this.length - 1; i >= 0; i--) { r += this[i]; } return r; } > s.reversed() nomiS
私たちの新しいメソッドは文字列リテラル上でさえも動きます!
> "This can now be reversed".reversed() desrever eb won nac sihT
前にも言ったように、prototype は連鎖の一部を構成します。連鎖の根は Object.prototype
であり、toString()
メソッドを含んでいます。これはオブジェクトを文字列で表そうとするときに呼ばれるメソッドです。これは Person オブジェクトをデバッグするときに役に立ちます:
> var s = new Person("Simon", "Willison"); > s [object Object] > Person.prototype.toString = function() { return '<Person: ' + this.fullName() + '>'; } > s <Person: Simon Willison>
avg.apply()
の第一引数が null であったことを覚えていますか? もう一度見てみましょう。apply()
の第一引数は 'this
' として扱われるオブジェクトです。例えば、これは 'new
' のちょっとした実装です:
function trivialNew(constructor) { var o = {}; // Create an object constructor.apply(o, arguments); return o; }
プロトタイプ連鎖を設定しないので、trivalNew()
は new
の完全な複製ではありません。apply()
は理解しづらいです。よく使うことはないでしょうが、知っていると役に立ちます。
apply()
には姉妹関数 call
があります。'this
' を設定できる点は同じですが、引数に配列ではなく展開された値のリストをとります。
function lastNameCaps() { return this.last.toUpperCase(); } var s = new Person("Simon", "Willison"); lastNameCaps.call(s); // Is the same as: s.lastNameCaps = lastNameCaps; s.lastNameCaps();
内部関数
JavaScriptでの関数宣言は他の関数内でも行えます。これは初期の makePerson()
関数で見ています。大事なことは内部関数内で親関数スコープの変数にアクセスできることです:
function betterExampleNeeded() { var a = 1; function oneMoreThanA() { return a + 1; } return oneMoreThanA(); }
内部関数は保守しやすいコードを書くときに多大な利便性をもたらします。ある関数が他の部分のコードでは役立たない関数を一つか二つ使っているなら、これらのユーティリティ関数を他から呼び出される関数の入れ子にすることが出来ます。内部関数はグローバルスコープでなくなるので、いいことです。
内部関数はグローバル変数を使うという誘惑に対する対抗措置です。複雑なコードを書くとき、複数の関数間で値を共有するためにグローバル変数を使いたくなります。しかし、これでは保守がしづらくなります。内部関数は親関数の変数を共有できるので、グローバルな名前空間を汚染せずに複数の関数をまとめることが出来ます。この仕組みは注意して使用する必要がありますが、便利です。
クロージャ
ここでは JavaScript が持つもっとも強力な、しかしもっともわかりにくいとも思われる概念のひとつを紹介します。これは何をしているのでしょうか?
function makeAdder(a) { return function(b) { return a + b; } } x = makeAdder(5); y = makeAdder(20); x(6) ? y(7) ?
makeAdder
関数がクロージャの正体を明らかにします。この関数は 1 つの引数とともに呼び出されたときに、その値と自身が生成されたときの引数の値とを加算する、新たな '加算' 関数を生成しています。
ここで起きていることは、前出の内部関数で起きていることとほとんど同じです。つまり、ある関数の内部で定義された関数は、外側の関数が持つ変数にアクセスすることができます。唯一の違いは外側の関数が返されることであり、それゆえ一般的な考えではローカル変数は存在しなくなると考えられます。しかし、ここではそれが残り続けます。そうでなければ、加算関数は動作しないでしょう。さらに、makeAdder
のローカル変数には異なる 2 つの "複製" が存在します。一方の a
は 5、もう一方の a
は 20 です。よって、これらの関数を呼び出した結果は以下のようになります:
x(6) // returns 11 y(7) // returns 27
これは実際に起きていることです。JavaScript で関数を実行するときは必ず、その関数内で作成されたローカル変数を保持する 'scope' オブジェクトが作成されます。それは関数の引数として渡された変数とともに初期化されます。これはすべてのグローバル変数やグローバル関数が存在している global オブジェクトに似ていますが、2 つの重要な違いがあります。ひとつは、関数を実行し始めるたびに新たな scope オブジェクトが生成されること、もうひとつは、global オブジェクト (ブラウザでは window としてアクセス可能) とは異なり、これらの scope オブジェクトに JavaScript のコードから直接アクセスできないことです。例えば、現存する scope オブジェクトのプロパティをたどる仕組みはありません。
よって makeAdder
が呼び出されたときは、1 個のプロパティを持つ scope オブジェクトが生成されます。そのプロパティとは、makeAdder
関数に渡される引数の a
です。そして makeAdder
は、新たに生成された関数を返します。通常 JavaScript のガベージコレクタは、この時点で makeAdder
のために生成された scope オブジェクトを破棄しますが、返された関数は scope オブジェクトへの参照を維持しています。その結果、scope オブジェクトは makeAdder
が返した関数オブジェクトへの参照がなくなるまでの間、ガベージコレクトの対象になりません。
JavaScript のオブジェクトシステムが利用するプロトタイプチェインと同様に、scope オブジェクトはスコープチェインと呼ばれるチェインを構成します。
クロージャは、関数と関数が生成した scope オブジェクトを組み合わせたものです。
クロージャは状態を保存します。従って、オブジェクトの代わりとしてよく使用されます。
メモリリーク
クロージャの良くない副作用として、Internet Explorer で簡単にメモリリークを発生させることがあります。JavaScript はガベージコレクトを行う言語です。オブジェクトは生成時にメモリが割り当てられ、オブジェクトへの参照がなくなったときにメモリがブラウザによって回収されます。ホストの環境から提供されるオブジェクトは、その環境に管理されます。
ブラウザは、与えられた HTML ページを構成する多くのオブジェクト (DOM のオブジェクト) を管理する必要があります。これらの割り当てや回収の管理を行うのはブラウザ次第です。
Internet Explorer は自身のガベージコレクト機能を用いますが、JavaScript で使用される仕組みとは分離されています。これにはメモリリークを発生させる、2 つの相互作用があります。
IE でのメモリリークは、JavaScript オブジェクトとネイティブオブジェクトとの間で循環参照が構成されたときに発生します。以下の例を考えてみましょう:
function leakMemory() { var el = document.getElementById('el'); var o = { 'el': el }; el.o = o; }
上記の例で構成された循環参照はメモリリークを引き起こします。IE はブラウザが完全に再起動されるまで、el
および o
で使用されているメモリを解放しません。
上記の例は気づかれない可能性があります。メモリリークはアプリケーションを長時間にわたって実行するときや、大きなデータ構造またはループ内でのリークが原因で大量のメモリがリークしたときのみ実際の問題になるためです。
このように明白なメモリリークはめったにありません。メモリリークを起こしたデータ構造にはたいてい、多段階の参照と見つけにくい循環参照があります。
クロージャでは、思わぬメモリリークが容易に発生します。以下のコードについて考えてみてください:
function addHandler() { var el = document.getElementById('el'); el.onclick = function() { this.style.backgroundColor = 'red'; } }
上記のコードは、要素がクリックされたときに色を赤に変えるものです。これはメモリリークが発生します。なぜでしょう? その理由は、無名の内部関数のために生成されたクロージャによって el
への参照が意図せずキャッチされるためです。これは JavaScript オブジェクト (function) とネイティブオブジェクト (el
) との間で循環参照を発生させます。
needsTechnicalReview();
この問題に対する回避策がいくつかあります。もっとも簡単な方法は、変数 el
を使用しないことです:
function addHandler(){ document.getElementById('el').onclick = function(){ this.style.backgroundColor = 'red'; } }
驚いたことに、クロージャによって発生する循環参照を解消する手法のひとつが、別のクロージャを追加します:
function addHandler() { var clickHandler = function() { this.style.backgroundColor = 'red'; }; (function() { var el = document.getElementById('el'); el.onclick = clickHandler; })(); }
内部関数は直ちに実行され、clickHandler
によって作成されたクロージャからそのコンテンツを隠蔽します。
クロージャに対する別の良い手法として、window.onunload
イベント中に循環参照を解消することがあります。多くのイベントライブラリがこれを行うでしょう。ただし、これを行うと Firefox 1.5 の bfcache が無効になりますので、他に理由がない限り Firefox では unload
リスナを登録するべきではないことに注意してください。
原文情報
- Author: Simon Willison
- Last Updated Date: March 7, 2006
- Copyright: © 2006 Simon Willison, contributed under the Creative Commons: Attribute-Sharealike 2.0 license.
- More information: For more information about this tutorial (and for links to the original talk's slides), see Simon's Etech weblog post.