Hatena::ブログ(Diary)

風と宇宙とプログラム このページをアンテナに追加 RSSフィード

2009-10-04

FAQ形式によるJavaScriptの本質がわかる超入門

はじめに

JavaScriptは簡単な言語のようでいて、実は奥が深く、初心者にとってなかなかその本質がわかりにくい言語です。ここでは、JavaScriptの言語的エッセンスを理解できるようなものをFAQ形式で書いてみました。ご意見や誤り等を指摘してもらえたら嬉しいです。

なお、JavaScript標準化であるECMAScriptは、今年末にEdition 5 がリリースされる予定です。このFAQは、現在のバージョンであるEdition 3をベースにしています。

ECMAScriptって何ですか?

ECMAはEuropean Computer Manufacturer Association(欧州電子計算機工業会)の略で、標準化団体です。NetscapeJavaScriptMicrosoftJScriptをベースに、純粋なプログラム言語部分を抽出したものをECMA標準化したスクリプト言語です。1998年に制定されたECMA262 3rd Edtionが現在の仕様となっています。

ECMAScriptJScriptJavaScriptと何がちがうの?

MicrosoftJScriptやJavaScrpt(ここではFirefoxMozillaJavaScript)はブラウザに組み込まれたスクリプト言語としてDOMCSS関連のAPIが提供されていますが、ECMAScriptではこれらのAPIは規定していなく、いくつかの汎用クラスのみが定義されている純粋なプログラミング言語です。JScriptJavaScriptECMAScriptのスーパーセットという位置づけですが、ECMAScriptで規定されているAPI部分に対しても若干の動作の違いがあります。また、Flashスクリプト言語であるActionScriptECMAScriptをベースに独自に拡張した言語になっています。

ECMAScriptで定義されている予約語を教えてください。

ECMAScript Edtion 3 では以下のものがキーワードとして定義されています。

break else new var case finally return void
catch for switch while continue function this with
default if throw delete in try do instanceof typeof

さらに、以下のものが将来のための予約語として定義されています。

abstract enum int short boolean export interface static
byte extends long super char final native synchronized
class float package throws const goto private transient
debugger implements protected volatile double import public

ここで、true, false, null, undefined, NaN, Infinityなどはキーワードでも予約語でもないことに注意してください。true, false, nullはリテラル予約語ですが、undefined, NaN, Infinityは単なるグローバル変数です。リテラルには値を代入することはできませんが、変数には代入できます。

true = 0;            // syntax error;
NaN = 1;             // OK
undefined = "hello"  // OK

NaNって何ですか?

NaNはNot a Numberの略で、IEEE754で定義されている数値表現の一種です。JavaScriptでは数値は64bit倍精度の浮動小数表現を用いています。全てのNaNはお互いに区別できないため、ある変数(x)の値がNaNかどうかを比較するのに x == NaN ではダメです。isNaN(x) というグローバル関数がありますので、isNaN(x)を必ず使うようにしましょう。NaN == NaNもfalseになります。

その他に、IEEE754で定義されている通常の数値以外のものはInfinity/-Infinity があります。通常の数値かどうかの判定にはisFinite(x)というグローバル関数がありますので、それを利用してください。この関数引数xが NaN, Infinity, -Infinity以外のときtrueを返します。

一方、-0というものもあります。ゼロにも符号があります。-0が意味のある使い方をする場合はほとんどありませんが、下記のコードではxにはInfinityが代入されますが、y には-Infinityが代入されます。

x = 1 / 0;
y = 1 / -0;

undefined やNaNの値を変えられるなんて非常に問題じゃありませんか?

はい。問題となる場合があるのは確かです。実は、これらの変数の他に、規格書で定義されている組込関数などもユーザプログラムによって変更することができてしまいます。そのような言語仕様ということで理解してください。逆に、これにより非常に動的で柔軟なプログラミングが可能となります。

ちなみに、undefiend, NaN, Infinityはその変数の値が変更されても以下の式で必ず元の定義された値を得ることができます。

my_undefined = void 0;
my_NaN = 0 / 0;
my_Infinity = 1 / 0;

また、Numberオブジェクトには、ReadOnlyのプロパティとしてNumber.NaN,Number.POSITIVE_INIFINITY, Number.NEGATIVE_INFINITYなどが定義されています。

プロパティのattributeにはReadOnly以外に何がありますか?

ReadOnly, DontDelete, DontEnumの3種類あります。ReadOnlyは代入等でプロパティの値を変更できないこと、DontDeleteはプロパティ自身を削除できないこと、DontEnumはfor-in構文で列挙できないことを意味します。これらはビルドインオブジェクトに対して定義されており、ユーザが定義したオブジェクトプロパティにはをつけることはできません。

thisの意味がよくわからないんですが。C++Javaのthisと何が違うの?

