関数と関数スコープ

概要

一般的に言うと、関数とは外部 (再帰の場合は内部) から 呼ばれる ことのできる「サブプログラム」です。プログラムそのもののように、関数は関数本体と呼ばれる連続した文で構成されます。値を関数に 渡す 事ができ、関数は値を 返す 事ができます。

JavaScript において、関数は第一級オブジェクトです。すなわち、関数はオブジェクトであり、他のあらゆるオブジェクトと同じように操作したり渡したりする事ができます。具体的には、関数は Function オブジェクトです。

一般

全ての関数は実際には Function オブジェクトです。Function オブジェクトのプロパティとメソッドについての情報は Function をご覧ください。

意味のある値を返すためには、返す値を指定する return 文が関数内になくてはなりません。return で明示的に値を返していない関数は undefined を返します (new キーワードとともに constructor が呼び出された場合は特別で、constructor で明示的に値を返さなかった場合、new 演算子式の値は、新しく作られたオブジェクトになります)。

関数の仮引数 (パラメータ) には、関数呼び出しにおいて実引数 (アーギュメント) が渡されます。実引数は、関数に「値渡し」されます: 関数の中で引数の値を変更しても、その変更はグローバルスコープもしくは呼び出し元の関数内には反映されません。オブジェクト参照も「値」ですが、こちらは特別です: 参照されているオブジェクトのプロパティを関数の中で変更すると、次の例にあるように、その変更を関数の外部から見ることができます:

 /* 関数 'myFunc' を宣言 */
 function myFunc(theObject)
 {
   theObject.brand = "Toyota";
 }
 
 /*
  * 変数 'mycar' を宣言
  * 新しいオブジェクトの生成と初期化
  * 'mycar' への参照をオブジェクトに代入
  */
 var mycar = {
   brand: "Honda",
   model: "Accord",
   year: 1998
 };

 /* 'Honda' を表示 */
 window.alert(mycar.brand);

 /* オブジェクト参照を関数に渡す */
 myFunc(mycar);

 /*
  * オブジェクトの 'brand' プロパティの値は関数によって変更されたので
  * 'Toyota' と表示される
  */
 window.alert(mycar.brand);

this キーワードは現在実行中の関数を参照しません。よって、関数内部であっても、名前によって Function オブジェクトを参照しなければなりません。実際は推奨されませんが、別のやり方として、arguments.callee プロパティを使う事も出来ます。

関数を定義する

関数を定義するのには 3 つの方法があります。

関数宣言 (function 文)

関数を宣言するための特殊な構文があります。(詳細は function 文 を参照)

function name([param[, param[, ... param]]]) {
   statements
}
name
関数名。
param
関数に渡される引数の名前。関数は最大 255 個の引数を持てます。
statements
関数の本体を構成する文。

関数式 (function 演算子)

関数式は、関数宣言と似ており、同じ構文を持っています。(詳細は function 演算子 を参照)

function [name]([param] [, param] [..., param]) {
   statements
}
name
関数名。省略する事ができ、その場合関数は無名関数と見なされます。
param
関数に渡される引数の名前。関数は最大 255 個の引数を持てます。
statements
関数の本体を構成する文。

アロー関数式 (=>)

注記: アロー関数式は実験的な機能、また Harmony (EcmaScript 6) 提案の一部であり、各ブラウザで広くサポートされてはいません。

アロー関数式は短縮構文を持ち、また関数の this 値を語彙的に結びつけます (詳細は arrow functions を参照):

([param] [, param]) => {
   statements
}

param => expression
param
引数の名前。0 個の引数は () で示すことが必要です。1 個だけの引数で丸括弧は必須ではありません。(例えば foo => 1)
statements または expression
複数の文は中括弧で括らなければなりません。単一の式では、中括弧は必須ではありません。式は、関数の暗黙的な戻り値でもあります。

Function コンストラクタ

他の全てのオブジェクトと同じように、new 演算子を使って Function オブジェクトを作成する事ができます。

new Function (arg1, arg2, ... argN, functionBody)
arg1, arg2, ... argN
関数で仮引数名として使われる、0 個以上の名前。それぞれが、妥当な JavaScript 識別子に相当する文字列、もしくはそういった文字列のカンマで分割されたリストでなくてはなりません。例えば "x" 、"theValue"、もしくは "a,b" などです。
functionBody
関数定義を構成する JavaScript 文を含む文字列。

Function コンストラクタを関数として (new 演算子を使わずに) 呼び出しても、コンストラクタとして呼び出すのと同じ効果があります。

