オレ得JSメモ2枚目。今日は関数のことを整理してみよう。
あ、オレ得jsメモはアドベントなんとかじゃないからいつまでとか決めてないし、何回書くかとかも決めてない。だってオレ得だからね。必要な時に必要なことをメモってくのです。
とりあえず関数定義にはいろいろ方法があるみたいなので、それぞれサンプル書いて確かめてみる。
関数宣言(function文)
JavaScriptでよく見かける関数の定義方法。
function 関数名(引数1、引数2、引数3,…){
処理1;
処理2;
…
}
行頭がfunctionで始まり、関数名に続いて()の中に関数に渡す引数を、{}の中に関数が呼び出されたときに実際に行う処理を記述する。
JavaScriptには関数の定義の仕方がいくつかあるが、こういうのを「関数宣言による定義」という(らしい)。
関数内の処理は何もしなくても良いし、他のオブジェクトのプロパティやメソッドを操作して終わる場合もあるし、returnで演算結果を関数の呼び出し元に返すこともある。
例えば引数numを渡して,numの二乗を返す関数は次のように書ける。
function sqr(num){
return num * num;
}
これはsqrという名前の関数を宣言し定義しただけなのでそれだけでは実行されない。実行するにはどうすればよいか?単純に関数の名前を呼んだら、その「呼ばれたとき」に実行される。引数の必要な関数なら、呼ぶ関数名の後に()で必要な数だけ引数を渡せばよい。引数の区切りは「,(コンマ)」でいい。
function sqr(num){
return num * num;
}
sqr(2);
function sqr~で定義された関数が、最後の行sqr(2)で「2」という引数の値を伴って呼び出され実行されている。ただ、これだと実行はされているけど、その結果をどこにも表示するよう記述してないので、例えばこうしてみようか。
function sqr(num){
return num * num;
}
console.log(sqr(2));
これでコンソール画面に「4」と表示され確認することができるようになる。
(console.logとalertについてはコチラ)
もうちょっと細かくプロセスを説明するならば
- 「sqr」という名前の関数を呼び出す
- 関数「sqr」を定義している部分を探して参照する
- 関数「sqr」に「2」という引数を渡す
- 「引数*引数」(ここでは2*2)という処理を行う
- returnでその結果(ここでは4)を関数の呼び出し元に返す
- console.logにより、返された結果(ここでは4)をコンソール画面に表示する
というようなことになると思う。
ところでこの例では、ソースのコード記述順としては「関数を定義する」→「関数を呼び出す」となっているが、行頭function~によって宣言された関数は、実際には「ソースの解釈時(コンパイル)時に」定義されるのであって「実行時に」定義されるわけではない。
つまり処理系全体がどのような処理経路を辿ろうとも、例え実際にその関数が1度も使用されることがなくてもそのソースがロードされ、コンパイルされたときに定義「だけ」はされる、ということ。
逆に言えば関数宣言により定義される関数は、ソース上その関数が呼び出しの前に書かれていようが後に書かれていようが呼ばれれば実行される、ということになる。だから
console.log(sqr(2));
function sqr(num){
return num * num;
}
ソースがこのように関数の呼び出し部分が先にあって、関数の定義部分が後に記述されていてもsqr(2)はきちんと実行され、コンソール画面には「4」と表示される。関数の定義が後に記述されているからと言ってsqr(2)実行時に「関数が見つかりません」みたいなエラーにはならないということだ。
こういった宣言より前からのアクセスを「ホイスト(巻上げ)」という。
ではhtml内に次のような記述がされていた場合はどうだろう。
<script type="text/javascript">
console.log(sqr(2));
</script>
<script type="text/javascript">
function sqr(num){
return num * num;
}
</script>
これはアウト。エラーを吐く。「関数宣言による定義は後に書いてもいいって言ったじゃん!」
それはそうなのだが、これはJavaScriptというよりhtmlの仕様によるものだ。htmlソースはブラウザにロードされると書いてあることが書いてある順に解釈(レンダリング)される。最初のscriptタグ内のレンダリングが完了しないと2番目のscriptタグのレンダリングに移れない。当然最初のscriptタグをレンダリングしている最中は2番目のscriptタグ内に記述されているコードはまだ一切解釈されていないのだ。
こんな変な書き方は普通しないと思うけど、念のため。外部JSファイルにJavaScriptコードを書くときも、ファイルを分割すると同様のことが起こり得るからね。注意しよっと。
関数式(function式)
関数は上記のような「関数宣言」によって定義する以外に、変数に直接代入して定義する方法もある。
var sqr = function(num){
return num * num;
};
これは私が激しく勘違いしていたことだが「sqr」は関数名「ではない」。ただの変数だ。そして「= function~」以降で記述されている関数そのものには名前がない。名前がない関数を「無名関数」とか「匿名関数」と呼んだりする。
またfunctionの閉じかっこ「}」の後ろに「;(セミコロン)」がついていることに注意。これはつまり、関数宣言ではなくあくまで変数を定義してそこに関数を代入している、ということを意味する。変数の中に値を代入したり、他のオブジェクトの参照を代入したりしているのと行為としては変わらない。
こういうのを「関数式」という。そして変数に引数を付けて渡すと関数の中の処理を実行し、また必要があればなんらかの値を返す。
var sqr = function(num){
return num * num;
};
console.log(sqr(2));
こうするとコンソールは「4」と応答する。まるで関数を呼び出しているようだ!しかし、くどいようだが呼び出しているのはあくまで変数だ。関数ではない。呼んだ変数の中にたまたま関数が入っていた、ということだ。
それゆえ
console.log(sqr(2));
var sqr = function(num){
return num * num;
};
関数式の定義(=変数の定義とそれへの関数の代入)以前で変数sqrを呼ぶとコンソールに「変数が見つからないよ!」と言われたり、この例のように引数付で呼ぶと「sqrは関数じゃないよ!」などとエラーを返されたりする。
関数宣言と関数式は似ているようで全然違うのだ。
ところでsqrは変数だ。変数だから当然中身も書き換えられる。
var sqr = function(num){
return num * num;
};
sqr = "ほげほげ";
console.log(sqr);
sqrに関数を代入した後、さらに「ほげほげ」という文字リテラルを代入しなおしているので、コンソールには「ほげほげ」と表示される。
関数式による関数定義は「その場で定義され、その場で実行される」ので、使い捨てるのに向いている関数定義、とも言える。
さて、関数式のここまでの例では無名関数を使ってきたが、関数に名前を付けても良い。ただし関数式で付けた関数の名前はグローバルスコープからは参照できない。
var sqr = function MyFunc(num){
return num * num;
};
console.log(MyFunc(2));
関数に「MyFunc」という名前を付けて、関数の外から呼んでみた。これはエラーになる。グローバルから結果を得たいときは代入された変数sqrのほうを呼ばなければならない。
この場合関数を呼び出せるのは関数自身の中からだけ。だとすると何のために関数に命名するのか?
var sqr = function MyFunc(num){
num = num * num;
console.log(num);
if(num < 65536){
MyFunc(num);
}
};
sqr(2);
このように一定の条件を満たすまで再帰的に関数自身を呼び出し計算を繰り返す。
例では引数として与えられた初期値を二乗しコンソールに表示、それが65,536未満なら再度その結果を二乗しコンs(ry…というようなことをしている。引数に1以下を指定すると無限ループになるね!
こういう単純な場合にはわざわざ再帰計算しなくてもforループでも同じことができる…
var sqr = function(num){
for(num ; num < 65536 ; num = num*num){
console.log(num);
}
};
sqr(2);
…ので一見意味がなさそうだが、高次の方程式で収束・発散調べる場合は算式そのものが複雑になるから、関数式を再帰するメリットが出てくるのかもしれない。
コンストラクタ(Functionコンストラクタ)
もう一つ。これは私自身あまり必要性を(今のところ)感じていない関数の定義方法。
var sqr = Function('num','return num * num');
console.log(sqr(2));
よく観察してほしい。functionではなくFunctionだ。引数のようなものがあるが、関数定義(function文)や関数式(function式)で関数本体のステートメントに相当する{}がない。
このFunctionはコンストラクタと呼ばれる。そして()の中の引数に見える(ってうか引数だけど)部分の最後が関数本体のステートメントだ。ただしクォーテーション’でくくって文字リテラルで与えられている。
本体ステートメントが複数行にわたる場合は;(セミコロン)で区切って書いても良い。また、引数が複数あっても良い。
var sqr = Function('num1','num2','num1++ ; num2++ ; return num1 * num2');
console.log(sqr(2,3));
とはいえ、複雑な処理を行おうとすると常人には理解しがたい表記になりそう。
Functionコンストラクタもsqrへの代入して利用されているから
console.log(sqr(2,3));
var sqr = Function('num1','num2','num1++ ; num2++ ; return num1 * num2');
これはエラーになる。関数式の時と同じように実行時に定義される、ということだ。
ちなみに、関数本体を文字リテラルで与えるということは、この部分を変数化することもできるのではないかな?
var str = 'return num * num';
var sqr = Function('num',str);
console.log(sqr(2));
おお、エラーを吐かない。コンソールにはちゃんと4と表示される。ということは関数ステートメント自体も入れ替えてられるということだ。用途思いつかないけど。
参考とか
Qiitaはやっぱり掘るといろんなもんが出てくるなー。ありがとう、ありがとう。
まとめ
関数定義には3つ方法がある。関数宣言(function文)、関数式(function文)、そして関数コンストラクタ(Functionコンストラクタ)。
関数宣言はホイストかかるのでどこでも呼び出せる。そのかわり使っても使わなくてもその分メモリは占有されるだろう。
関数式は自身を自身の中からしか参照できない(ローカルスコープ)。実行時定義なので使い捨て。処理が通過したらメモリも破棄される(たぶん)。
関数コンストラクタは…よくわからないから使わない!
関数についてはほかにもコールバックの話とかクロージャとかまだまだ深い話があるけど、一度に深追いしてもアレなので今日はここまで。
識者からは「いい加減なこと書くな!」と怒られそうだが、これが今のところの私の理解。んじゃまたー。
コメント