C++Javaのthisとだいたい同じですが、通常の関数の中やどの関数にも属さない関数の外のコードでもthisを使うことができます。この場合、thisにはGlobalオブジェクトがバインドされています。JavaScriptではthisには常に何らかのオブジェクトがバインドされています。thisがnullになることはありません。

グローバルコードってどういう意味ですか?

トップレベルの関数の外で実行されるコードのことです。トップレベルには関数とそれ以外の代入文やif文など一般の文を書くことができます。この関数以外のコードをグローバルコードと呼びます。プログラムを最初に実行するときには、関数定義を読み込んだ後に、このグローバルコードが最初に実行されます。

グローバルオブジェクトって何ですか?

JavaScriptでは全てのデータは何かのオブジェクトプロパティの値になっていますが、グローバル関数グローバル変数などが属するオブジェクトがグローバルオブジェクトです。ECMA262ではグローバルオブジェクトには特定のシンボルが割り当てられていません。グローバルスコープではthisがグローバルオブジェクトになるので、グローバルコードの先頭で、

var Global = this;

などとやると、ローカル変数等で上書きされない限り、任意のスコープでGlobalでグローバルオブジェクトを参照できるようになります。

ちなみに、Webブラウザではwindow変数がグローバルオブジェクトを参照するようになっています。

ところで、オブジェクトって何ですか?

JavaScriptの場合、プロパティと値の組の集合オブジェクトと言うことができます。プロパティの値には、数値や文字列オブジェクト関数など何でも値にすることができます。概念的には、グローバル変数、ローカル変数関数の実引数関数自身、内部関数など全ての対象は何らかのオブジェクトプロパティの値として定義されます。

関数を定義する前にその関数をコールできるの?

はい、できます。下記のコードは問題なく実行され、vには246が代入されます。

v = myfunc(123);
function myfunc (x) {
  return x * 2;
}

同じ名前の関数を複数回定義したらどうなるの?

最後に定義された関数が有効になります。下のプログラムを実行すると

myfunc();
function myfunc() {
  print("Hello");
}
myfunc();
function myfunc() {
  print("World");
}
myfunc();

以下のようにWorldが3回表示されます。

World
World
World

変数は宣言しなくても使えるの?

はい、使えます。ただし、宣言した場合としない場合とでは動作が異なります関数の中で宣言した変数は、一般にはその関数の中だけで有効ですが、宣言しない場合には、グローバル変数と同様の意味になります。内部関数の場合は、これとは異なりますので、別の項目で説明します。

グローバル変数は宣言してもしなくても同じなの?

ちょっと違います。varで宣言したグローバル変数は削除できませんが、宣言しないグローバル変数は deleteで削除できます。

より正確にはグローバルオブジェクトからその変数名を文字列としてもつプロパティを削除します。

また、var宣言した変数は初期値がなければ、その値はundefinedで初期化されますが、var宣言されていない変数は代入されるまで変数自体が存在しません。したがって、 その変数の値を参照しようとするとReferenceError例外が発生します。

変数宣言はどこですればよいの?

どこで宣言しても構いません。変数を宣言する前にその変数を使うこともできます。

x = v;
var v = 123;

変数xにvの値を代入していますが、v は後で宣言されています(そして123で初期化しています)。xには何が代入されるのでしょうか?この場合、undefinedが代入されます。これは、

var v;
x = v;
v = 123;

と同じ意味になるからです。一方、vを変数宣言しないで

x = v;
v = 123

とすると、x = vのところでvを参照する際に、ReferenceError例外が発生します。

一般には、C++のように使う前に(初期化するとき)宣言するのがよいと思いますが、変数のスコープは関数全体に亘るため、関数の先頭でまとめて宣言するのがよいとの意見もあります。

複数の値を返す関数をつくるにはどうするの?ポインタがないので引数のOUTパラメータで返す方法がないのですが。

配列オブジェクトを返すようにすれば容易に実現できます。

配列の場合には、下記のようにArrayリテラルを使えば、スマートに書けます。

function myfunc(x, y) {
  return [x + y, x - y];
}

値の数が多い場合などは、オブジェクトを返す方が分りやすいでしょう。

function myfunc () {
  return { x:100, y:100, width: 300, height: 400 };
}

どちらの方法も、オブジェクトが生成されるので、効率が問題のときは、引数オブジェクトを渡して、呼ばれた側でプロパティにセットするなどの方法が必要でしょう。

switch文はC/C++Javaswitchと同じですか?

ちょっと違いますC/C++Javaではcaseのところにはコンパイル時に値を決定できる数値定数しか記述できませんが、JavaScriptでは任意の式を記述できます。その式を実行した結果の値とswitchで指定した値がStrictEquals(===)のときcase本体が実行されます。それ以外はC/C++Javaと同じです。

==と===の違いは何ですか?

JavaScriptでは2項比較オペレータの両者の型が異なるとき、強制的に型を合わせる機能があります。==は型を合わせ、===では型を合わせることはしません。