注記: Function コンストラクタによる関数の生成は推奨されません。これは、JS エンジンによる最適化の一部を妨げたり他の問題を引き起こしたりする場合がある文字列が関数本体として必要であるためです。

arguments オブジェクト

arguments オブジェクトを使って、関数内部で関数の引数を参照することができます。arguments を参照して下さい。

スコープと関数スタック

スコープと他の関数を呼び出す関数についての幾つかのセクション

再帰

関数は自分自身を参照し、呼び出す事ができます。関数がそれ自身を参照するには 3 つの方法があります。

  1. 関数名
  2. arguments.callee
  3. その関数を参照するスコープ内変数

例えば、次のような関数定義を考えます。

var foo = function bar() {
   // ここで命令文が実行される
};

関数本体の内部では、次の 3 つは全て等価です。

  1. bar()
  2. arguments.callee()
  3. foo()

自分自身を呼び出す関数の事を 再帰関数 といいます。いくつかの点で、再帰はループと類似しています。両方とも同じコードを複数回実行し、両方とも (無限ループ、この場合は無限再帰を回避するための) 条件を必要とします。例えば、次のループは、

var x = 0;
while (x < 10) { // "x < 10" はループ条件
   // 何かする
   x++;
}

再帰関数とその関数の呼び出しに変換する事ができます。

function loop(x) {
   if (x >= 10) // "x >= 10" は終了条件 ("!(x < 10)" と等価)
      return;
   // 何かする
   loop(x + 1); // 再帰呼び出し
}
loop(0);

しかしながら、単純な反復ループにできないアルゴリズムもあります。例えば、(DOM などの) ツリー構造の全てのノードを得るには、再帰を使うのがより簡単です。

function walkTree(node) {
   if (node == null) // 
      return;
   // ノードに対して何かする
   for (var i = 0; i < node.childNodes.length; i++) {
      walkTree(node.childNodes[i]);
   }
}

関数 loop と比べると、ここでは再帰呼び出し自体がそれぞれたくさんの再帰呼び出しを行っています。

どんな再帰アルゴリズムでも非再帰アルゴリズムに変換する事が可能ですが、ロジックが非常に複雑になったり、スタックの使用が必要になることがあります。実際、再帰それ自体がスタックを使用しています。関数スタックです。

次の例でスタック的な振る舞いを見る事ができます。

function foo(i) {
   if (i < 0)
      return;
   document.writeln('begin:' + i);
   foo(i - 1);
   document.writeln('end:' + i);
}
foo(3);

これは次のように出力されます。

begin:3
begin:2
begin:1
begin:0
end:0
end:1
end:2
end:3

入れ子の関数とクロージャ

関数の内部に関数を入れ子にする事ができます。入れ子にされた (内側の) 関数は、それを含む (外側の) 関数に対してプライベートです。それは同時に クロージャ を形成します。

クロージャとは、自由変数と同時にその変数を束縛する (式を「閉じて」いる) 環境を持ち得る式 (主として関数) である。

入れ子にされた関数はクロージャであり、これは入れ子にされた関数がそれを含む関数の引数と変数を「継承」できるということを意味します。言い換えれば、内側の関数は外側の関数のスコープを持っています。

要約すると、

  • 内側の関数には外側の関数内の文からのみアクセスできる。
  • 内側の関数はクロージャを形成する。内側の関数は外側の関数の引数と変数を使う事ができるが、外側の関数は内側の関数の引数と変数を使う事はできない。

次の例は入れ子の関数を示しています。

function addSquares(a,b) {
   function square(x) {
      return x * x;
   }
   return square(a) + square(b);
}
a = addSquares(2,3); // 13 を返す
b = addSquares(3,4); // 25 を返す
c = addSquares(4,5); // 41 を返す

内側の関数がクロージャを形成するので、外側の関数を呼び出して、外側と内側の関数の両方の引数を指定する事ができます。

function outside(x) {
   function inside(y) {
      return x + y;
   }
   return inside;
}
fn_inside = outside(3); 
result = fn_inside(5); // 8 を返す

result1 = outside(3)(5); // 8 を返す

変数の保持

inside が返されたときに x が保持されているということに注目してください。クロージャは、参照しているすべてのスコープ内の引数と変数を保持します。呼び出されるたびに違う引数が与えられる可能性があるので、クロージャは外側の関数が呼び出されるたびに作らなければなりません。メモリが解放されるのは、返された inside にアクセスできなくなったときだけです。

これは他のオブジェクトの参照を保持することと同じですが、参照を直接設定するのではなく、また調査ができないため理解しにくいことがよくあります。

