とりあえず雑記帳(跡地)
Functionオブジェクトあれこれ
最終更新:
Bot(ページ名リンク)
-
view
WebコミックLibraryhttp://web-comi.appspot.com/ GAE/JとSlim3で作成してみた、各出版社から配信されているWebコミックをまとめて閲覧できるサイトです。只今、実験運用中… |
JavaScriptでは関数もオブジェクト(Functionオブジェクト)
functionステートメント
- 通常、JavaScriptで関数を定義する場合に用いる。
- あまり、Functionオブジェクトとかは意識してないかと思います。意識しなくても良い関数の定義方法。
// 定義
function foo() {
alert("test");
}
// 呼び出し
foo();
function演算子とfunction式
- function演算子によって、Functionオブジェクトが生成される
- function演算子の後に、生成するFunctionオブジェクトの、関数の定義を記述します(Functionオブジェクトのリテラル表現)。
- ぱっと見た目はfunctionステートメントと変わりませんな。
- 特徴的なのは、関数名が無いことです※実際は記述できるらしいですが、滅多に記述しません
- function演算子の後に、生成するFunctionオブジェクトの、関数の定義を記述します(Functionオブジェクトのリテラル表現)。
- 結果がFunctionオブジェクトへの参照(以下「への参照」は省略)となる式を「function式」と呼ぶ。
- function演算子を用いた、Functionオブジェクトのリテラル表現もfunction式
- Functionオブジェクトを代入した変数のみで表される式もfunction式
/* Stringオブジェクトの場合 */
// リテラル表現"test"が表すStringオブジェクトが、strに設定される。
var str = "test";
/* Functionオブジェクトの場合 */
// リテラル表現function(){...}が表すFunctionオブジェクトが、func1に設定される。
var func1 = function() {
alert("test");
};
// 引数がある場合
var func2 = function(arg) {
alert(arg);
};
functionステートメントの再解釈
- 実は、functionステートメントとは
- 記述された内容のFunctionオブジェクトを生成し
- グローバルオブジェクトに関数名と同名のプロパティを定義し
- そのプロパティに生成したFunctionオブジェクトを設定する、という一連の動作を行う「ステートメント」です
- つまり、「function式」では無いことに注意(後ほど説明)
functionオブジェクトの処理実行
- Functionオブジェクトに対して"()"を適用することで、関数を実行する。
- つまり、function式に対して()を適用する
/* Stringオブジェクトの場合 */
var str = "test";
alert(str.length); // strが参照するStringオブジェクトの、lengthプロパティへのアクセス
alert("test".length); // リテラル表現に対して、直接プロパティを参照してもよい
/* Functionオブジェクトの場合 */
var func1 = function() {
alert("test");
};
func1(); // func1が参照するFunctionオブジェクトが表す処理を実行する。
(
function() {
alert("test");
}
)(); // リテラル表現に対して、直接()を適用して処理を実行してもよい。
// 引数がある場合
var func2 = function(arg) {
alert(arg);
};
func2("test"); // func2が参照するFunctionオブジェクトが表す処理を実行する。
(
function(arg) {
alert(arg);
}
)("test"); // リテラル表現に対して、引数を指定した()を直接適用して処理を実行してもよい。
- Functionオブジェクトのリテラル表現を直接実行する場合にリテラル表現を丸括弧で囲んでいるのは、丸括弧で囲まないとリテラル表現(=function式)では無く「functionステートメント」と解釈されてしまうためです。「functionステートメント」はFunctionオブジェクトを表しているわけではないので、()を適用できません。
Functionオブジェクトをやりとり
- Functionオブジェクトは、当然関数の引数や戻り値として使えます。
function bar(func) {
func();
}
var foo = function() {
alert("test");
};
bar(foo);
- 上記の例では、"test"をalert表示するFunctionオブジェクトを生成し、それを関数barに渡しています。
- 関数barは、引数funcに渡されたFunctionオブジェクトを、引数なしで呼び出します。
- 渡されたFunctionオブジェクトが要求する引数と、実際に呼び出すときの引数は一致しなくても大丈夫っぽいです。
function foo() {
return function() {
alert("test");
};
}
var func = foo();
func();
- 上記の例では、"test"をalert表示するFunctionオブジェクトを戻り値として返す関数fooを、functionステートメントで定義しています。
- 戻り値を受け取った後、()を適用して呼び出しています。
クロージャ
- 下記のケースを考えます。
function foo(arg) {
var val = "123";
return function(str) {
alert(val + arg + str);
};
}
var func = foo("456");
func("789");
- この例では、関数foo()内部で生成したFunctionオブジェクトが、function式の外側で宣言されている変数valや引数argを参照しています。またFunctionオブジェクト自身で引数strを定義しています。
- foo()はFunctionオブジェクトを返しますが、Functionオブジェクト「だけ」では、Functionオブジェクト自身で定義していないvalやargの値を決定できません。
- そこで、通常はfoo()終了後に削除されるvalやargのメモリ領域は、function式で参照された場合は削除されず、それを参照するFunctionオブジェクトが削除されるまで存在します。
- このような、「関数(Functionオブジェクト)と、関数(Functionオブジェクト)自身で定義されていない変数(argとval)のメモリ領域」の組み合わせを「クロージャ」と呼びます。
- クロージャの「環境」とは、JavaScriptにおいては、argやvalのメモリ領域を指します。
- 別に「定義外の変数のメモリ領域を保持しているものだけがクロージャ」ってわけでもありません。「定義外の変数を参照していない=0個の変数のメモリ領域を確保している」と解釈すれば、メモリ領域を保持していなくてもクロージャです。
- まぁJavaScript的には要するに、Functionオブジェクト≒クロージャと考えておいてもいいです。
- foo()の呼び出し毎に、ローカル変数のメモリ領域は異なるものが割り当てられるので、foo()が返すクロージャのargやvalのメモリ領域(環境)も、foo()の呼び出し毎に異なります。
function foo(arg) {
var val = "123";
return function(str) {
alert(val + arg + str);
};
}
var func1 = foo("456"); // func1のargは"456"になる
var func2 = foo("abc"); // func2のargは"abc"になる
// valもメモリ領域は別物だが、内容は同じ"123"が設定されている
func1("789");
func2("def");
お堅い説明
- 関数で参照している変数は「束縛変数」と「自由変数」に分類されます。
- 厳密な定義はよくわからないので、不正確かもしれませんが、
- 束縛変数とは、(引数を含め)関数自身で定義している変数(上記のstr)
- 自由変数とは、関数自身では定義されていない変数(上記のargやval)
- 厳密な定義はよくわからないので、不正確かもしれませんが、
- 関数が実行可能であるためには、関数内の全変数が束縛されている(値が設定されることが保証されている)必要があります。
- 束縛変数は、既に関数自身の定義によって束縛されています(引数として値が外部から渡される、ローカル変数として値が代入される等)。
- 自由変数は、関数自身では定義されていないため、関数自身の定義だけでは何の値が設定されるかわかりません。
- そこで、関数に対して、その関数の自由変数の値を定義するのが環境です。
- 関数に対して環境を与えることで、関数の自由変数を束縛します(自由変数の値を定数化する)。
- ということで、関数と環境のペアをクロージャと呼びます。
- 同じ関数でも、環境が異なれば、返す結果も異なります。
- 関数と環境の関係は、アプリケーションと環境変数の関係と、まさに同じと考えてください。
thisが指すもの
- 関数内で用いるthisキーワードが何を指すか
var func = function() {
alert(this.bar);
};
- Javaとかに慣れていると、関数もオブジェクトということから、thisは生成されたFunctionオブジェクト自身を指すと勘違いしそうですが、違うのです
- thisが指すのは、関数が実行された際に指定されたコンテキストオブジェクト
- コンテキストオブジェクトを指定する方法は幾つかある
- 代表的なのは、関数オブジェクトを指すプロパティに対して()を適用した場合は、そのプロパティを持つオブジェクトがコンテキストに選ばれる
- 文章で書くと回りくどいが、要するに下記のような、ありきたりな方法
var obj1 = new Object(); // オブジェクトを生成
obj1.bar = "obj1"; // オブジェクトにbarプロパティを定義して、文字列を設定
obj1.func = function() { // オブジェクトにfuncプロパティを定義して、Functionオブジェクトを設定
alert(this.bar);
};
obj1.func(); // オブジェクトのfuncプロパティに設定されたFunctionオブジェクトを実行(コンテキストオブジェクトとしてobj1が選ばれる)
- つまり、同じ関数オブジェクトでも、コンテキストオブジェクトが違えば、thisが指すオブジェクトも違う
var func = function() { // Functionオブジェクト生成
alert(this.bar);
};
var obj1 = new Object(); // オブジェクト1を生成
obj1.bar = "obj1";
var obj2 = new Object(); // オブジェクト2を生成
obj2.bar = "obj2";
obj1.foo = func;
obj1.foo(); // コンテキストオブジェクトはobj1
obj2.foo = func;
obj2.foo(); // コンテキストオブジェクトはobj2
- functionステートメントで生成したFunctionオブジェクトのthisが指すのは?
- functionステートメントによって、グローバルオブジェクトのプロパティに設定されるので、コンテキストオブジェクトはグローバルオブジェクトになる
applyメソッド
- コンテキストオブジェクトを指定する別な方法として、Functionオブジェクトのapply()メソッドを用いる方法があります
var obj1 = new Object();
obj1.bar = "obj1";
var func = function(arg1, arg2) {
alert(this.bar + ":" + arg1 + ":" + arg2);
};
func.apply(obj1, ["abc", "123"]);
- apply()メソッドを呼ぶと、そのFunctionオブジェクトが表す関数が実行されます。
- 第1引数に、実行時のコンテキストオブジェクトとするオブジェクトを指定します。
- 本来の関数の引数は、apply()メソッドの第2引数に、配列で指定します。
new演算子とコンストラクタ
見た目はJavaと同じでも、仕組みは全然別物なので要注意
- new演算子を使うと、新しいオブジェクトを生成できます。
var obj = new Object();
var str = new String();
var dt = new Date();
- JavaScriptにはクラスが存在していないわけですが、ではnew演算子の後に指定しているのは何でしょう?
- 実は、Functionオブジェクトを指定しているのです。
var Foo = function() {
this.bar = "test";
}
var obj = new Foo();
alert(obj.bar);
- オブジェクトが生成される際に、オブジェクトを初期化するための関数として、new演算子の後に指定したFunctionオブジェクトが実行されます。
- 実行されるFunctionオブジェクトのコンテキストオブジェクトは、new演算子で新しく生成したオブジェクトになります。
- new演算子で生成されるオブジェクトは常に、(既定のプロパティ以外は)何もプロパティを持たない、プレーンなオブジェクトです。
- そのプレーンなオブジェクトをコンテキストオブジェクトとして、Functionオブジェクトの実行時にプロパティを定義していきます。
- つまりは、生成したオブジェクトのコンストラクタの役割をするFunctionオブジェクトをnew演算子の後に指定しているのです。
- コンストラクタとして指定したFunctionオブジェクトには、生成したオブジェクトのconstructorプロパティでアクセスできます。
var Foo = function() {
this.bar = "test";
}
var obj = new Foo();
alert(obj.constructor);
プロトタイプチェーンによる、プロパティの共有
- JavaScriptでは通常、プロパティはオブジェクト毎に、動的に定義します。
- 例えば、Functionオブジェクトのプロパティfooを、複数のオブジェクトに設定する場合は、
var func = function() {
alert(this.bar);
}
var obj1 = new Object();
obj1.foo = func;
obj1.bar = "obj1";
var obj2 = new Object();
obj2.foo = func;
obj2.bar = "obj2";
obj1.foo();
obj2.foo();
- オブジェクトが多い場合に、これをオブジェクトを生成する度にやるのは面倒くさいですね。
- 例えば、先に説明した、コンストラクタ内でプロパティを設定する方法がありそうです。
var Foo = function() {
this.foo = function() {
alert(this.bar);
}
}
var obj1 = new Foo();
obj1.bar = "obj1";
var obj2 = new Foo();
obj2.bar = "obj2";
obj1.foo();
obj2.foo();
- ところがこの方法では、オブジェクト毎にFunctionオブジェクトも生成することになり、メモリ効率はよろしくありません。
- そこで、JavaScriptにはプロトタイプチェーンという、プロパティを複数のオブジェクト間で共有する仕組みが用意されています。
プロトタイプチェーンを詳しく
- JavaScriptのオブジェクトは全て、内部プロパティ__proto__を持ちます
- なお、__proto__を直接利用することは推奨されていません。IEでは外部からアクセスすらできません。
obj.foo();
- 上記のような、オブジェクトのプロパティfoo(fooはFunctionオブジェクトとする)にアクセスする際に
- まず、指定したobjに対して、プロパティfooが定義されているか検索
- objにプロパティfooが定義されていない場合は、obj.__proto__に設定されたオブジェクトに対して、プロパティfooが定義されているか検索
- obj.__proto__にプロパティfooが定義されていない場合は、obj.__proto__.__proto__に設定されたオブジェクトに対して、プロパティfooが定義されているか検索
- 以降、同様にして、プロパティfooが見つかるか、__proto__がnullになるまで、__proto__を遡っていきます
- このような、指定されたプロパティの検索対象となる、内部プロパティ__proto__の連鎖構造をプロトタイプチェーンと呼びます。
- つまり、__proto__に同じオブジェクトが設定されているオブジェクト同士は、__proto__に設定されたオブジェクト(と、そのプロトタイプチェーン)のプロパティを共有することになります。
- では、__proto__に設定されているのは何者か?
- __proto__に設定されてるのは、そのオブジェクト生成時のコンストラクタとなったFunctionオブジェクトの、プロパティprototypeが指すオブジェクトです。
- Functionオブジェクトは全て、プロパティprototypeを持ちます。
- Functionオブジェクト作成時には、prototypeにはプレーンなオブジェクト(new Object()で返されるオブジェクト)が設定されます。
- prototypeに設定されたオブジェクトに対して、共有したいプロパティを定義していけば、そのFunctionオブジェクトをコンストラクタとして生成されたオブジェクトは、そのプロパティを共有することになります。
var Foo = function() {
}
Foo.prototype.foo = function() { // 共有したいプロパティfooをprototypeのオブジェクトに定義する
alert("test");
}
var obj1 = new Foo(); // ここで、obj1.__proto__にFoo.prototypeが設定される
obj1.foo(); // obj1にはプロパティfooは定義されていないが、obj1.__proto__にあるFoo.prototypeにプロパティfooが定義されているため、実行可能。
var obj2 = new Foo();
obj2.foo(); // obj1と同じFooをコンストラクタとして生成されたobj2からも、fooを実行可能
- プロトタイプチェーン上で発見されたプロパティが参照する、Functionオブジェクトを実行する際のコンテキストオブジェクトは、最初にプロパティのアクセスを試みたオブジェクトになります。
- 実際にそのプロパティが定義されている、prototypeのオブジェクトではないことに注意
var Foo = function() {
this.bar = "test";
}
Foo.prototype.foo = function() {
alert(this.bar);
}
var obj = new Foo();
obj.foo(); // foo()のコンテキストオブジェクトはobj
- この、最初にプロパティのアクセスを試みたオブジェクトがコンテキストオブジェクトになる仕組みを利用して、クラスもどきが実現できます