true == 1                  // true
true === 1                 // false
123 == new String("123")   // true
123 === new String("123")  // false
null == undefined          // true
null === undefined         // false

オブジェクト同士の等価性判定はどうすればよいのですか?

オブジェクト同士は==と===は全く同じ結果になりますが、これがtrueになるのは、同一のオブジェクト、またはjoinされたオブジェクトに限ります。同一のオブジェクトとは、実体が同じオブジェクトです。アドレスが等しいオブジェクトと解釈してもよいでしょう。joinされたオブジェクトとは、実体が等しいかどうかは問わず、一方のオブジェクトプロパティを変更したら、もう一方のオブジェクトの対応するプロパティも変更されるような関係をもつオブジェクトです。ライブコピーのようなイメージですが、joinedというのは実装の最適化が可能になるように==の条件を緩めるために導入されました。

内部関数関数式はそれが実行される度に、新たな関数オブジェクトが生成されますが、クロージャが生成されない場合には、関数オブジェクトの新たな生成は無駄になります。つまり、同一の関数オブジェクトを共有した方が効率的です。この場合、新たな関数オブジェクトを生成するという仕様と矛盾してしまいますが、join された関数オブジェクトと解釈すれば、矛盾は解消されます。ちょっと、こじつけたような感じですが、規格の中で最適化の実装を考慮した部分です。

文字列をBooleanに変換したり、いろいろな型変換が自動的に行われるようだけど、どういった規則があるの?

主な型変換には以下のものがあります。これ以外にも、ToInteger, ToInt32,ToUint32, ToUint16があります。C/C++のキャストとは異なりますので、注意が必要です。

  • ToBoolean

Undefined false
Null false
Boolean そのまま
Number 0,-0, NaNのときfalse, それ以外は true
String 空文字("")のときfalse, それ以外は true
Object true

  • ToNumber

Undefined NaN
Null +0
Boolean trueのとき1, falseのとき+0
Number そのまま
String parseFloat()の結果の数値
Object 基本型に変換した結果に対して、ToNumberを適用する

  • ToString

Undefined "undefined"
Null "null"
Boolean trueのとき"true", falseのとき"false"
Number (複雑なので省略)
String そのまま
Object 基本型に変換した結果に対して、ToStringを適用する

  • ToObject

Undefined TypeError
Null TypeError
Boolean new Boolean(value)
Number new Number(value)
String new String(value)
Object そのまま

&, |, >> などのビット演算引数をToUint32に変換してからビット操作を行うので、任意の数値を強制的に32ビット整数に変換するときなどに利用できます。

var x = 1234.5678 | 0     // 1234がxに代入される

同様に、単項演算子+はオペランドをToNumberしますので、数値か文字列か不明なとき数値に変換する手法としてしばしば用いられます。

var x = ....;
var y = +x;        // y は強制的にNumber型になる

これは、

var y = Number(x);

と等価です。+x は記述量が少ないため好んで用いる人もいますが、Number()関数を使ったほうが、明示的でわかりやすいでしょう。

goto文はないの?

ありません。ただし、gotoは将来の予約語として定義されていますので、変数名などには使えません。

FirefoxIEなどの殆どのブラウザでは将来のための予約語として定義されてるシ ンボルのなかで、実際には使われていないものは、変数として使えますが、使わない 方がよいでしょう。

多重ループの内側から外に抜けるにはどうするの?

try-catch throwを使う方法もありますが、一般にはラベルを使います。forやwhileなどの前にラベルをつけると、breakやcontinueの際にそのラベルを指定することができます。Javaと同じです。

loop:
for (var i = 0; i < 100; i++) {
  for (var j= 0; j < 200; j++) {
    for (var k = 0; k < 300; k++) {
      if (k == 100) {
        break loop; // loopというラベルのついた一番外のfor文を抜ける。
      }
    }
  }
}

単純にgotoに相当することはできないの?エラー処理などでgotoが便利なときがあるのですが。

この場合、ラベル付きブロック文を利用すれば実現できます。 JavaScriptラベルつきブロック文があることを知らないプログラマも多いようですが、知っておくと役に立つでしょう。

 aaa;
 if (bbb) goto Exit;
 ccc;
 if (ddd) goto Exit;
 return OK;
Exit:
 ....
 return FAIL;

というコードに相当するには、以下のコードで実現できます。

Exit: {
  aaa;
  if (bbb) break Exit;
  ccc;
  if (ddd) break Exit;
  return OK;
}
...
return FAIL;

break Exitでブロック文から抜けることで、return FAILが実行されます。何事もなければブロックの最後の return OKが実行されます。ラベルつきブロック文は入れ子にすることができます。

Exceptionとして投げることができる値の条件はありますか?

いいえ、全ての対象をExceptionとして投げることができます。