多重に入れ子にされた関数

関数は多重に入れ子にすることができます。つまり、関数 (A) が関数 (B) を含み、関数 (B) が関数 (C) を含む事ができます。このとき関数 B と C はともにクロージャを形成するので、B は A にアクセスでき、C は B にアクセスできます。さらに、C が アクセスできる B は A にアクセスできるので、C は A にアクセスすることもできます。このように、クロージャは複数のスコープを持つ事ができます。クロージャはそれを内包する関数のスコープを再帰的に内包しています。これは スコープチェーニング (scope chaining) と呼ばれます。(なぜ「チェーニング」と呼ばれるのかは後で説明します。)

次の例を考えてみて下さい。

function A(x) {
   function B(y) {
      function C(z) {
         alert(x + y + z);
      }
      C(3);
   }
   B(2);
}
A(1); // 6 (1 + 2 + 3) とアラートされる

この例では、CByAx にアクセスしています。これが可能な理由は、

  1. BA を含むクロージャを形成する。つまり、BA の引数と変数にアクセスできる。
  2. CB を含むクロージャを形成する。
  3. B のクロージャには A が含まれているので、C のクロージャには A が含まれており、よって CB A 両方の引数と変数にアクセスできる。言い換えると、CBA のスコープをその順番に 繋いで (chains) いる。

しかしながら、その逆は真ではありません。AC にアクセス出来ません。なぜなら AB のどんな引数や変数にもアクセスできず、C はその B の変数だからです。よって、CB に対してのみプライベートなままです。

名前の衝突

クロージャのスコープ内で 2 つの引数または変数が同じ名前を持っていると、 名前の衝突 が起こります。スコープは内側にあるものほど優先順位が高いので、最も内側にあるスコープが最も高い優先順位を持ち、最も外側にあるスコープの優先順位が最も低くなります。これがスコープチェーンです。チェーンの最初が最も内側のスコープで、最後が最も外側のスコープです。次の例を考えてみて下さい。

function outside() {
   var x = 10;
   function inside(x) {
      return x;
   }
   return inside;
}
result = outside()(20); // 10 ではなく 20 を返す

命令文 return x において、inside のパラメータ xoutside の変数 x の間に名前の衝突が起こっています。この場合のスコープチェーンは {inside, outside, グローバルオブジェクト} です。したがって insidexoutsidex よりも優先順位が高いので、10 (outsidex) ではなく 20 (insidex) が返ってきます。

Function コンストラクタか関数宣言か関数式か

以下のものを比較してみて下さい。

  1. Function コンストラクタによって定義され、変数 multiply に代入された関数
    var multiply = new Function("x", "y", "return x * y;");
    
  2. multiply と命名された関数の 関数宣言
    function multiply(x, y) {
       return x * y;
    }
    
  3. 変数 multiply に代入された、無名関数の関数式
    var multiply = function(x, y) {
       return x * y;
    }
    
  4. 変数 multiply に代入された、func_name と命名された関数式
    var multiply = function func_name(x, y) {
       return x * y;
    }
    

