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

第4回JavaScriptの基礎知識#1

こんにちは、太田です。前回はクロスブラウザのパターンについてまとめました。今回はより具体的にJavaScriptの基礎的な部分からそこそこJavaScriptに慣れた方でも間違いやすいポイントを中心に解説します。

JavaScriptの背景知識

JavaScriptは(未だに)誤解されがちな言語です。まずはJavaScriptの背景から解説していきます。

(広義の)JavaScriptとはEcma Internationalによって策定されているECMA-262という規格(ECMAScript)を実装した処理系で実行される言語を指します。遠回りな表現になっていますが、これはJavaScriptのややこしさの一端を表しています。つまり、JavaScriptそれ自体に仕様があるわけではない、ということです。ECMAScriptと呼ばれる言語の仕様があって、その仕様に準拠した言語を(広義の)JavaScriptと(慣例的に)呼んでいるだけなのです。

ECMAScriptは比較的「ゆるい」言語です。ECMAScriptで規定された範囲については実装側がサポートすることを要求していますが、それ以外の部分について実装側で型やオブジェクトなどを追加することを許可しています。これはそもそも、ECMAScriptはNetscapeのJavaScriptとIEのJScriptをベースに共通部分を仕様化することで生まれた言語という歴史的な経緯の結果でもあり、同時にJavaScriptの進化(混乱?)の要因でもあります。

さて、上記でNetscapeのJavaScriptと書きました。そう、JavaScriptは元々Netscapeのものでした。Netscapeは開発もサポートも終了してしまっていますが、その系譜はMozillaに引き継がれています。先程「⁠⁠広義の)JavaScript」とも書きましたが、これに対する「狭義のJavaScript」とはNetscape、MozillaのJavaScriptを指します。

よって、単にJavaScriptと書くとMozillaのJavaScriptを指すのか、クロスブラウザなJavaScriptを指すのかはっきりしない、ということです。実際に、MozillaのJavaScriptエンジンであるSpiderMonkeyはJavaScript 1.5、1.6(Firefox 1.5⁠⁠、1.7(Firefox 2⁠⁠、1.8(Firefox 3⁠⁠、1.8.1(Firefox 3.5⁠⁠、1.8.2(Firefox 3.6)とバージョンアップを続けていますが、このバージョン番号が広義のJavaScriptのバージョンと混同されてしまうといったことはよくあります(今回は扱いませんが、FirefoxのJavaScriptは独自の進化を遂げ、ECMAScriptとはかなり距離ができています。ECMAScriptがMozillaのJavaScriptから離れたとも言えます⁠⁠。なお、この連載では単にJavaScriptと書いたときはもちろん広義のJavaScriptを指します。これに対して、例えばMozilla Developer Center - MDCにおけるJavaScriptとは、多くの場合でMozillaのJavaScriptを指しています。⁠Mozillaの)JavaScript 1.5はECMA-262 3rd editionとほぼ同等で、Opera、Safari、ChromeなどもJavaScript 1.5をひとつの指標としてJavaScriptエンジンを実装している面があるので、⁠OperaのJavaScriptエンジンはJavaScript 1.5に対応」といった表現も間違いではないのがややこしいところです。

JavaScriptのオブジェクトと型

JavaScriptのデータ型は大きく分けて「プリミティブ値」「オブジェクト」の2種類に分けることができます。プリミティブ値を細かく分けると、数値、文字列、真偽値、null、undefinedの5種類です(nullとundefinedはやや特殊ですがここではまとめておきます⁠⁠。オブジェクトはキー(文字列)とバリュー(プリミティブ値とオブジェクト)で表現される複合データで、配列や関数などもオブジェクトに含まれます。

JavaScriptのprototype

JavaScriptはprototypeベースの言語です。null、undefined以外のデータはprototypeを介して拡張することが可能です。これはプリミティブな値も例外ではありません。

例えば下記のようにString.prototypeを拡張することで、文字列リテラルにも任意のメソッドを追加することができます。

String.prototype.trimの実装
if (!String.prototype.trim) {
  String.prototype.trim = function(){
    return this.replace(/^\s+|\s+$/g, '');
  }
}
' string \u00a0 \t \v \f '.trim() // -> 'string'

なお、String.prototype.trimはECMA-262 5th editionで追加されたメソッドです。Chrome 4やFirefox 3.6ではすでにネイティブに実装されています(Chrome、Firefoxではtrim以外にもtrimLeft、trimRightも実装していますが、標準ではありません⁠⁠。

こういったprototypeの拡張は便利ですが、問題もあります。prototypeに追加されたプロパティはfor inで列挙されてしまうという問題です。for inを使うことがないモノについてはほとんど問題ありませんが、ObjectやArrayなどでは重大な問題です。

Array.prototypeの拡張とIEでのfor in
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function(func, that) {
    for (var i = 0, len = this.length; i < len; i++) {
      if (i in this) {
        func.call(that, this[i], i, this);
      }
    }
  };
}
var a = [1,2];
a.forEach(function(v,i,a){
  alert(v);// 1, 2
});
for (var i in a) {
  alert(i);// 0, 1, forEach
}
for (var i in a) {
  if (a.hasOwnProperty(i)) {
    alert(i);// 0, 1
  }
}

IEで上記のコードを実行すると途中でforEachがアラートされます。hasOwnPropertyというメソッドで自分自身がもつプロパティかどうかチェックすることで回避することはできますが、なるべくならシンプルに書きたいところでしょう。よって、⁠ネイティブな)prototypeを拡張する場合はfor inが使われていない、使ってしまうことがないと保証できる場合に限る必要があります。逆にfor inを使う場合はprototypeが拡張されていないかどうかに注意する必要があります。

Object.prototypeについては、拡張すると影響範囲が非常に大きいため禁止としたほうがよいでしょう。なお、ECMAScript 5では安全にprototypeを拡張する方法が用意されているので、Object.prototypeに任意のメソッドを追加することが可能になります。

なお、これらはネイティブなprototypeを拡張する場合の注意ですので、自前で定義した関数のprototypeを拡張するのはまったく問題ありません。

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の最重要ポイントであるクロージャーを中心に見ていきたいと思います。

おすすめ記事

記事・ニュース一覧