throw new Object();
throw 123;
throw "hello";
throw function () { return 321; };
throw undefined;
throw NaN;
throw null;

finallyの使い方を教えて。

tryブロックから抜けるとき必ず実行される文をfinallに記述することができます。openしたら必ずcloseするようなパターンのとき、下記のように書けます。

for (....) {
  try {
    open();
    if (aaa) return;
     ...
    if (bbb) throw Error();
    if (ccc) continue;
    if (ddd) break;
  }  finally {
    close();
  }
}

try中でreturnしたり、例外をthrowするような場合でも、必ずfinallyブロックが実行されます。上記のようにfor文などの中にある場合には、breakやcontinueを実行した後にもfinallyが実行されます。try-catch-finallyとcatchブロックも同時に使うことができます。

finally中で明示的にreturnするとtry中で発生した例外が無効になります。

function func1() {
  var x = 0;
  try {
    throw Error();
  } finally {
    x = 321;
  }
  return x;
}
  
function func2() {
  var x = 0;
  try {
    throw Error();
  } finally {
    x = 321;
    return x;
  }
}

func1()を実行するとError例外が投げられますが、func2()は321が返値として返されます。基本的にJavaと同じです。

tryにcatchとfinallyを両方記述したらどうなるの?

try中で例外が発生した場合、catch部が実行された後に、finally部が実行されます。catch部でさらに別の例外が発生しても、finallyは実行されます。要するに、finallyがあると、どんなことが起こってもfinllayが実行されることが保障されます。

下記のプログラムを実行すると

function test() {
  try {
    throw "hello";
  } catch (ex) {
    print("ex=" + ex);
    throw "world";
  } finally {
    print("finally");
  }
}
  
try {
  test();
} catch (ex) {
  print(">>> ex=" + ex);
}

と以下のように表示されます。

ex=hello
finally
>>> ex=world

C++Javaでは複数のcatchで場合分けできるけど、JavaScriptはできないの?

JavaScript変数に型がないので、複数のcatchを記述することはできません。場合わけするとしたら、catch部の中でinstanceofオペレータなどを利用するとよいでしょう。

try {
  ...
} catch (ex) {
  if (ex instanceof TypeError) {
    ...
  } else if (ex instanceof Number) {
    ...
  } else if (ex instanceof Object) {
    ...
  }
  ....
}

ちなみに、FirefoxJavaScriptでは下記のようにcatchを複数記述することができます。

try {
  ...
} catch (ex if ex instanceof TypeError) {
  ...
} catch (ex if ex instanceof Number) {
  ...
} catch (ex if ex instanceof Object) {
  ...
} catch (ex) {
  ...
}

名前空間はないの?

ありません。しかし、名前空間に相当するものは簡単に表現できます。

var myns = {
  Foo: function () {...}
};

myns.Foo.prototype = {
  func: function () {...},
  func2: function () { ...}
};

var v = new myns.Foo();
v.func();
v.func2();

Ajaxなどのライブラリとして流通しているソフトでは、このようは方法で名前衝突を避けています。

ユーザ定義のオブジェクトはどうやって生成するの?

JavaScriptにはクラスというものはありませんが、関数をnewするとオブジェクトが生成されます。

function myfunc() {
}

v = new myfunc();

vはオブジェクトです。プロパティを設定し値をつけることができます。

v.abc = 123;

関数関数としてコールする以外に newができるってちょっとわかり難いです。

確かに最初は混乱します。JavaScriptでは関数オブジェクトです。オブジェクトにはプロパティと値がありますが、関数の実行コードは、実はあるプロパティの値になっています。newされるときに実行されるものは、[[Consturctor]]という内部の(仮想的な)プロパティの値に設定されているコードです。通常の関数コールの際には、[[Call]]というプロパティに設定されているコードが実行されます。この両者のコードはユーザが定義した関数本体のコードに一致しますが、コンテキストが異なります。

function myfunc(x, y) {
  this.v = x;
  this.w = y;
  return x + y;
}

a = myfunc(3, 4);

を実行すると、aには7が代入されます。this.v, this.w に代入したものはどうなるのでしょう?この場合は、thisにはGlobalオブジェクトが代入されているため、vとwというグローバル変数にx,yの値が代入されます。

print(v);    // 3
print(w);    // 4

一方、

b = new myfunc(3, 4);

を実行すると、まず、オブジェクトを生成して、そのオブジェクトに対して関数本体が実行されます。このとき、thisにはそのオブジェクトが代入されています。

print(b.v);    // 3
print(b.w);    // 4

この場合、myfunc()のreturn値は無視されます。

nullはオブジェクトですか?

いいえ、プロパティを持たないという点でオブジェクトではありません。また、

var v = null instnaceof Object;

の結果もfalseです。しかし、typeofオペレータではobjectが返されます。仕様がそうなっているのですが、これは仕様のバグといってもよいかも知れません。

