Visit Mozilla.org

A re-introduction to JavaScript

出典: MDC


目次

[編集] 導入

なぜ「再」紹介なのか? なぜなら JavaScript世界で最も誤解されたプログラミング言語【訳注: 日本語訳であるという合理的な理由があるからです。しばしばおもちゃだと馬鹿にされながら、しかしその人を欺くような単純さの下に、強力な言語機能が眠っているのです。2005 年は数々の高い注目を集める JavaScript アプリケーションが発表され、この技術の深い知識がどんなウェブ開発者にとっても重要なスキルだということが示されました。

この言語の歴史について理解することから始めるのが役立つでしょう。JavaScript は 1995 年、Netscape の技術者 Brendan Eich によって創られ、1996 年の初めに Netscape 2 で初めてリリースされました。当初は LiveScript と呼ばれる予定でしたが、Sun Microsystems の Java 言語の人気にあやかろうという不運なるマーケティング上の決定により改名されました 2 つの言語が共通点をほとんど持っていないにもかかわらず。それ以来、このことは未だに混同の元となっています。

Microsoft はその数か月後、IE 3 で JScript というほとんど互換性のある言語をリリースしました。Netscape はこの言語をヨーロッパの標準化団体 Ecma International に提出し、その結果 1997 年に ECMAScript という標準の第 1 版が生まれました。この標準は重要なアップデートを受けて 1999 年に ECMAScript 第 3 版となり、その後現在までほとんど安定していますが、第 4 版が現在作成中です。

この安定性は開発者にとって、様々な実装系が標準に追い付くのに十分な時間が与えられたという意味でうれしいニュースです。私はほとんどもっぱら第 3 版の方言に焦点を当てていきます。ただ、なじみの深さから JavaScript という語は最後まで使い続けるつもりです。

大部分のプログラミング言語と違って、JavaScript という言語には入出力の概念がありません。この言語はあるホスト環境でのスクリプト言語として実行されるよう設計されており、外部の世界とコミュニケーションするための機構はそのホスト環境が提供するものとしているのです。もっとも一般的なホスト環境はブラウザですが、JavaScript のインタプリタは Adobe Acrobat や Photoshop、Yahoo! ウィジェットエンジンなどでも見つけることができます。

[編集] 概要

まずはあらゆる言語の構成要素、「型」を見ることから始めましょう。JavaScript のプログラムは値を操作し、それらの値はすべてある型に属しています。JavaScript の型は:

…ああ、あと Undefined と Null、これらはちょっと変わっています。そして Array(配列)、これは特殊なオブジェクトの一種。さらに DateRegExp(正規表現)、これらは自由に使えるオブジェクトです。あと技術的に正確なことを言うと、関数は単に特殊なオブジェクトの一種です。したがってこの型の図はより正確にはこうなります:

  • 数値
  • 文字列
  • 真偽値
  • オブジェクト
    • 関数
    • 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

足し算、引き算、モジュロ(剰余)など、標準的な算術演算がサポートされています。さらに、これは先ほど言及し忘れたのですが、より高度な数学関数や定数を扱うMath という組み込みオブジェクトもあります。

Math.sin(3.5);
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

もし文字列が数でない場合、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

[編集] 文字列

JavaScript における文字列は、文字の連なったもの (sequences) です。より正確に言えば、文字列は Unicode 文字の連なったものであり、つまりそれぞれの文字は 16 ビットの整数で表されます。これは国際化 (internationalisation) に対処しなければならない誰もが歓迎するニュースでしょう。

もし単一文字を表したいなら、単純に長さ 1 の文字列を使います。

文字列の長さを知るには、その文字列の length プロパティにアクセスしましょう:

> "hello".length
5

JavaScript のオブジェクトとの初めての接触です! 私、文字列もまたオブジェクトだと述べましたでしょうか? 文字列はまた、メソッドも持っています:

> "hello".charAt(0)
h
> "hello, world".replace("hello", "goodbye")
goodbye, world
> "hello".toUpperCase()
HELLO

[編集] その他の型

JavaScript は nullundefined を区別します。null は、意図的に「値がない」ということを指し示す object 型のオブジェクトです。対して undefined とは、初期化されていない値 ― すなわち、「まだ値が代入されていない」ということを指し示す undefined 型のオブジェクトです。変数については後で話しますが、JavaScript では値を代入しないで変数を宣言することができるのです。そのようにした場合、その変数の型は undefined です。

JavaScript は truefalse(これらはともにキーワードです)を取りうる値とする真偽値型を持っています。どんな値でも以下のルールに基づいて真偽値に変換できます:

  1. false0、空文字 ("")、NaNnull、および undefined は、すべて false になる
  2. その他の値はすべて 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 になります。

