関数の宣言と式 関数はJavaScriptの第一級オブジェクトです。この事は、その他の値と同じように渡す事が出来るという事です。この機能で良く使われる一つとして匿名関数 を他のオブジェクトにコールバックとして渡すというものがあり、これで非同期での実装が可能になります。
関数
宣言
function foo() {}
上記の関数はプログラムの開始時の前に評価されるように巻き上げ られます。従って定義 されたスコープ内のどこでも 使用する事が可能になります。ソース内での実際の定義が呼ばれる前でもです。
foo(); // このコードが動作する前にfooが作られているので、ちゃんと動作する
function foo() {}
関数
式
var foo = function() {};
この例では、foo
という変数に無名で匿名 の関数が割り当てられています。
foo; // 'undefined'
foo(); // これはTypeErrorが起こる
var foo = function() {};
var
は宣言である為に、変数名foo
がコードが開始される実際の評価時より前のタイミングにまで巻き上げられています。foo
は既にスクリプトが評価される時には定義されているのです。
しかし、コードの実行時にのみこの割り当てがされるため、foo
という変数は対応するコードが実行される前にデフォルト値であるundefined が代入されるのです。
名前付き関数式
他に特殊なケースとして、名前付き関数があります。
var foo = function bar() {
bar(); // 動作する
}
bar(); // ReferenceError
この場合のbar
はfoo
に対して関数を割り当てるだけなので、外部スコープでは使用できません。しかし、bar
は内部では使用できます。これはJavaScriptの名前解決 の方法によるもので、関数名はいつも 関数自身のローカルスコープ内で有効になっています。
this
はどのように動作するのかJavaScriptのthis
と名付けられた特殊なキーワードは他のプログラム言語と違うコンセプトを持っています。JavaScriptのthis
は正確に5個 の別々の使い道が存在しています。
グローバルスコープとして
this;
this
をグローバルスコープ内で使用すると、単純にグローバル オブジェクトを参照するようになります。
関数呼び出しとして
foo();
このthis
は、再度グローバル オブジェクトを参照しています。
メソッド呼び出しとして
test.foo();
この例ではthis
はtest
を参照します。
コンストラクター呼び出し
new foo();
new
キーワードが付いた関数呼び出しはコンストラクター として機能します。関数内部ではthis
は新規に作成された Object
を参照します。
this
の明示的な設定
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // 配列は下記で展開される
foo.call(bar, 1, 2, 3); // 結果はa = 1, b = 2, c = 3
Function.prototype
のcall
やapply
メソッドを使用した時には、呼び出された関数の内部でのthis
の値は、対応する関数呼び出しの最初の引数に明示的に設定 されます。
結果として、上記の例ではメソッドケース が適用されず 、foo
の内部のthis
はbar
に設定されます。
注意: this
はObject
リテラル内部のオブジェクトを参照しません 。
ですので、var obj = {me: this}
でのme
はobj
を参照しません 。
this
はここで紹介ている5個のケースの内どれか一つに束縛されます。
良くある落し穴
これらのケースのほとんどは理にかなったものですが、最初のケースは実際に利用されることが絶対 にないので、間違った言語設計だとみなせるでしょう。
Foo.method = function() {
function test() {
// このファンクションはグローバルオブジェクトに設定される
}
test();
}
良くある誤解としてtest
の中のthis
がFoo
を参照しているというものがありますが、そのような事実は一切 ありません。
test
の中のFoo
にアクセスする為には、Foo
を参照するmethod
のローカル変数を作る必要があります。
Foo.method = function() {
var that = this;
function test() {
// ここでthisの代わりに使用する
}
test();
}
that
は通常の変数名ですが、外部のthis
の参照の為に良く使われます。クロージャ と組み合わせる事でthis
の値を渡す事ができるようになります。
メソッドの割り当て
JavaScriptを使用する上で、もう一つ動かない ものが関数のエイリアスです。これは変数へメソッドを割り当て する事です。
var test = someObject.methodTest;
test();
最初のケースのtest
は通常の関数呼び出しになる為に、この中のthis
は、もはやsomeobject
を参照できなくなってしまいます。
this
の遅延バインディングは最初見た時にはダメなアイデアに見えますが、プロトタイプ継承 により、きちんと動作します。
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
method
がBar
のインスタンスにより呼び出された時に、this
はまさにそのインスタンスを参照するようになります。
クロージャと参照 JavaScriptの一番パワフルな特徴の一つとしてクロージャ が使える事が挙げられます。これはスコープが自身の定義されている外側のスコープにいつでも アクセスできるという事です。JavaScriptの唯一のスコープは関数スコープ ですが、全ての関数は標準でクロージャとして振る舞います。
プライベート変数をエミュレートする
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
ここでCounter
は2つ のクロージャを返します。関数increment
と同じく関数get
です。これら両方の関数はCounter
のスコープを参照 し続けます。その為、そのスコープ内に定義されているcount
変数に対していつもアクセスできるようになっています。
なぜプライベート変数が動作するのか?
JavaScriptでは、スコープ自体を参照・代入する事が出来無い為に、外部から変数count
にアクセスする手段がありません 。唯一の手段は、2つのクロージャを介してアクセスする方法だけです。
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
上記のコードはCounter
のスコープ中にある変数count
の値を変更する事はありません 。foo.hack
はその スコープで定義されていないからです。これは(Counter
内の変数count
の変更)の代わりにグローバル 変数count
の作成 -または上書き- する事になります。
ループ中のクロージャ
一つ良くある間違いとして、ループのインデックス変数をコピーしようとしてか、ループの中でクロージャを使用してしまうというものがあります。
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上記の例では0
から9
の数値が出力される事はありません 。もっと簡単に10
という数字が10回出力されるだけです。
匿名 関数はi
への参照 を維持しており、同時にforループ
は既にi
の値に10
をセットし終ったconsole.log
が呼ばれてしまいます。
期待した動作をする為には、i
の値のコピー を作る必要があります。
参照問題を回避するには
ループのインデックス変数をコピーする為には、匿名ラッパー を使うのがベストです。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
外部の匿名関数はi
を即座に第一引数として呼び出し、引数e
をi
の値 のコピーとして受け取ります。
e
を参照しているsetTimeout
を受け取った匿名関数はループによって値が変わる事がありません。
他にこのような事を実現する方法があります。それは匿名ラッパーから関数を返してあげる事です。これは上記のコードと同じ振る舞いをします。
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
オブジェクトのarguments
JavaScriptの全ての関数スコープはarguments
と呼ばれる特別な変数にアクセスできます。この変数は関数が受け取った全ての引数を保持する変数です。
arguments
オブジェクトはArray
ではありません 。これは配列と同じような -length
プロパティと名付けられています- 文法を持っていますが、Array.prototype
を継承している訳では無いので、実際Object
になります。
この為、arguments
でpush
やpop
、slice
といった通常の配列メソッドは使用する事が出来ません 。プレーンなfor
ループのような繰り返しでは上手く動作しますが、通常のArray
メソッドを使いたい場合は本当のArray
に変換しなければなりません。
配列への変換
下のコードはarguments
オブジェクトの全ての要素を含んだ新しいArray
を返します。
Array.prototype.slice.call(arguments);
この変換は遅い です。コードのパフォーマンスに関わる重要な部分での使用は推奨しません 。
引き数の受け渡し
下記の例はある関数から別の関数に引数を引き渡す際に推奨される方法です。
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// do stuff here
}
他のテクニックとして、高速で非結合のラッパーとしてcall
とapply
両方を一緒に使用するという物があります。
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// "メソッド"の非結合バージョンを作成する
// このメソッドはthis, arg1, arg2...argNのパラメーターを持っている
Foo.method = function() {
// 結果: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
仮パラメーターと引数のインデックス
arguments
オブジェクトはゲッター とセッター 機能を自身のプロパティと同様に関数の仮パラメーターとして作成します。
結果として、仮パラメーターを変更するとarguments
の対応する値も変更されますし、逆もしかりです。
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
パフォーマンスの神話と真実
arguments
オブジェクトは、関数の内部の名前宣言と仮パラメーターという2つの例外を常に持ちながら生成されます。これは、使用されているかどうかは関係がありません。
ゲッター とセッター は両方とも常に 生成されます。その為これを使用してもパフォーマンスに影響は全くといって言い程ありません。arguments
オブジェクトのパラメーターに単純にアクセスしているような、実際のコードであれば尚更です。
しかし、一つだけモダンJavaScriptエンジンにおいて劇的にパフォーマンスが低下するケースがあります。そのケースとはarguments.callee
を使用した場合です。
function foo() {
arguments.callee; // この関数オブジェクトで何かする
arguments.callee.caller; // そして関数オブジェクトを呼び出す
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // 通常はインライン展開する
}
}
上記のコードでは、foo
は自身と自身の呼び出し元の両方を知らないとインライン展開 の対象になる事が出来ません。この事は、インライン展開によるパフォーマンスの向上の機会を失くす事になり、また、特定のコンテクストの呼び出しに依存する関数のせいで、カプセル化が解除されてしまいます。
この為にarguments.callee
を使用または、そのプロパティを決して 使用しない事を強く推奨 します。
コンストラクタ JavaScriptのコンストラクタは色々ある他のプログラム言語とは一味違います。new
キーワードが付いているどんな関数呼び出しも、コンストラクタとして機能します。
コンストラクタ内部では -呼び出された関数の事です- this
の値は新規に生成されたObject
を参照しています。この新規 のオブジェクトのprototype
は、コンストラクタとして起動した関数オブジェクトのprototype
に設定されています。
もし呼び出された関数が、return
ステートメントを明示していない場合は、暗黙の了解でthis
の値を -新規のオブジェクトとして- 返します。
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
上記は、Foo
をコンストラクタとして呼び出し、新規に生成されたオブジェクトのprototype
をFoo.prototype
に設定しています。
明示的にreturn
ステートメントがある場合、関数は返り値がObject
である場合に限り ステートメントで明示した値を返します。
function Bar() {
return 2;
}
new Bar(); // 新しいオブジェクト
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // 返ってきたオブジェクト
new
キーワードが省略されている場合は、関数は新しいオブジェクトを返す事はありません 。
function Foo() {
this.bla = 1; // グローバルオブジェクトに設定される
}
Foo(); // undefinedが返る
JavaScriptのthis
の働きのせいで、上記の例ではいくつかのケースでは動作するように見える場合がありますが、それはグローバルオブジェクト がthis
の値として使用されるからです。
ファクトリー
new
キーワードを省略するためには、コンストラクタ関数が明示的に値を返す必要があります。
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
Bar
で呼び出されたものは両方とも全く同じものものになります。これには、method
と呼ばれるプロパティを持ったオブジェクトが新しく生成されますが、これはクロージャ です。
また、注意する点として呼び出されたnew Bar()
は返ってきたオブジェクトのプロトタイプに影響しません 。プロトタイプは新しく生成されたオブジェクトにセットされはしますが、Bar
は絶対にその新しいオブジェクトを返さないのです。
上記の例では、new
キーワードの使用の有無は機能的に違いがありません。
ファクトリーとして新しくオブジェクトを作成する
多くの場合に推奨される事として、new
の付け忘れによるバグを引き起こしやすいので、new
を使用しない ようにするという事があります。
新しいオブジェクトを作成するためにファクトリーを使用して、そのファクトリー内部に新しいオブジェクトを作成すべきだという事です。
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.someMethod = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
上記の例ではnew
キーワードが無いため堅牢になりますし、確実にプライベート変数 を使用するのが簡単になりますが、いくつかの欠点があります。
作られたオブジェクトがプロトタイプ上のメソッドを共有しないために、よりメモリーを消費してしまいます。
ファクトリーを継承するために、他のオブジェクトの全てのメソッドをコピーする必要があるか、新しいオブジェクトのプロトタイプ上にそのオブジェクトを設置する必要があります。
new
キーワードが無いという理由だけで、プロトタイプチェーンから外れてしまうのは、どことなく言語の精神に反します。
終わりに
new
キーワードが省略される事によりバグの可能性がもたらされますが、それによりプロトタイプを全く使わないという確かな理由にはなりません 。最終的には、アプリケーションの必要性により、どちらの解決法がより良いかが決まってきます。特に大切なのは、オブジェクトの作成に特定のスタイルを選ぶ事、またそのスタイルに固執する事 です。
スコープと名前空間 JavaScriptはブロックに2つのペアの中括弧を使うのが素晴しいですが、これはブロックスコープをサポートしていません 。その為、この言語に残されているのは関数スコープ だけです。
function test() { // スコープ
for(var i = 0; i < 10; i++) { // スコープではない
// 数える
}
console.log(i); // 10
}
注意: 代入が使用されてない時、return文や関数の引数、{...}
表記はブロック文として
解釈されて、オブジェクトリテラルとはなりません 。これはセミコロン自動挿入
と連動して奇妙なエラーを引き起こすことになります。
JavaScriptはまた明確な名前空間を持ちません。この事は全て一つのグローバルで共有された 名前空間で定義されるという事です。
変数が参照されるまでの間、JavaScriptはスコープ全てを遡って参照を探索します。グローバルスコープまで遡っても要求した名前が無いとReferenceError
が発生します。
グローバル変数の致命傷
// スクリプト A
foo = '42';
// スクリプト B
var foo = '42'
上記の2つのスクリプトは同じ効果を持っていません 。スクリプト Aはfoo
と呼ばれる変数を、グローバル スコープに定義しており、スクリプト Bはfoo
を現在 のスコープで定義ています。
繰り返しますが、この2つのスクリプトは同じ影響 を全く持っていない スクリプトになります。var
を使用しない事は重大な意味を持ちます。
// グローバルスコープ
var foo = 42;
function test() {
// ローカルスコープ
foo = 21;
}
test();
foo; // 21
test
関数の中のvar
ステートメントを省略するとfoo
の値をオーバーライドします。最初の内は大した事ではないように思いますが、JavaScriptが何千行規模になると、var
を使っていない事でバグの追跡が酷く困難になります。
// グローバルスコープ
var items = [/* 何かのリスト */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// サブループのスコープ
for(i = 0; i < 10; i++) { // varステートメントが無くなった
// 素敵な実装を!
}
}
外側のループはsubloop
が最初に呼ばれた後に終了します。なぜなら、subloop
がグローバル変数i
の値で上書きされているからです。2番目のfor
ループにvar
を使用する事によって簡単にこのエラーを回避する事ができます。目的とする効果 を外側のスコープに与えようとしない限り、絶対 にvar
ステートメントは省略してはいけません。
ローカル変数
JavaScriptのローカル変数の為の唯一の作成方法はfunction パラメーターとvar
ステートメントによって宣言された変数になります。
// グローバルスコープ
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// 関数testのローカル変数
i = 5;
var foo = 3;
bar = 4;
}
test(10);
foo
とi
は、関数test
のスコープ内のローカル変数ですが、bar
の代入は同じ名前でグローバル変数で上書きしてしまいます。
巻き上げ
JavaScriptは宣言を巻き上げ ます。これはvar
ステートメントとfunction
宣言が、それらを含むスコープの一番先頭に移動するという事を意味します。
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
上記のコードは、実行を開始する前に変換されてしまいます。JavaScriptはvar
ステートメントと同じように、直近で囲んでいるfunction
宣言を先頭に移動させます。
// varステートメントはここに移動する
var bar, someValue; // 'undefined'がデフォルト
// function宣言もここに移動する
function test(data) {
var goo, i, e; // 無くなったブロックスコープはこちらに移動する
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // barが'undefined'のままなので、Typeerrorで呼び出し失敗
someValue = 42; // 割り当てすると巻き上げの影響を受けない
bar = function() {};
test();
ブロックスコープの欠落はvar
ステートメントをループやボディの外に移動するだけでなく、if
の構成を直感的ではないものにしてしまいます。
元のコードの中のif
ステートメントはグローバル変数 であるgoo
も変更しているように見えますが、実際には -巻き上げが適用された後に- ローカル変数 を変更しています。
巻き上げ についての知識がないと、下に挙げたコードはReferenceError
になるように見えます。
// SomeImportantThingが初期化されているかチェックする
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
しかし、勿論上記の動きはvar
ステートメントがグローバルスコープ の上に移動しているという事実に基づいています。
var SomeImportantThing;
// 他のコードがSomeImportantThingをここで初期化するかもしれないし、しないかもしれない
// SomeImportantThingがある事を確認してください
if (!SomeImportantThing) {
SomeImportantThing = {};
}
名前解決の順序
JavaScriptのグローバルスコープ を含む、全てのスコープは、現在のオブジェクト を参照している特殊な名前this
を持っています。
関数スコープはまた、arguments
という名前も持っています。それは関数スコープの中で定義され、関数に渡された引数を含んでいます。
例として、関数の中でfoo
と命名された変数にアクセスしようとする場合を考えましょう。JavaScriptは以下の順番で、その名前を探索しようとします。
var foo
ステートメントが現在のスコープで使われている場合
foo
という名前の関数パラメーターが存在するかどうか
関数それ自体がfoo
として呼ばれているかどうか
一つ外のスコープに行き、再度#1 から始める
名前空間
一つしかグローバルの名前空間を持たない事による良くある問題は変数名の衝突による問題の起きる可能性です。JavaScriptでは、この問題を匿名関数ラッパー の助けで簡単に回避できます。
(function() {
// "名前空間"に自分を含む
window.foo = function() {
// 露出したクロージャ
};
})(); // 即座に関数を実行する
無名関数はexpressions とみなされ、呼び出し可能になり最初に評価されます。
( // カッコ内の関数が評価される
function() {}
) // 関数オブジェクトが返される
() // 評価の結果が呼び出される
関数式を評価し、呼び出す別の方法として構文は違いますが、同様の動作をするのが下記です。
// 2つの別の方法
+function(){}();
(function(){}());
終わりに
自身の名前空間にカプセル化する為に常に匿名関数ラッパー を使用する事を推奨します。これは、コードを名前衝突から守る為だけでなく、プログラムのより良いモジュール化の為でもあります。
さらに、グローバル変数の使用は悪い習慣 と考えられています。一回 でもグローバル変数を使用するとエラーが発生しやすく、メンテナンスがしにくいコードになってしまいます。