数値を文字列に、逆に文字を数値に変換するにはどうすればよいの?

数値を文字列に変換するには、いくつかの方法があります。

var x = 123;
var s1 = String(x);
var s2 = x.toString();
var s3 = "" + x;
var s4 = x + "";

s1の方法がよいでしょう。s3,s4はよく見かけますが、見た目が不自然ですし、効率が多少落る場合もあります。

文字列を数値に変換するには、

var x = "321";
var n1 = Number(x);
var n2 = parseInt(x);
var n3 = parseFloat(x);
var n4 = x - 0;
var n5 = +x;

などの方法がありますが、それぞれ意味が異なります(n1とn4,n5はほぼ同じ)。

関連エントリーJavaScriptで ””+x を文字列変換に使うのは気持ち悪い」をご覧ください。

Arrayをnewしないで関数のように使っているプログラムを見ましたが、問題ないのですか?

var x = new Array(1,2,3);

var x = Array(1,2,3);

ですが、Arrayはコンストラクタ関数コールが同じであり、上の文は全く同等です。コンストラクタ関数が同じものは、Array以外に、Function, RegExp, Error(とそのサブのTypeError, EvalError, ReferenceError, URIError)があります。

もっとも、Arrayの場合は、Arrayリテラルを用いた方がわかりやすいです。Arrayリテラルは下記のように[]中にコンマで要素を列挙します。

var v = [1,2,3];

特に、要素が1つであり、それが数値の場合には、Array()関数コンストラクタでは生成する方法がありません。

var v = new Array(5);

v[0]が5の配列ではなく、長さが5の配列を生成することを意味します。v[0]が5の長さ1の配列をArrayリテラルならば

var v = [5];

と記述することができます。

また、Arrayリテラルを用いると、途中の要素を省略したり(この場合はundefinedで初期化)最後のコンマを無視するなどの機能があります。

v = ["a","b",,,"f","g",]

v = ["a","b",undefined,undefined,"f","g"]

と等価です。

Arrayのようにユーザ定義関数でnew しても関数コールしても同じ動作をするものは作れますか?

ユーザ定義関数に対して new すると、その関数戻り値オブジェクトの場合、newの結果はそのオブジェクトになります。

function MyTest () {
  return {
    name: "hello",
    value: 123
  };
}

var v = new MyTest();
var w = MyTest();

vとwはどちらもMyTest()の戻り値であるオブジェクトが代入されます。特に、コード上は v は MyTestをnewしたインスタンスのように見えますが、v instanceof MyTestは falseであることに注意してください。

配列によるアクセス([])はArrayでなくてもできますが、Arrayは何が違うの?

確かに、通常のオブジェクトを生成して配列のように使うことができます。

var v1 = new Object();
v1[0] = "hello";
v1[1] = "world";

Arrayを使っても見た目は同じです。

var v2 = new Array();
v2[0] = "hello";
v2[1] = "world";

しかし、Arrayには要素数を示すlengthプロパティがあります。lengthには値をセットすることができ、セットするとArrayの要素数がセットした値に変更されます。したがって、v2.length = 1; を実行すると要素数をはみ出したv2[1]はundefinedになります。一方、 v1.length = 1;を実行してもv1[1]は"world"のままです。

配列でない普通のオブジェクトがどうして[]で配列アクセスできるのですか?

オブジェクトプロパティと値の集合ですが、配列インデックスプロパティだからです。一般に、インデックスが数値でない配列は特に連想配列と呼ばれていますが、オブジェクトはまさに連想配列なのです。

v.myprop = "hello";

v["myprop"] = "hello";

は全く同じです。数値は識別子ではないのでプロパティ名にはならないためで、配列アクセス表現のみで記述することができます。

prototypeプロパティの意味がよくわかりません。

一般のオブジェクト指向言語では、クラスの階層はsuper-subの関係がありますが、JavaScrptではそれに対応するものとしてprototypeチェーンがあります。あるオブジェクトプロパティを探索する際には、最初にそのオブジェクト自身に目的のプロパティがあるかどうかを調べ、あればその値を返します。もし、無ければ、そのオブジェクトを生成したオブジェクト(もしあれば)のprototypeプロパティの値のオブジェクトに目的のプロパティがあるかどうかを調べます。あればそれを返し、なければさらにそのオブジェクトを生成したオブジェクトprototypeプロパティを同様に再帰的に繰り返し探索します。

function Foo() {
  this.p1 = 123;
}
 
function Foo2() {
  this.p2 = "hello";
}
Foo2.prototype = new Foo();
 
function Foo3() {
  this.p3 = 987;
}
Foo3.prototype = new Foo2();
 
var v = new Foo3();
var v1 = v.p1;
var v2 = v.p2;
var v3 = v.p3;

v1,v2,v3はそれぞれ、123, "hello", 987になります。prototypeチェーンを辿って目的のプロパティを探索しています。