[編集] 演算子

JavaScript の算術演算子は、+-*/、そして剰余演算子の % です。値は = を使って代入されます。また +=-= のような複合代入文もあります。これらは x = x operator 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 言語ファミリーの他の言語とよく似た制御構造セットを持っています。条件文は ifelse でサポートされています。必要ならこれらを連鎖させることもできます:

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) {
  // 無限ループ!
}

do {
  var 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 つ目はオブジェクトリテラル構文と呼ばれ、こちらの方がより便利です。オブジェクトリテラル構文はこの言語の非常に初期のバージョンでは存在しませんでした。多くのコードで古い方法を使っているのを見かけるのはこのためです。

一度作ってしまえば、オブジェクトのプロパティには2つの方法のいずれかで再びアクセスすることができます:

obj.name = "Simon"
var name = obj.name;

そして…

obj["name"] = "Simon";
var name = obj["name"];

これらもまた意味的に等価です。2 つ目の方法には、プロパティの名前を文字列として与えるため、名前を実行時に計算できるという利点があります。この方法はまた、プロパティを設定したりや取得したりするのに予約語である名前を使うことができます。

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]; i++) {
    // item について何かする
}

ここでは 2 つの変数を準備しています。for ループの中央部の代入文は、同時にそれが真であるか検査しています ― もしこれが真なら、ループは継続します。i は毎回インクリメントされるので、配列の要素は順番に item に代入されます。ループは偽な要素が見つかったとき (例えば undefined) 止まります。

Note that this trick should only be used for arrays which you know do not contain "falsy" values (arrays of objects or DOM nodes for example). If you are iterating over numeric data that might include a 0 or string data that might include the empty string you should use the i, j idiom instead.

Another way to iterate is to use the for...in loop. Note that if someone added new properties to Array.prototype, they will also be iterated over by this loop:

for (var i in a) {
  // Do something with a[i]
}

If you want to append an item to an array, the safest way to do it is like this:

a[a.length] = item;                 // same as a.push(item);

Since a.length is one more than the highest index, you can be assured that you are assigning to an empty position at the end of the array.

Arrays come with a number of methods:

a.toString(), a.toLocaleString(), a.concat(item, ..), a.join(sep),
a.pop(), a.push(item, ..), a.reverse(), a.shift(), a.slice(start, end),
a.sort(cmpfn), a.splice(start, delcount, [item]..), a.unshift([item]..)
  • concat returns a new array with the items added on to it.
  • pop removes and returns the last item
  • push adds one or more items to the end (like our ar[ar.length] idiom)
  • slice returns a sub-array
  • sort takes an optional comparison function
  • splice lets you modify an array by deleting a section and replacing it with more items
  • unshift prepends items to the start of the array

[編集] 関数

Along with objects, functions are the core component in understanding JavaScript. The most basic function couldn't be much simpler:

function add(x, y) {
    var total = x + y;
    return total;
}

This demonstrates everything there is to know about basic functions. A JavaScript function can take 0 or more named parameters. The function body can contain as many statements as you like, and can declare its own variables which are local to that function. The return statement can be used to return a value at any time, terminating the function. If no return statement is used (or an empty return with no value), JavaScript returns undefined.

The named parameters turn out to be more like guidelines than anything else. You can call a function without passing the parameters it expects, in which case they will be set to undefined.

> add()
NaN // You can't perform addition on undefined

You can also pass in more arguments than the function is expecting:

> add(2, 3, 4)
5 // added the first two; 4 was ignored

That may seem a little silly, but functions have access to an additional variable inside their body called arguments, which is an array-like object holding all of the values passed to the function. Let's re-write the add function to take as many values as we want:

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

That's really not any more useful than writing 2 + 3 + 4 + 5 though. Let's create an averaging function:

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

This is pretty useful, but introduces a new problem. The avg() function takes a comma separated list of arguments - but what if you want to find the average of an array? You could just rewrite the function as follows:

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

But it would be nice to be able to reuse the function that we've already created. Luckily, JavaScript lets you call a function and call it with an arbitrary array of arguments, using the apply() method of any function object.

> avg.apply(null, [2, 3, 4, 5])
3.5

The second argument to apply() is the array to use as arguments; the first will be discussed later on. This emphasizes the fact that functions are objects too.

JavaScript lets you create 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;
}

This is semantically equivalent to the function avg() form. It's extremely powerful, as it lets you put a full function definition anywhere that you would normally put an expression. This enables all sorts of clever tricks. Here's a way of "hiding" some local variables - like block scope in C:

> var a = 1;
> var b = 2;
> (function() {
    var b = 3;
    a += b;
})();
> a
4
> b
2

JavaScript allows you to call functions recursively. This is particularly useful for dealing with tree structures, such as you get in the browser 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;
}

This highlights a potential problem with anonymous functions: how do you call them recursively if they don't have a name? The answer lies with the arguments object, which in addition to acting as a list of arguments also provides a property called arguments.callee. This always refers to the current function, and hence can be used to make recursive calls:

var charsInBody = (function(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 += arguments.callee(child);
    }
    return count;
})(document.body);

Since arguments.callee is the current function, and all functions are objects, you can use arguments.callee to save information across multiple calls to the same function. Here's a function that remembers how many times it has been called:

function counter() {
    if (!arguments.callee.count) {
        arguments.callee.count = 0;
    }
    return arguments.callee.count++;
}

> counter()
0
> counter()
1
> counter()
2

[編集] カスタムオブジェクト

In classic Object Oriented Programming, objects are collections of data and methods that operate on that data. Let's consider a person object with first and last name fields. There are two ways in which their name might be displayed: as "first last" or as "last, first". Using the functions and objects that we've discussed previously, here's one way of doing it:

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

This works, but it's pretty ugly. You end up with dozens of functions in your global namespace. What we really need is a way to attach a function to an object. Since functions are objects, this is easy:

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

There's something here we haven't seen before: the 'this' keyword. Used inside a function, 'this' refers to the current object. What that actually means is specified by the way in which you called that function. If you called it using dot notation or bracket notation on an object, that object becomes 'this'. If dot notation wasn't used for the call, 'this' refers to the global object. This is a frequent cause of mistakes. For example:

> s = makePerson("Simon", "Willison")
> var fullName = s.fullName;
> fullName()
undefined undefined

When we call fullName(), 'this' is bound to the global object. Since there are no global variables called first or last we get undefined for each one.

We can take advantage of the 'this' keyword to improve our makePerson function:

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");

We've introduced another keyword: 'new'. new is strongly related to 'this'. What it does is it creates a brand new empty object, and then calls the function specified, with 'this' set to that new object. Functions that are designed to be called by 'new' are called constructor functions. Common practise is to capitalise these functions as a reminder to call them with new.

Our person objects are getting better, but there are still some ugly edges to them. Every time we create a person object we are creating two brand new function objects within it - wouldn't it be better if this code was shared?

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;
}

That's better: we are creating the method functions only once, and assigning references to them inside the constructor. Can we do any better than that? The answer is 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 is an object shared by all instances of Person. It forms part of a lookup chain (that has a special name, "prototype chain"): any time you attempt to access a property of Person that isn't set, JavaScript will check Person.prototype to see if that property exists there instead. As a result, anything assigned to Person.prototype becomes available to all instances of that constructor via the this object.

This is an incredibly powerful tool. JavaScript lets you modify something's prototype at any time in your program, which means you can add extra methods to existing objects at runtime:

> 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

Interestingly, you can also add things to the prototype of built-in JavaScript objects. Let's add a method to String that returns that string in reverse:

> 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

Our new method even works on string literals!

> "This can now be reversed".reversed()
desrever eb won nac sihT

As I mentioned before, the prototype forms part of a chain. The root of that chain is Object.prototype, whose methods include toString() - it is this method that is called when you try to represent an object as a string. This is useful for debugging our Person objects:

> var s = new Person("Simon", "Willison");
> s
[object Object]
> Person.prototype.toString = function() {
    return '<Person: ' + this.fullName() + '>';
}
> s
<Person: Simon Willison>

Remember how avg.apply() had a null first argument? We can revisit that now. The first argument to apply() is the object that should be treated as 'this'. For example, here's a trivial implementation of 'new':

function trivialNew(constructor) {
    var o = {}; // Create an object
    constructor.apply(o, arguments);
    return o;
}

This isn't an exact replica of new as it doesn't set up the prototype chain. apply() is difficult to illustrate - it's not something you use very often, but it's useful to know about.

apply() has a sister function named call, which again lets you set 'this' but takes an expanded argument list as opposed to an array.

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 function declarations are allowed inside other functions. We've seen this once before, with an earlier makePerson() function. An important detail of nested functions in JavaScript is that they can access variables in their parent function's scope:

function betterExampleNeeded() {
    var a = 1;
    function oneMoreThanA() {
        return a + 1;
    }
    return oneMoreThanA();
}