これらは全ておおよそ同じ働きをしますが、いくつか微妙に異なる点があります。

  • 関数名と関数が代入された変数の間には違いがあります。
    • 関数名は変える事ができませんが、関数が代入された変数は再代入する事ができます。
    • 関数名は関数本体の内部でのみ使用する事ができます。関数本体の外側でそれを使用しようとするとエラー (その関数名がそれより前に var 文によって宣言されていれば undefined ) になります。例えば、
      var y = function x() {};
      alert(x); // エラーを投げる
      

      関数名は Function の toString メソッド によってシリアライズした時にも見る事ができます。

      一方、関数が代入された変数はそのスコープ内でのみ有効で、そのスコープは関数が宣言されたスコープを含んでいる事が保証されています。

    • 4 つめの例にあるように、関数名はその関数が代入される変数と違っていても構いません。お互いの間に関連性は有りません。
  • 関数宣言は同時にその関数名と同じ名前の変数を作成します。よって、関数式で定義されたものと違って、関数宣言で定義された関数は定義されたスコープ内でその名前によってアクセスできます。
    function x() {}
    alert(x); // 文字列にシリアライズされた x を出力する
    

    次の例は、関数名と関数が代入された変数の間には関係がないということを示しています。「関数変数」が他の値に代入されても、依然として同じ関数名を持っています。

    function foo() {}
    alert(foo); // アラートする文字列には関数名 "foo" を含む
    var bar = foo;
    alert(bar); // アラートする文字列には変わらず関数名 "foo" が含まれている
    
  • Function によって定義された関数は関数名を持ちません。しかし、JavaScript エンジンの SpiderMonkey では、その関数をシリアライズされた形式にすると "anonymous" という名前を持っているかのように表示されます。例えば、alert(new Function()) はこのように出力されます。
    function anonymous() {
    }
    

    この関数は実際には名前を持っていないので、anonymous は関数内部でアクセスできる変数ではありません。例えば、次の文はエラーになります。

    var foo = new Function("alert(anonymous);");
    foo();
    
  • 関数式や Function コンストラクタで定義されたものとは違い、関数宣言で定義された関数は、関数自体が宣言される前に使用する事ができます。例えば、
    foo(); // FOO! とアラートされる
    function foo() {
       alert('FOO!');
    }
    
  • 関数式で定義された関数は現在のスコープを継承します。つまり、関数がクロージャを形成します。一方、Function コンストラクタで定義された関数は (あらゆる関数が継承する) グローバルスコープ以外はどんなスコープも継承しません。
  • 関数式と関数宣言で定義された関数は一度しか解析されませんが、Function コンストラクタで定義された関数はそうではありません。つまり、Function コンストラクタに渡された関数本体を表す文字列が、評価されるたびに必ず解析されます。関数式は毎回クロージャを作成しますが、関数本体は再解析されないので、"new Function(...)" よりは関数式の方がまだ高速です。したがって Function コンストラクタはできる限り避けるべきでしょう。
    ただし、Function コンストラクタの文字列を解析することで生成された関数内で入れ子にされている関数式や関数宣言は、繰り返し解析されないことに注意してください。例えば:
    var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
    foo(); // 関数本体の文字列で "function() {\n\talert(bar);\n}" の部分は再解析されません

関数宣言はとても簡単に (しばしば意図せずに) 関数式に変化します。関数宣言は以下のような時には関数宣言ではなくなります。

  • 式の一部になった時
  • 関数またはスクリプト自体の「ソース要素 (source element)」でなくなった時。「ソース要素」はスクリプトや関数本体の中で入れ子にされていない文の事です。
    var x = 0;               // ソース要素
    if (x == 0) {            // ソース要素
       x = 10;               // ソース要素ではない
       function boo() {}     // ソース要素ではない
    }
    function foo() {         // ソース要素
       var y = 20;           // ソース要素
       function bar() {}     // ソース要素
       while (y == 10) {     // ソース要素
          function blah() {} // ソース要素ではない
          y++;               // ソース要素ではない
       }
    }
    

例:

  • // 関数宣言
    function foo() {}
    
    // 関数式
    (function bar() {})
    
    // 関数式
    x = function hello() {}
    
  • if (x) {
       // 関数式
       function world() {}
    }
    
  • // function 文
    function a() {
       // function 文
       function b() {}
       if (0) {
          // 関数式
          function c() {}
       }
    }
    

条件付きで関数を定義する

関数は //function 文// (ECMA-262 Edition 3 標準の許可された拡張) か Function コンストラクタを使って条件付きで定義することができます。function 文は ES5 の strict モードで許可されないといったことに注意してください。加えて、この機能はブラウザ間で一貫して動作していませんので頼るべきではありません。

次のスクリプトでは、zero 関数は決して定義されず、呼び出すことはできません。 'if (0)' が偽であると評価されるからです。

if (0) {
   function zero() {
      document.writeln("This is zero.");
   }
}

条件式が 'if (1)' となるようにスクリプトが変更されれば、関数 zero は定義されます。

注: これは関数宣言のように見えますが、他の文の中に入れ子にされているので、実際には関数式 (または文) です。関数宣言と関数式の違い を参照して下さい。

注: SpiderMonkey 以外の JavaScript エンジンの中には、名前の付いた関数式を全て関数宣言と間違ってみなすものがあります。これによって常に偽となる if 条件式の中にあっても zero が定義されてしまいます。条件付きで関数を定義する時には、無名で関数を定義してそれを変数に代入するのがより安全です。

if (0) {
   var zero = function() {
      document.writeln("This is zero.");
   }
}

イベントハンドラとしての関数

JavaScript では、DOM イベントハンドラは関数です (他の DOM 言語バインディングでは handleEvent メソッドを持つオブジェクトであるのに対して) 。その関数には、第一番目かつ唯一のパラメータとしてイベントオブジェクトが渡されます。他のパラメータと同じように、イベントオブジェクトを使う必要が無ければ仮引数のリストから省略できます。