ここで、

v.p2 = "world";

を実行すると、当然、v.p2の値は"hello"ではなく、"world"になります。 この場合、p2プロパティオブジェクトvに直接付加されています。その後、

delete v.p2;

を実行すると、v.p2の値は、再び"hello"になります。

また、for-in構文はvに直接付加されていないプロパティも対象になり、値はprototypeチェーンを辿って検索されます。

for (var i in v) {
  print("name=" + i + ", value=" + v[i]);
}

の結果は、

name=p3, value=987
name=p2, value=hello
name=p1, value=123

になります。

数値やtrue,falseなどの基本型はオブジェクトではないの?オブジェクト指向言語ならこれらの基本型もオブジェクトであるべきと思うのですが。

プロパティと値の集合をオブジェクトと定義するなら、基本型はオブジェクトではありません。しかし、基本型もあるオブジェクトから生成されたインスタンスと解釈することもできます。(この場合インスタンスという用語は適切ではありませんが)。したがって、次のようなコードは問題なく動作します。

var v1 = (123).toString();
var v2 = true.toString();

123に括弧をつけたのは、123.toString()としてしまうと、文法的には123の次のドットを小数点として解釈してしまい、SyntaxErrorになるからです。

さらに、新たなメソッドを追加することもできます。例として、正数を負数に、trueをfalse などに反転するメソッドneg()は以下のようになります。

Number.prototype.neg = function () { return -this; }
Boolean.prototype.neg = function () { return !this.valueOf(); }
  
var v1 = (123).neg();
var v2 = ture.neg();

v1, v2はそれぞれ -123, falseになります。

文字列に対しても同様なことができます。

123はオブジェクトでなく、また、123 instanceof Numberもfalseなのに、どうして (123).toString()などが実行できるのかがわかりません。

確かに混乱しやすいですね。v.fun() という形式は、vに対してToObjectという型変換を実行した後に、funプロパティを探索して、その値を関数として実行するという意味を持ちます。このとき、thisにはToObjectした値がバインドされます。したがって、数値や文字列などの基本型に対してもオブジェクトに対するメソッドコールと同等の記述ができるわけです。

つまり、

(123).toString();

(new Object(123)).toString();

と同等です。さらにnew Object(123)は new Number(123)と同等です。

前のFAQ項目で -this としていますが、thisは new Object(123)したオブジェクトです。'-'オペレータは 引数を ToNumberした結果に適用するのでうまく意図した結果が得られます。'!'オペレータも同様にToBooleanした結果に対して適用されます。ただし、new Boolean(false)をToBooleanした値はtrueなので、valueOf()メソッドで基本型に変換しています。

文字列に対してStringクラスのメソッドcharAt(), indexOf()などが利用できるのも同じ理由によるものです。

var x = "abcdefg".charAt(3)    // xには"d"が代入される

基本型にメソッドを適用する構文を書くと、オブジェクトが生成されることになるから、効率が悪いの?

必ずしもそうとは限りません。オブジェクトが生成されるというのは、言語の意味的なことで、そのオブジェクトは外部から参照することができませんから、実際にはオブジェクトを生成しなくてもすむような実装が可能です。処理系依存です。

実際に確かめてみたので、こちらをご覧ください。

newとdeleteというオペレータがありますが、C++とどう違うの?

newはConstructorオペレータでC++とほぼ同じ意味ですが、deleteはDestructorオペレータではなく、C++のdeleteとは全く意味が違いますJavaScriptではdeleteはオブジェクトから属性を削除するという意味のオペレータです。

var v = new Object();
v.foo = "hello";
print(v.foo);     //-- (1)
delete v.foo;
print(v.foo);     //-- (2)
delete v;

(1)ではhelloが表示されますが、(2)では undefinedが表示されます。これは、オブジェクトvの属性fooがdeleteにより削除されたからです。最後の delete vは delete this.v と同じ意味になります。これは現在のthisオブジェクトの属性vを削除することになります。new で生成したオブジェクト自体を削除することではありません。(この場合、vがvar宣言されていますので、実際には属性vは削除することはできません)

C++Javaでは動的なnewはできませんが、JavaScriptではどうですか?

C++Javaでは new Foo()のようにFooの部分は必ずクラス名や型名でなければいけませんが、JavaScriptでは Fooの部分は実行時に評価されますので、動的なnewができます。

function mynew(obj, arg) {
  return new ((typeof obj == "function") ? obj : Object)(arg);
}
  
v = mynew(Number, 123);

new されたオブジェクトは誰が開放するの?

ユーザプログラムが明示的にオブジェクトを開放することはできません。必要ないオブジェクトガーベジコレクション機能により処理系が自動的に開放します。

voidというのがあるけど、C/C++のvoidとどう違うの?

