これでできる! クロスブラウザJavaScript入門

第4回 JavaScriptの基礎知識#1

この記事を読むのに必要な時間:およそ 4 分

JavaScriptの関数

JavaScriptで特に特徴的なのは関数の扱いです。関数もオブジェクトに含まれると書いた通りで,関数をオブジェクトのプロパティにすることや,変数に入れ替えるなど,自由自在に操作することができます。こういった性質をもった関数をプログラミング用語で第一級関数(JavaScriptの関数はファーストクラスオブジェクトである,ともいう)と呼びます。すでに何度か登場したaddEvent関数はブラウザの実装にあわせて関数の定義をわけるようにしていますが,これができるのは関数がファーストクラスオブジェクトだからです。

イベント登録用関数

var addEvent;//変数を用意
  if(document.addEventListener) {// IE以外
    addEvent = function(node,type,handler){
      node.addEventListener(type,handler,false);
    };
  } else if (document.attachEvent) {// IE用
    addEvent = function(node,type,handler){
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    };
  }

関数には大きく分けて2種類の定義方法があります。

関数の定義方法

var A = function(){
};
function B(){
}

このAとBはよく似ていますが,微妙に違いがあります。まず,前者は関数を変数Aに代入する形になっています。よって代入が行われるまで変数Aはundefinedな状態です。それに対してBはこのコンテキストの実行前に評価されるので,Bより上に位置するコードもBを呼び出すことができます。関数を定義する位置をコントロールできるので,ソースの可読性を高めることが可能です。

関数宣言の呼び出し

B();
function B(){
}

また,AとBをそれぞれ文字列に変換してみると,Aはfunction(){},Bはfunction B(){}となります。BにはBという名前が含まれています。つまり,Bは自分自身がBという関数であることを知っているわけです。実際,IE以外のブラウザではnameというプロパティで関数の名前が取得できます(B.name === 'B')⁠この名前によって,エラーが起きた際のデバッグがしやすくなるので,Bを使うようにするのがよいと言えます。

さて,ここで問題です。次のサンプルコードを実行したとき最後のalertに表示されるのは1, 2, 3のどれでしょうか?

関数定義のクイズ

if(true) {
  function someFunc(){
    return '1';
  };
} else {
  function someFunc(){
    return '2';
  };
}
var notSomeFunc = function someFunc(){
  return '3';
};
alert(someFunc());// 1 or 2 or 3 ?

正解は,⁠すべて⁠です。ちょっと嫌らしい答えですね。すみません。というのも,この問題はブラウザによって回答が異なり,Firefoxは1,Opera/Safari/Chromeは2,IEは3になります。

なぜこうなるのか,を説明します。まず,ECMAScriptでは関数宣言が重複したときは,後ろで定義されたものを優先します。そのルールに従ってOpera/Safari/Chromeは2を返します。Firefoxでは,関数宣言の外側の条件文をみて関数の定義を決定するという独自拡張をしているので,1を返します。最後にnotSomeFuncに代入しているsomeFuncは関数宣言ではない(FunctionExpression)ので,このsomeFuncは外から参照できないはずですが,IEは関数宣言と同じように扱ってしまうのでsomeFuncを上書きしてしまうので,3を返します。

さて,もう一度addEvent関数に戻ります。と,いい加減しつこくなってきたので,実装を変えてみます。

無名関数による関数定義

var addEvent = (function(){
  if(document.addEventListener) {
    return function(node,type,handler){
      node.addEventListener(type,handler,false);
    };
  } else if (document.attachEvent) {
    return function(node,type,handler){
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    };
  }
})();

まず重要なのは無名関数とそれを呼び出している部分です。つまり,次の部分です。

無名関数と呼び出し

(function(){
// 関数内部
})();

function(){}で関数を定義して,その関数を () で囲って関数宣言(FunctionDeclaration)ではなくFunctionExpressionであることを明確にして,そのすぐ後ろの () で関数を呼び出しています。なお,関数の周囲の括弧は関数宣言ではないことを明確にするための括弧なので,元々関数宣言でないことが明確な場合は省略できます。

そして,もうひとつのポイントは関数が関数を返している部分です。

関数を返す関数

function A(){
  return function B(){
    return 'B';
  }
}
A();// function B
A()();// 'B'

このように,関数を返す関数(または関数を引数にする関数など)をプログラミング用語で高階関数と呼びます。この高階関数はクロージャーを正しく理解する上極めて重要な要素となります。

最後に,addEventの定義方法はほかにもいくつかあります。よりシンプルに実装するなら3項演算子がよいでしょう。

addEvent関数の定義方法:3項演算子

var addEvent = (document.addEventListener) ?
    function(node,type,handler){
      node.addEventListener(type,handler,false);
    }
  : function(node,type,handler){
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    };

また,関数が呼び出されたときに振り分ける方法も一般的です。

addEvent関数の定義方法:関数内部の分岐

function addEvent(node,type,handler){
  if (document.addEventListener) {
    node.addEventListener(type,handler,false);
  } else {
    node.attachEvent('on' + type, function(evt){
      handler.call(node, evt);
    });
  }
};

変則的ですが,関数内で自分自身を書き換えることも可能です。

addEvent関数の定義方法:関数内での書き換え

function addEvent(node,type,handler){
  if (document.addEventListener) {
    addEvent = function (node,type,handler) {
      node.addEventListener(type,handler,false);
    }
  } else {
    addEvent = function (node,type,handler) {
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    }
  }
  addEvent(node,type,handler);
}

後半の2つは関数宣言のメリットである記述した場所より上のコードから呼び出すことができる点がポイントです。

まとめ

今回はJavaScriptの基礎的な部分を中心に解説しました。次回はJavaScriptの最重要ポイントであるクロージャーを中心に見ていきたいと思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/