This provides a great deal of utility in writing more maintainable code. If a function relies on one or two other functions that are not useful to any other part of your code, you can nest those utility functions inside the function that will be called from elsewhere. This keeps the number of functions that are in the global scope down, which is always a good thing.

This is also a great counter to the lure of global variables. When writing complex code it is often tempting to use global variables to share values between multiple functions - which leads to code that is hard to maintain. Nested functions can share variables in their parent, so you can use that mechanism to couple functions together when it makes sense without polluting your global namespace - 'local globals' if you like. This technique should be used with caution, but it's a useful ability to have.

[編集] クロージャ

This leads us to one of the most powerful abstractions that JavaScript has to offer - but also the most potentially confusing. What does this do?

function makeAdder(a) {
    return function(b) {
        return a + b;
    }
}
x = makeAdder(5);
y = makeAdder(20);
x(6)
?
y(7)
?

The name of the makeAdder function should give it away: it creates new 'adder' functions, which when called with one argument add it to the argument that they were created with.

What's happening here is pretty much the same as was happening with the inner functions earlier on: a function defined inside another function has access to the outer function's variables. The only difference here is that the outer function has returned, and hence common sense would seem to dictate that its local variables no longer exist. But they do still exist - otherwise the adder functions would be unable to work. What's more, there are two different "copies" of makeAdder's local variables - one in which a is 5 and one in which a is 20. So the result of those function calls is as follows:

x(6) // returns 11
y(7) // returns 27

Here's what's actually happening. Whenever JavaScript executes a function, a 'scope' object is created to hold the local variables created within that function. It is initialised with any variables passed in as function parameters. This is similar to the global object that all global variables and functions live in, but with a couple of important differences: firstly, a brand new scope object is created every time a function starts executing, and secondly, unlike the global object (which in browsers is accessible as window) these scope objects cannot be directly accessed from your JavaScript code. There is no mechanism for iterating over the properties of the current scope object for example.

So when makeAdder is called, a scope object is created with one property: a, which is the argument passed to the makeAdder function. makeAdder then returns a newly created function. Normally JavaScript's garbage collector would clean up the scope object created for makeAdder at this point, but the returned function maintains a reference back to that scope object. As a result, the scope object will not be garbage collected until there are no more references to the function object that makeAdder returned.

Scope objects form a chain called the scope chain, similar to the prototype chain used by JavaScript's object system.

A closure is the combination of a function and the scope object in which it was created.

Closures let you save state - as such, they can often be used in place of objects.

[編集] メモリリーク

An unfortunate side effect of closures is that they make it trivially easy to leak memory in Internet Explorer. JavaScript is a garbage collected language - objects are allocated memory upon their creation and that memory is reclaimed by the browser when no references to an object remain. Objects provided by the host environment are handled by that environment.

Browser hosts need to manage a large number of objects representing the HTML page being presented - the objects of the DOM. It is up to the browser to manage the allocation and recovery of these.

Internet Explorer uses its own garbage collection scheme for this, separate from the mechanism used by JavaScript. It is the interaction between the two that can cause memory leaks.

A memory leak in IE occurs any time a circular reference is formed between a JavaScript object and a native object. Consider the following:

function leakMemory() {
    var el = document.getElementById('el');
    var o = { 'el': el };
    el.o = o;
}

The circular reference formed above creates a memory leak; IE will not free the memory used by el and o until the browser is completely restarted.

The above case is likely to go unnoticed; memory leaks only become a real concern in long running applications or applications that leak large amounts of memory due to large data structures or leak patterns within loops.

Leaks are rarely this obvious - often the leaked data structure can have many layers of references, obscuring the circular reference.

Closures make it easy to create a memory leak without meaning to. Consider this:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

The above code sets up the element to turn red when it is clicked. It also creates a memory leak. Why? Because the reference to el is inadvertently caught in the closure created for the anonymous inner function. This creates a circular reference between a JavaScript object (the function) and a native object (el).

There are a number of workarounds for this problem. The simplest is this:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
    el = null;
}

This works by breaking the circular reference.

Surprisingly, one trick for breaking circular references introduced by a closure is to add another closure:

function addHandler() {
    var clickHandler = function() {
        this.style.backgroundColor = 'red';
    }
    (function() {
        var el = document.getElementById('el');
        el.onclick = clickHandler;
    })();
}

The inner function is executed straight away, and hides its contents from the closure created with clickHandler.

Another good trick for avoiding closures is breaking circular references during the window.onunload event. Many event libraries will do this for you. Note that doing so disables bfcache in Firefox 1.5, so you should not register an unload listener in Firefox, unless you have other reasons to do so.

[編集] 原文情報

  • 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.