JavaScriptのvoidはC/C++Javaのvoidとは全く別の意味になります。JavaScriptのvoidは結果をundefinedにするというオペレータです。

var x = void (3 + 5);

は 3+5の結果を無効にしてundefinedがxに代入されます。undefinedはキーワードでなく、単なるグローバル変数ですので、値を変更することができてしまいます。voidを使うと、必ずundefinedが得られるという点で必要なときもありますが、実際にはほとんど使われることがありません。

undefined = 123;
var x = undefined;
var y = void 0;

xには123が代入されますが、yにはundefinedが代入されます。

グローバル変数関数オブジェクトプロパティの値ということがいまいちピンときませんが。

グローバル変数関数は、実はプログラムから参照するための特定の名前が付けられていないGlobalオブジェクトというオブジェクトプロパティです。グローバルスコープでは、Globalオブジェクトへはthisで参照することができます。

var x = 123;
var y = this.x;

yにはxの値である123が代入されます。

グローバルコードで定義した関数定義

function myfunc () { ... }                //-- (1)

は、

var myfunc = function myfunc () {...}     //-- (2)

とほぼ同じ意味になります。変数がGlobalオブジェクトプロパティであるのと同様にグローバル関数もGlobalオブジェクトプロパティです。

関数オプション引数を表現するにはどうすればよいですか?

ユーザ定義関数では、定義された引数(仮引数)の数より実際の引数(実引数)が少ない場合には、残りの引数にundefinedが代入されます。また、argumentsオブジェクトのlengthプロパティには、実際の引数の数がセットされます。これらの機構を用いればオプション引数が実現できます。

一方、オプション引数の数が多い場合には、オブジェクトリテラルを利用してキーワードパラメータ風の表現もよく用いられます。

gfx.setProperty({
  x:      10,
  y:      20,
  width:  80,
  height: 40
});

見てわかりやすく、順番に依存せず、また、新たなオプションの追加などが簡単にできます。

内部関数って何ですか?

関数の中で定義される関数です。関数定義は入れ子にすることができます。内部関数は親の関数のトップレベルに記述することができます。ifやwhile文などの中には内部関数を定義することはできません。JScriptFirefoxJavaScriptなどは文として記述可能なところにはどこでも関数定義が可能ですが、動作が微妙に異なります。

内部関数の中からは、親の関数のローカル変数にアクセスすることができます。また、内部関数の名前はローカルな名前になりますので、外部からはその名前で参照することはできません。さらに、内部関数は動的に生成されるものです。

function func1(x) {
  var y = 2 * x;
  function func2(z) {
    return x + y * z;
  }
  return func2(x + y);
}
    
v = func1(5);   // vには155が代入される

一方、関数オブジェクトですから、関数戻り値関数を返すこともできます。

function func1(x) {
  var y = 2 * x;
  function func2(z) {
    return x + y * z;
  }
  return func2;
}

v = func1(5);   // vには関数func2が代入される。

vは関数なので、関数コールが可能です。

w = v(8);

wには何が代入されるでしょうか?vは関数オブジェクトですが、この関数オブジェクトが生成されたときのローカル変数xとyの値が関数オブジェクトと一緒に保持されています。このような状態を関数クロージャ、あるいは単にクロージャと呼びます。したがって、v(8)の値はその時の環境がx=5,y=10,z=8なので、85になります。

ローカル変数関数パラメータが何かのオブジェクトプロパティだという意味がわかりません。

関数に制御が移ったとき、プログラムからは参照することができないActivationObjectと呼ばれる仮想的なオブジェクトが生成されます。意味的には関数のローカル変数は、このActivationObjectのプロパティなのです。ActivationObjectは関数に対して1つですので、ローカル変数のスコープも関数の中全体のスコープになります。また、同じ名前のローカル変数は1つしか定義できないことになります。これは、C/C++Javaなどと大きく異なる点ですので注意が必要です。

function myfunc() {
  var i = 3;
  for (var j = 0; j < 10; j++) {
    var i = j;
  }
  print(i);  // iの値は9
}

一方、関数パラメータは、argumentsというオブジェクトプロパティでもあります。

function plus(x, y) {
  return x + y;
}

これは、次の関数と同等です。

function plus() {
  return arguments[0] + arguments[1];
}

関数パラメータとローカル変数の名前が同じだったらどうなるの?

ローカル変数初期化されていなけば、関数パラメータの値が優先されます。初期値のないvar宣言は、その時点でその変数が定義されていなければ定義し、既に定義されていれば何もしない(undefinedで初期化はしない)という動作になります。

function myfunc(x, y) {
  var x;
  var y = 987;
  print("x=" + x);
  print("y=" + y);
}
myfunc(135, 246);
x=135
y=987

ホストオブジェクトって何ですか?

ホスト環境によって提供されるオブジェクトのことです。ホストオブジェクトの詳細な動作はECMA262では多くが実装依存となっています。

