JavaScriptでの関数の特徴や注意点
JavaScript で使われる関数の特徴や注意点を思いついた限り書いていこうと思います。勉強し始めて間もない人には厳しいかもしれません。ある程度サンプルコードを書いたりした人にとっては、「で JavaScript の関数って結局なんなの?」って思うところを書いてみたつもりです。
目次
JavaScript での関数はオブジェクトである
関数オブジェクト(Function オブジェクト)
まずはコードから。
var add = function(x,y) {
return x+y;
}
console.log(add(4,5)); // 9
add = 0;
console.log(add); // 0
console.log(add(4,5)); // エラーになる
ここでは add
という名前をもった変数に関数オブジェクトを格納しています。JavaScript では関数はオブジェクトなので、add
という変数は関数オブジェクトになるわけです。そして関数オブジェクトを引数にして console.log
でオブジェクトの中身を表示させています。
次に add=0
で変数を数値として上書きしています。この時点で add
という変数には 0
という数値が代入されて関数オブジェクトではなくなります。なので console.log(add)
では "0" が出力されますが、console.log(add(4,5))
ではエラーが出ます。add
はもはや関数オブジェクトではないからです。
なので
var add = function(x,y) {
return x+y;
}
window.alert(add); // function add(x,y){ return x+y };
というように関数を alert
で表示させると、add
変数に格納されている関数オブジェクトが表示されます。
JavaScriptでは関数が第一オブジェクトである
それもこれも JavaScript では関数が第一級オブジェクトであることからきています。第一級オブジェクトというのは
あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである
第一級オブジェクト – Wikipedia
らしいです。でもって第一級オブジェクトは
無名のリテラルとして表現可能である。
変数に格納可能である。
データ構造に格納可能である。
それ自体が独自に存在できる(名前とは独立している)。
他のものとの等値性の比較が可能である。
プロシージャや関数のパラメータとして渡すことができる。
プロシージャや関数の戻り値として返すことができる。
実行時に構築可能である。
表示可能である。
読み込むことができる。
分散したプロセス間で転送することができる。
実行中のプロセスの外に保存することができる。
第一級オブジェクト – Wikipedia
というような性質をもっているようです。さっきの add
の例でいえば、『変数に格納可能である』というのがまさに当てはまっています。
ということで改めて: "JavaScript での関数はオブジェクトである"
ただし全部が全部こうではないです。唯一の例外がクロージャーと呼ばれる関数です。クロージャーについては後述します。
JavaScript での色々な関数
普通の関数
// 関数の定義
function hoge() {
alert("Hello, World!");
}
// 関数の実行
hoge();
JavaScript を勉強し始めたらまず目にするものではないでしょうか。
無名関数/匿名関数
// 関数の定義
var hogeFunc = function() {
alert("Hello, World!");
}
// 関数の実行
hogeFunc();
第一級オブジェクトの特徴の一つ、『変数に格納可能である』というのを思い出してください。そうなってますよね。この記述は関数リテラルと呼ばれるもので、ここでは function()...
でもって名前のない関数を定義した上でそれを hogeFunc
という名前の変数に代入しています。hogeFunc
という変数に代入されるのは関数リテラルへの参照です。以下のように配列リテラル ['a','b','c','d']
を変数 hoge
に格納して配列にしているのと同じようなものです;
var hoge = ['a','b','c','d']; // 配列リテラルを変数hogeに代入して配列を定義.
ちなみに Wikipedia では「リテラル」というのは
コンピュータプログラミングにおいてリテラルは、ソースコード内に値を直接表記したものをいう。数値、文字列、関数などさまざまな型のものが存在し、それぞれの表記方法も言語によって異なる。即値ともいう。
リテラル – Wikipedia
と書かれています。
JavaScript でクラスのようなものを作ろうとしたら、この無名関数をコンストラクタ関数として使ったりします。この場合は、『関数リテラルの構文で関数オブジェクトを生成して hogeFunc
という名前の変数に参照代入している』という感じに捉えることができます。詳しくはJavaScript でのオブジェクト指向プログラミングを読んでください。
名前付き関数
// 関数の定義
var hogeFunc = function hoge() {
alert("Hello, World!");
}
// 関数の実行
console.log(hogeFunc.name); // "hoge"
hogefunc();
コロン関数/連想配列関数
// 関数の定義
var hogeFunc = {
hoge: function() {
alert("Hello, World!");
},
hogehoge: function() {
console.log("Hello, World!");
}
}
// 関数の実行
hogeFunc.['hoge']();
hogeFunc.hoge();
hogeFunc.['hogehoge']();
hogeFunc.hogehoge();
コロン関数/連想配列関数と書きましたが、これは { hoge: fuctioon () {...} ... }
という無名オブジェクトを生成して hogeFunc
という変数に代入している、と考えた方がしっくりくるかもしれません。これもJavaScriptでのオブジェクト指向プログラミングを読んでみてください。おそらくそっちを読んでから見ると印象がだいぶ変わると思います。
高階関数
function hoge(func,x,y) {
return func(x,y);
}
function add(x,y) {
return x + y;
}
console.log(hoge(add,1,1)); // 2
hoge
が高階関数です。関数を引数にとっています。第一級オブジェクトの性質の一つ、『プロシージャや関数のパラメータとして渡すことができる』というのに当てはまってますね。
無名即時関数
// 関数の定義
(function() {
alert("Hello, World!");
}() );
// 関数の実行は関数の定義と同時に行われます
名前付き即時関数、無名即時関数ともにfunction
自体を"()"でくくっている点、さらに一番後ろに"()"がついているのが即時関数の特徴です。これは関数リテラルをなにかの変数に直接代入せずに直接呼び出していることになっています。
即時関数ですが、最後の "()" の位置をどうするかで2通りの書き方があるようです。でもって推奨されているのは "{}" の後に "()" を書く方だそうです;
( function() { ... }() ); // 推奨
( function() { ... } )(); // 非推奨
即時関数についておまけ
function
を "()" でくくらなくても即時関数にすることができます(参考:即時関数(function(){ … })()の別の書き方いろいろ – 泥のように)。書き方が変わると戻り値が変わるのが特徴です。
(function(){ ... }()); // 戻り値は普通
+function(){ ... }(); // 戻り値は数値になる
-function(){ ... }(); // 戻り値は数値になる
!function(){ ... }(); // 戻り値は論理値になる
void function(){ ... }(); // 戻り値は常にundefinedになる
名前付き即時関数
// 関数の定義
(function hogeFunc() {
alert("Hello, World!");
}) ();
// 関数の実行は関数の定義と同時に行われます.
JavaScript では関数自体もグローバルオブジェクト( Window
オブジェクト)のプロパティになります。そのため、関数をグローバル空間で定義すると関数自体もグローバル変数に割り当てられることになります。一方、即時関数はグローバル変数に設定される前に実行されて破棄されます。つまり使い捨てられるわけです。なので即時関数には名前をつけないのが普通です。
クロージャー
function hoge(num) {
var cnt = num;
return function(){
return ++cnt;
}
}
var hoge1 = hoge(1);
var hoge2 = hoge(10);
console.log(hoge1()); // 2
console.log(hoge1()); // 3
console.log(hoge2()); // 11
console.log(hoge2()); // 12
console.log(hoge1()); // 4
関数を返り値として返す関数です。これも第一級オブジェクトの性質の一つ、『プロシージャや関数の戻り値として返すことができる』に当てはまってますね。関数が返り値なので hoge1
や hoge2
は関数になっていて、なおかつそれぞれが干渉し合わないようになっています。
さて、このクロージャーですが関数を上書きすると少し変わった挙動をします。
function hoge(num) {
var cnt = num;
return function(){
return ++cnt;
}
}
var hoge1 = hoge(1);
hoge = 0;
console.log(hoge1()); // 2
var hoge2 = hoge(10); // エラーになる
hoge1
は7行目ですでに返り値の関数がセットされているので、9行目で hoge
を上書きしても影響を受けません。ですが hoge2
はすでに hoge
が上書きされた後なのでエラーになります。これは上のような関数宣言を下のような関数式として書き直すと納得できます;
var hoge = function(num) {
var cnt = num;
return function(){
return ++cnt;
}
}
var hoge1 = hoge(1);
hoge = 0;
console.log(hoge1()); // 2
var hoge2 = hoge(10); // エラーになる
このように書くと、hoge=0
のところで変数 hoge
に 0
という数値が代入されてもともとの function(num) {...}
とは関係なくなってしまうので、最後の var hoge2 = hoge(10);
ではエラーが出る、というのが理解できるんじゃないでしょうか。また変数hoge1には次の行の hoge = 0
とは関係なくクロージャーがセットされているので、console.log(hoge1())
はエラーにならずに"2"が表示されます。
function hoge() {...}
と書くと分かりにくいかもしれませんが、やっているのは変数hogeへの関数オブジェクトの代入です。JavaScriptでの関数名というのはその関数オブジェクトを代入するための変数であると考えてください。var hoge = function() {...}
の形だとそれがより分かりやすいと思います。
関数宣言と関数式
関数宣言(function declaration)と関数式(function expression)
function foo() { // これは関数宣言
// 処理
}
var foo = function() { // これは関数式
// 処理
}
ではその関数を使えるタイミングが異なります。関数宣言だとどこに書いても関数を実行できますが、関数式だと宣言した後じゃないとその関数を使えません。なぜなら関数宣言はホイスティングされますが関数式はホイスティングされないからです。さらに関数宣言された関数はコンパイル時に定義されますが、関数式の関数は代入時に定義されるからです。