HTML ドキュメントの中でイベントのターゲットになり得るものには、window (Window オブジェクト、フレームを含む) 、document (HTMLDocument オブジェクト) 、要素 (Element オブジェクト) などがあります。HTML DOM では、イベントのターゲットはイベントハンドラプロパティを持ちます。このプロパティは、onfocus のように小文字にしたイベント名の頭に "on" を付けたものです。DOM Level 2 Events では、イベントリスナを追加するのにより強力な代替手段が提供されています。

注: イベントは DOM の一部であり、JavaScript の一部ではありません。(JavaScript は DOM へのバインディングを提供しているに過ぎません。)

次の例では window の "focus" イベントハンドラに関数を代入しています。

window.onfocus = function() {
   document.body.style.backgroundColor = 'white';
};

関数が変数に代入されていれば、その変数をイベントハンドラに代入する事ができます。次のコードでは変数 setBGColor に関数を代入しています。

var setBGColor = new Function("document.body.style.backgroundColor = 'white';");

この変数を使って、いくつかの方法でイベントハンドラに関数を代入できます。これらはその方法のうちの 2 つです。

  1. スクリプト内で DOM HTML イベントプロパティを使う
    document.form1.colorButton.onclick = setBGColor;
    
  2. HTML イベント属性
    <input name="colorButton" type="button"
       value="Change background color"
       onclick="setBGColor();"/>
    

    この方法でセットされるイベントハンドラは、実際には、その属性に基づいて名付けられ、指定されたコードをラッピングしている関数です。"setBGColor()" に丸括弧が ( 単に "setBGColor" ではなく) 必要なのはこういった理由からです。これは以下と等価です。

    document.form1.colorButton.onclick = function onclick(event) {
       setBGColor();
    };
    

    この関数にイベントオブジェクトがパラメータ event として渡されている事に注目してください。これにより指定したコードの中でイベントオブジェクトを使うことができます。

    <input ...
        onclick="alert(event.target.tagName);"/>
    

関数を参照する他のプロパティと同じように、イベントハンドラはメソッドとしてふるまう事ができ、this はイベントハンドラを持つ要素を参照します。次の例では onfocus によって参照された関数が呼ばれており、その中では thiswindow と同値です。

window.onfocus();

JavaScript 初心者によくある間違いは、変数の最後に丸括弧、もしくは丸括弧とパラメータの両方を追加してしまう事、すなわち代入する時にイベントハンドラを呼び出してしまうことです。丸括弧が足されていると、イベントハンドラを呼び出して返された値 を代入してしまう事になります。これは多くは (その関数が何も返さなければ) undefined になり、イベントハンドラそのものは代入されません。

document.form1.button1.onclick = setBGColor();

イベントハンドラにパラメータを渡すには、次のようにイベントハンドラを他の関数でラッピングしなければなりません。

document.form1.button1.onclick = function() {
   setBGColor('some value');
};

関数内部のローカル変数

arguments: 現在実行中の関数に渡された引数を格納している配列風のオブジェクト。

arguments.callee : 現在実行中の関数を示す。

arguments.caller  : 現在実行中の関数を呼び出した関数を示す。

arguments.length: 関数に渡された引数の数を示す。

例: フォーマットされた数値を返す

次の関数は、数値の先頭にゼロを足して固定長にした形で表される文字列を返します。

// この関数は先頭にゼロを足して固定長にした文字列を返す
function padZeros(num, totalLen) {
   var numStr = num.toString();             // 戻り値を文字列に初期化する
   var numZeros = totalLen - numStr.length; // ゼロの数を計算する
   for (var i = 1; i <= numZeros; i++) {
      numStr = "0" + numStr;
   }
   return numStr;
}

次の文で padZeros 関数を呼び出します。

var result;
result = padZeros(42,4); // "0042" を返す
result = padZeros(42,2); // "42" を返す
result = padZeros(5,4);  // "0005" を返す

例: 関数が存在するかどうか確認する

typeof 演算子を使うと関数が存在するかどうかを確かめる事ができます。次の例では、window オブジェクトが noFunc という関数のプロパティを持つかどうかを確かめるためのテストが行われます。もし持っていたら、それが使われます。そうでなければ、他の行動が取られます。

 if ('function' == typeof window.noFunc) {
   // noFunc() を使う
 } else {
   // 何か他のことをする
 }

if のテストの中で、noFunc への参照が使われているのに注目してください。関数名の後に括弧 "()" が無いので、実際の関数は呼ばれません。

関連情報

Document Tags and Contributors

Contributors to this page: Taken, ethertank, yyss, Hfjapancom, Shoot, Nanto vi, Mgjbot, Potappo
最終更新者: yyss,