クロージャって何ですか?

実は、クロージャという用語はECMAScript Edition3の規格書には登場しません。関数ファーストクラスオブジェクトであり、関数の中で関数を定義できるような言語ではクロージャが登場します。

一般に、関数の中で定義したローカル変数はその関数が終了した時点で消滅しますが、内側で定義した関数の中でその変数を参照している状態で、その関数を親の関数戻り値として返した場合、親の関数が終了した後でもそのローカル変数が有効な状態を保持しています。関数変数が1つのオブジェクトとしてカプセル化された状態であり、これが関数クロージャと呼ばれます

先のFAQ項目で、関数の制御が移ったとき仮想的なオブジェクトであるActivationObjectが生成されると書きましたが、関数が終了しても、そのActivationObjectが存在し続けるような状況が発生した場合には、その関数の中から参照できるローカル変数等も関数が終了した後も存在し続けます。

function Counter(n) {
  var cnt = 0;
  function StepUp() {
    return cnt += n;
  }
  return StepUp;
}

var fun = Counter(5);    // -- (a)
print(fun());
print(fun());

Counter関数の中でStepUp関数を定義し、Counter関数戻り値としてStepUp関数を返しています。(a)でfunに代入されるのはその戻り値関数オブジェクトです。Counter関数は終了しているので、パラメータ引数nとローカル変数cntは消滅しているはずですが、funを関数として実行すると、5, 10, 15, ...という値が実行するたびに返されます。一見、不思議な現象ですが、変数nとcntはCounter関数スタックフレーム中にあるのではなく、StepUp関数に関連付けられたクロージャの中に存在し続けています。JavaScriptの場合、プロパティにアクセス制限を設定することはできませんが、クロージャを用いると完全に外部から隠蔽した変数を実現することができます。

クロージャが有効な簡単な例はありませんか?

例えば、こんなのはどうでしょう?

指定した関数を定期的に指定した回数だけ繰り返して実行する関数repeatを実装してください。繰り返しを途中で中断できること、および、現在の繰り返し回数を取得できることとします。つまり、以下のような仕様の関数になります。

repeat(num, dur, func, args)
num: 	繰り返し回数
dur:  	繰り返しの時間間隔
func:	繰り返し実行される関数オブジェクト。 
        関数中でのthisの値は戻り値と同一のオブジェクト。
args:	funcに与えられる引数の配列
戻り値: count, stopというメソッドを持つオブジェクト
count:  現在の繰り返し回数
stop:   繰り返しを強制終了する

かなり複雑で実装も難しいように見えますが、クロージャを使えば非常に簡単に実現できます。以下がそのコードです。1つの関数で実現しています。

function repeat(num, dur, func, args) {
  var cnt = 0;
  var timer = setInterval(function () {
    if (cnt++ < num) {
      func.apply(rv, args);
    } else {
      clearInterval(timer);
    }
  }, dur);
  var rv = {
    stop:  function () { clearInterval(timer); },
    count: function () { return cnt; }
  };
  return rv;
}

クロージャを使わないで実装することもできますが、複雑で見通しの悪いコードになるでしょう。

おわりに

HTML5が注目を集めています。そして、次の規格であるECMAScript 5th Editionがまもなくリリースされます。間違いなくJavaScriptは今以上に重要な言語になるでしょう。その基礎をしっかり理解することはとても大切です。一番正確に理解するには規格書を読むことですが、なかなか難解な内容で、すぐに理解できるものではありません。本エントリーがJavaScriptの初学者にとって少しでも参考になっていただけたら幸いです。 

scientrescientre 2009/10/06 22:57 はじめまして。興味深く読ませていただきました。
「Arrayのようにユーザ定義関数でnew しても関数コールしても同じ動作をするものは作れますか?」についてですが、戻り値のオブジェクトの書式が間違っているように思います。(各項の区切りがセミコロンになっている)

mindcatmindcat 2009/10/07 01:41 あっ、ほんとうだ。直しておきました。ご指摘ありがとうございます。

anpontan-pokananpontan-pokan 2009/10/07 19:52 偽gotoのやりかたがループとかに対応してなさそうだったので勝手に引用して修正しました
http://d.hatena.ne.jp/anpontan-pokan/20091007

jserjser 2011/04/01 06:52 > ここで、true, false, null, undefined, NaN, Infinityなどはキーワードでも予約語でもないことに注意してください。
true, false, null はリテラルであると同時に予約語でもあります(3rd, 5th 共通)。

http://www.ecma-international.org/publications/standards/Ecma-262-arch.htm
3rd edition pp.14--15

ReservedWord ::
 Keyword
 FutureReservedWord
 NullLiteral
 BooleanLiteral

Identifier ::
 IdentifierName but not ReservedWord

mindcatmindcat 2011/04/03 14:42 jserさん、
ご指摘の通りです。修正しておきました。ありがとうございます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証