JavaScriptでの関数の特徴や注意点

JavaScriptで使われる関数の特徴や注意点を思いついた限り書いていこうと思います。勉強し始めて間もない人には厳しいかもしれません。ある程度サンプルコードを書いたりした人にとっては、「でJavaScriptの関数って結局なんなの?」って思うところを書いてみたつもりです。

JavaScriptでの関数はオブジェクトである

関数オブジェクト(Functionオブジェクト)

まずはコードから。

1
2
3
4
5
6
7
8
9
10
11
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はもはや関数オブジェクトではないからです。

なので

1
2
3
4
5
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での色々な関数

普通の関数

1
2
3
4
5
6
7
// 関数の定義
function hoge() {
alert("Hello, World!");
}

// 関数の実行
hoge();

JavaScriptを勉強し始めたらまず目にするものではないでしょうか。

無名関数/匿名関数

1
2
3
4
5
6
7
// 関数の定義
var hogeFunc = function() {
alert("Hello, World!");
}

// 関数の実行
hogeFunc();

第一級オブジェクトの特徴の一つ、『変数に格納可能である』というのを思い出してください。そうなってますよね。この記述は関数リテラルと呼ばれるもので、ここではfunction()...でもって名前のない関数を定義した上でそれをhogeFuncという名前の変数に代入しています。hogeFuncという変数に代入されるのは関数リテラルへの参照です。以下のように配列リテラル['a','b','c','d']を変数hogeに格納して配列にしているのと同じようなものです;

1
var hoge = ['a','b','c','d']; 配列リテラルを変数hogeに代入して配列を定義

ちなみにかのWikipediaいわく、「リテラル」というのは

コンピュータプログラミングにおいてリテラルは、ソースコード内に値を直接表記したものをいう。数値、文字列、関数などさまざまな型のものが存在し、それぞれの表記方法も言語によって異なる。即値ともいう。
リテラル - Wikipedia

だそうです。

JavaScriptでクラスのようなものを作ろうとしたら、この無名関数をコンストラクタ関数として使ったりします。この場合は、『関数リテラルの構文で関数オブジェクトを生成してhogeFuncという名前の変数に参照代入している』という感じに捉えることができます。詳しくはJavaScriptでのオブジェクト指向プログラミングを読んでください。

名前付き関数

1
2
3
4
5
6
7
8
// 関数の定義
var hogeFunc = function hoge() {
alert("Hello, World!");
}

// 関数の実行
console.log(hogeFunc.name); // "hoge"
hogefunc();

コロン関数/連想配列関数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 関数の定義
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でのオブジェクト指向プログラミングを読んでみてください。おそらくそっちを読んでから見ると印象がだいぶ変わると思います。

高階関数

1
2
3
4
5
6
7
function hoge(func,x,y) {
return func(x,y);
}
function add(x,y) {
return x + y;
}
console.log(hoge(add,1,1)); // 2

hogeが高階関数です。関数を引数にとっています。第一級オブジェクトの性質の一つ、『プロシージャや関数のパラメータとして渡すことができる』というのに当てはまってますね。

無名即時関数

1
2
3
4
5
6
// 関数の定義
(function() {
alert("Hello, World!");
}() );

// 関数の実行は関数の定義と同時に行われます

名前付き即時関数、無名即時関数ともにfunction自体を”()”でくくっている点、さらに一番後ろに”()”がついているのが即時関数の特徴です。これは関数リテラルをなにかの変数に直接代入せずに直接呼び出していることになっています。

即時関数ですが、最後の”()”の位置をどうするかで2通りの書き方があるようです。でもって推奨されているのは”{}”の後に”()”を書く方だそうです;

1
2
( function() { ... }() ); // 推奨
( function() { ... } )(); // 非推奨、らしい

即時関数についておまけ

functionを”()”でくくらなくても即時関数にすることができます(参考:即時関数(function(){ … })()の別の書き方いろいろ - 泥のように)。書き方が変わると戻り値が変わるのが特徴です。

1
2
3
4
5
6
7
8
9
(function(){ ... }()); // 戻り値は普通

+function(){ ... }(); // 戻り値は数値になる

-function(){ ... }(); // 戻り値は数値になる

!function(){ ... }(); // 戻り値は論理値になる

void function(){ ... }(); // 戻り値は常にundefinedになる

まぁ”()”が一番使いやすそうということです。

名前付き即時関数

1
2
3
4
5
6
// 関数の定義
(function hogeFunc() {
alert("Hello, World!");
}) ();

// 関数の実行は関数の定義と同時に行われます

JavaScriptでは関数自体もグローバルオブジェクト(Windowオブジェクト)のプロパティになります。そのため、関数をグローバル空間で定義すると関数自体もグローバル変数に割り当てられることになります。一方、即時関数はグローバル変数に設定される前に実行されて破棄されます。つまり使い捨てられるわけです。なので即時関数には名前をつけないのが普通です。

クロージャー

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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は関数になっていて、なおかつそれぞれが干渉し合わないようになっています。

さて、このクロージャーですが関数を上書きすると少し変わった挙動をします。

1
2
3
4
5
6
7
8
9
10
11
12
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が上書きされた後なのでエラーになります。これは上のような関数宣言を下のような関数式として書き直すと納得できます;

1
2
3
4
5
6
7
8
9
10
11
12
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)

1
2
3
function foo() { // これは関数宣言
// 処理
}

と関数式(function expression)

1
2
3
var foo = function() { // これは関数式
// 処理
}

ではその関数を使えるタイミングが異なります。関数宣言だとどこに書いても関数を実行できますが、関数式だと宣言した後じゃないとその関数を使えません。なぜなら関数宣言はホイスティングされますが関数式はホイスティングされないからです。さらに関数宣言された関数はコンパイル時に定義されますが、関数式の関数は代入時に定義されるからです