Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード Twitter

2011-04-24

普通の業務系PGには意外と知られていないJavaとJavaScriptの相違点10選

以前はJava EEの普通のWebアプリケーションで、JavaScriptはあくまでも利便性のために補助的に使うものという認識がありましたが、さすがに最近では普通の業務系のSI案件でもテーブル表示や入力補助などで高度なAjaxライブラリーの使用が当たり前のように求められるようになりつつあります。サーバーサイドのJavaScript技術といったものもありますが、そういった新しい技術を使わないまでも、ごく普通にある程度大きなJavaScriptの作成が必要になってきているということです。

もちろん、JavaJavaScriptはその名前にかかわらず、本来全く別の言語です。しかし、意図的に似た構文でロジックが書ける*1ため、兄弟の言語として認識している人も意外に多いのではないかと思います。しかし、使用できるライブラリーに違いがあるという点が一見してわかる最も大きな違いですが、基本的な言語の文法そのものでも意外に見落としがちな相違点がいくつか存在します。ここでは、その中でもJavaプログラマーの視点から見て特に気になったポイントを10個に絞り紹介します。

JavaScript文字列は基本型

見かけ上Java文字列JavaScript文字列文字列リテラル引用符にシングルクオーテーションが使える点を除くと、そっくりに作られています。そのため、意外と見落としがちなことですが、JavaScript文字列は言語上はオブジェクトではなく基本型の値です。Javaの場合、文字列はStringクラスのインスタンスで、変数は他のオブジェクト型の変数と同様に参照型となるのですが、JavaScriptの場合は数値やbooleanと同様の基本型の値として振る舞います。

ですから、文字列の等価性を比較する場合は、以下のように==演算子や===演算子を適用することができます。

var str = "Hello";
var str2 = "Hello World".substring(0, 5);

console.log(str == str2); // true

Javaプログラマー的な発想では、strとstr2は別々の文字列オブジェクトを参照しているため、最後の行の==による比較がfalseとなるように勘違いしそうですが、JavaScriptでは基本型として値の比較となるためtrueとなります。逆にJavaに戻った時にequals()を使うことを忘れてしまうとバグの原因となります。JavaJavaScriptにおけるこの文字列の扱いの違いは十分に理解して正しく使い分ける必要があります。

なお、上記のコードサンプルでconsole.log()メソッドデバッグ文を埋め込む場合に便利です。以前はalert()ダイアログを表示させてデバッグしていたものですが、最近のブラウザではconsoleオブジェクトが組み込まれているため、これを活用するのが便利でしょう。

(追記)JavaScriptの==演算子については、以下のエントリもご参照ください。JavaScriptで文字列の等価性を==で評価できるのは文字列が基本型だからなのか: 発火後忘失

基本型の違い

Javaにはintやdoubleなどの多数の基本型が定義されていますが、JavaScriptの場合基本型に相当するのは以下の3通り*2しかありません。

  • boolean型
  • number型
  • string型

つまり、

  • 基本型としてchar型はない
  • 基本型として整数実数の区別はない
  • byte、short、int、longといった精度による区別がない

ということですね。なお、JavaScript変数の基本型は(Javaには無い)typeof演算子を使って調べることができます。

var strValue = "Hello";
var booleanValue = true;
var intValue = 1;
var doubleValue = 1.0;

console.log(typeof strValue); // "string"
console.log(typeof booleanValue); // "boolean"
console.log(typeof intValue); // "number"
console.log(typeof doubleValue); // "number"

// 整数と浮動小数は区別がない
console.log(intValue == doubleValue); // true
console.log(intValue === doubleValue); // true

なお、Javaと同様にJavaScriptにも基本型に対応するラッパーオブジェクトとしてString、Number、Booleanというのがあります。これらのラッパーオブジェクトを使って文字列演算や型変換などのロジックを実行できます。

var strValue = "Hello";
var strWrapper = new String("Hello");

console.log(typeof strValue); // "string"
console.log(typeof strWrapper); // "object"

console.log(strWrapper.toUpperCase()); // "HELLO"

console.log(strValue == strWrapper); // true
console.log(strValue === strWrapper); // false

以上の例で、strWrapperの型はstringではなくobjectとなっていることに注意してください。だから、暗黙の型変換が行われる==比較ではstrValueと等価として判断されますが、型の変換を行わない===比較ではfalseと判断されています。

なお、JavaScriptでは通常ラッパーオブジェクトを明示的にnewで生成することは実際にはまれです。なぜなら、基本型に対してメソッド呼び出しの形で記述すると暗黙のラッパーオブジェクトに自動変換されるというしくみがあるからです。ですから、

console.log(strValue.toUpperCase()); // "HELLO"
console.log("Hello".toUpperCase()); // "HELLO"

のような書き方ができます。この糖衣構文によって、一見JavaScriptRubyScalaのような基本型のない純粋なオブジェクト指向の言語に見えてしまうのですが、実はそうではなくて、内部でラッパーオブジェクトが一時的に生成されていると考えるのが正しい理解です。

undefined値の存在

JavaScriptでは値が存在しないことを示す特別な値としてundefined値があります。nullはJavaにも存在しますが、undefinedはJavaScriptに固有の型です。

var a;
var b = null;

console.log(typeof a); // "undefined"
console.log(typeof b); // "object"

console.log(a == b); // true
console.log(a === b); // false

undefinedは上記のaのように初期値が代入されていない変数の値や型となっていますが、==比較ではnull値とのみ等価と判断されます。

console.log(a == ""); // false
console.log(a == false); // false
console.log(a == 0); // false
console.log(a == null); // true

ところが、if文の条件式のように強制的にboolean型が期待される場所ではfalseに変換されます。

if (!a) {
	console.log("########"); // 実行される
} 

また、数値に変換される場所では、NaNに変換されます。

console.log(a + 3); // NaN

なお、NaNの型はnumber型でなくて、undefined型となります。

var c = Math.sqrt(-1);
console.log(c); // NaN
console.log(typeof c); // "number"
console.log(isNaN(c)); // true

NaNはNot A Numberということなので、number型というのはちょっとおかしいように思えますが。

なお、undefined型は上記を含めて

の場合に登場します。以下のエントリも参考になります。

JavaScriptのundefinedというクセ者のいろいろ - 風と宇宙とプログラム

配列初期化の仕方、配列メソッド

Javaの場合配列初期化は中括弧{}で行いますが、JavaScriptの場合は角括弧[ ]で行います。

var a = [1, 2, 3];
var b = {1, 2, 3}; // エラー

なお、Javaと違いJavaScript配列は普通のオブジェクトと同様にメソッド呼び出しを使って要素を操作することができます。特に、push()などのメソッドを使って要素を追加することができます。JavaScript配列Javaで言うところのArrayListに近いと思った方がよいでしょう。

正規表現の扱い

正規表現の扱いは、JavaScriptJavaとで全く違っています。Javaの場合は、Javaで正規表現を扱うのは難しい - 達人プログラマーを目指してでも書いたとおり、かなり面倒なAPIを利用する必要がありますが、現在のJavaScriptではかなり強力な正規表現の仕組みが言語に組み込まれています。特にスラッシュを使った正規表現リテラルを使えば、RegExpオブジェクトを簡単に生成できます。

var regExp = /end$/;
console.log(regExp.test("The end")); // true
console.log(regExp.test("The end is near.")); // false

RegExpオブジェクトメソッドを使う以外にStringオブジェクトのいくつかのメソッド正規表現を利用できます。

var regExp = /\s/g;
console.log("This is a test.".replace(regExp, "-")); // "This-is-a-test."

JavaScript正規表現については、以下が参考になります。

no title

{}のブロックがスコープにならない。代わりに関数がスコープ

JavaScriptではJavaと異なり中括弧のブロックが変数のスコープにはなりません。

function test() {
    var a = 3;

    console.log(a); // 3;

    {
        var a = "Hello";
    }

    console.log(a); // "Hello";
}

以上のような独立したブロックだけでなく、if文やfor文のブロックもJavaと違って変数のスコープとはなりません。ここもJavaプログラマーは結構勘違いしやすいところですね。ただし、JavaScriptの場合は関数の中に関数を入れ子で定義できて、この関数変数のスコープになります。

function test2() {
    var a = 3;

    console.log(a); // 3;

    function inner() {
        var a = "Hello";
    }

    console.log(a); // 3;
}

関連して、JavaScriptでは変数定義の巻き上げという現象があり、varで宣言された場所によらず、ローカル変数関数の先頭から存在しているものとして扱われるので注意が必要です。

var a = "Hello";
function test3() {
    console.log(a); // undefined;
    
    var a = 3;

    console.log(a); // 3;
}

以上の例では、ローカル変数aは関数の先頭から存在していることになるため、直感に反して関数の先頭からグローバル変数にアクセスできなくなります。Javaと違い、JavaScriptのローカル変数関数の先頭でまとめて宣言するのが良いようです。

switch文の値に任意の型が使える

構文はJavaとそっくりですが、JavaScriptでは整数値に限らず、任意の値をcase式に指定することができます。でも、実質的に意味があるのは文字列や数値などの基本型の値です。

	switch (value) {
	case "Hello":
		console.log("文字列 " + value);
		break;
	case 1:
		console.log("数値 " + value);
		break;
	default:
		console.log("その他");
		break;
	}

なお、JavaでもようやくJava7からswtich文における文字列の比較が可能になりました。

Java7 体当たり/strings switch - 日々常々

最後に、JavaScriptのswitch文による値比較では===演算子のように厳密な型比較が行われ、暗黙の型変換は行われないことに注意する必要があります。つまり、"3"は3とは等しいとは見なされないということですね。値の比較に柔軟性を持たせられるRubyのcase構文とは違っているため、勘違いしないように注意が必要です。

定数の作成はfinalでなくてconst(だけど実質使えない?)

JavaScript1.5からはvarの代わりにconstキーワードを用いて定数を定義できます。

const PI = 3.141592;

しかし、残念ながらconstキーワードIEではサポートされていないようです。ですから、実質的にはJavaScriptには定数はないと思っておいた方がよいかもしれません。

なお、オブジェクトの変更を禁止するObject.freeze()メソッドはありますが、constと違い定数そのものを定義できるわけではありません。つまり、このメソッドオブジェクトに対して新しいプロパティの追加、削除や状態変更を禁止するために利用します。つまり、変数が参照するオブジェクトそのものの状態が不変になるのですが、それを参照する(ローカル)変数に対する再代入を防ぐものではありません。*3

関数が一級市民のオブジェクト*4

これはJavaScriptの最大の特徴の一つだと思いますが、JavaScriptでは関数そのものをオブジェクトとして変数に代入したり、パラメーターで受け渡ししたりすることができます。そして、関数オブジェクトクロージャーとして動作します。いまさらだけど、Java言語にはクロージャーがない - 達人プログラマーを目指して

このポイントは、昔であれば上級者のみ理解していればよい内容だったのですが、今ではほとんどのJavaScriptライブラリーが関数オブジェクトを様々なところで利用していますので、避けて通ることはできません。

関数オブジェクト関数定義を行う方法以外に関数生成式(関数リテラル)を用いて簡単に生成できます。

function calc(a, b, op) {
	return op(a, b);
}

var add = function(a, b) {
	return a + b;
}; // 関数オブジェクトを生成して変数に代入

var subtract = function(a, b) {
	return a - b;
}; // 関数オブジェクトを生成して変数に代入


var resultAdd = calc(1, 2, add);
var resultSub = calc(1, 2, subtract);

console.log(resultAdd); // 3
console.log(resultSub); // -1

なお、今時の他のスクリプト言語と違い、関数から値を返す場合にはreturn文が省略できないことに注意が必要です。(returnを忘れると戻り値がundefinedになってしまう。)

オブジェクトの生成方法とクラス

JavaScriptではオブジェクトリテラルを使って、オブジェクトを生成することができます。

var dog = {
    name : "ポチ",
    breed : "チワワ",
    bark : function() {
        return "わんわん";
    }
};

console.log(dog.name); // "ポチ"
console.log(dog.breed); // "チワワ"
console.log(dog.bark()); // "わんわん"

Javaと同じようにオブジェクトの中に含まれる値はプロパティ(フィールド)、関数メソッドと呼ばれます。ここでは、オブジェクトリテラルを使って、プロパティメソッドを最初から保持させていますが、オブジェクトに対しては後からこれらの要素を追加することもできます。

var dog = {};

dog.name = "コロ";
dog.breed = "ダックス";
dog.bark = function() {
	return "きゃんきゃん";
};

console.log(dog.name); // "コロ"
console.log(dog.breed); // "ダックス"
console.log(dog.bark()); // "きゃんきゃん"

このようにJavaScriptオブジェクトではJavaの様にpublicやprivateといったアクセス制御がなく、また、プロパティ値だけでなく、プロパティの定義やメソッドも動的に追加できます。このことは、独自に生成したオブジェクトだけでなく、Stringなどの組み込みのオブジェクトに対しても実施できます。

var strObject = new String("Hello");
strObject.test = function() {
	return "======" + this + "======";
};

console.log(strObject.test()); // "===== Hello ====="

なお、JavaScriptにはJavaのクラスに相当するものがありません。そのかわりに、コンストラクタ関数という特別な関数を定義することができます。コンストラクタ関数は以下の特徴をもった関数です。

  • 命名規約上、先頭の文字を大文字にする
  • new演算子の後に指定することで呼び出す

以下のように、コンストラクタ関数を定義すると、new演算子を使ってオブジェクトを生成できます。

// コンストラクタ関数
function Dog(name, breed) {
	this.name = name;
	this.breed = breed;
	this.bark = function() {
		return "わんわん";
	};
}

var dog = new Dog("ポチ", "チワワ");

console.log(dog.name); // "ポチ"
console.log(dog.breed); // "チワワ"
console.log(dog.bark()); // "わんわん"

console.log(typeof dog); // "object"

console.log(dog.constructor.name); //"Dog"
console.log(dog instanceof Dog); //true

ここで、typeof演算子を使って調べると変数dogの型はobjectなのですが、instanceof演算子を使うとオブジェクトの型がDogであることがわかります。構文的にもJavaScriptの場合はコンストラクタ関数オブジェクト変数の型と関連しており、クラスに近い役割を持っていることがわかります。なお、JavaScriptオブジェクトを理解するためにはプロトタイプという考え方を理解する必要がありますが、ここでは説明しません。以下の本を参考にしてください。

まとめ

外見(言語名、構文規則、命名規約など)がJavaとそっくりなため、JavaScriptJavaプログラマーであれば、何となく使えてしまいそうなイメージがあり、意味がわからないままWebや書籍のサンプルコードを貼付けて使っている人が多いかもしれませんが、詳しく調べていくと細かいところでかなり違っている部分があるため注意が必要です。最近は、社内システムのSI案件でもスクリプトとして短いコードを書くのみでなく、複雑なユーザーインターフェースの実現のため、相当の分量のJavaScriptコードを書かなくてはならないことも多く、一通り基本を勉強しておくべきかと思います。SI業界では多くの場合、Javaは習っても、JavaScriptをきちんと教えられる機会は少ないと思いますので。

JavaScriptの本はたくさん出ていますが、私が読んだ本としては、

初めてのJavaScript 第2版

初めてのJavaScript 第2版

をお勧めします。また、より本格的に勉強する本としてはやはり、サイ本として有名な

JavaScript 第5版

JavaScript 第5版

が良いですね。また、比較的基本がわかっている人なら

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

を読むと、効果的なJavaScriptコーディング技法が理解できます。

*1:多くのSI開発の現場ではオブジェクト指向設計など考慮せずifとforのみでロジックのみを書き下すという慣習があるため、特に差がないように見えてしまうところがあるのでは。

*2:後述のundefinedは除く

*3C++言語で言うところのポインタの定数かポインタの指す先が不変なのかの違いと似ている

*4JavaScriptでは関数が主役ということで、部分的に関数型言語風の記述も可能な側面はありますが、不変な変数という概念が弱いため、純粋に関数型のプログラムを作成するのは厳しいと思います。

mumoshumumoshu 2011/04/24 17:58 分かりやすい記事をありがとうございます!

一点ツッコミなのですが、
console.log(typeof c); // undefined
は"number"になるのではないでしょうか?(当方Google chromeで確認)

ryoasairyoasai 2011/04/24 18:08 ありがとうございます。確認しながら作成していたのですが、再度確認したら、確かに"number"となりました。どうやら勘違いだったみたいですね。本文の方は訂正しておきます。

tmtmstmtms 2011/04/25 00:40 最後のサンプルの dog.bark() の結果は "わんわん" じゃないでしょうか

ryoasairyoasai 2011/04/25 01:04 ミスをご指摘いただき、どうもありがとうございました。修正させていただきました。

platinumplatinum 2011/04/25 22:38 大変参考になる良い記事をありがとうございました。

platinumplatinum 2011/04/25 22:38 大変参考になる良い記事をありがとうございました。

iyoiyo 2011/04/26 00:17 「代わりに関数がスコープ」の最後の例,最後の表示されるのが何故4なのかどうも分かりません。「ローカル変数aは関数の先頭から存在していることになる」のに最初の結果がundefinedなのも分からないです。この辺りもう少し突っ込んでいただけないでしょうか

ryoasairyoasai 2011/04/26 02:27 すみません、4は凡ミスで3の間違いです。修正しておきました。
実際に確認していただけるとそうなることがわかりますが、varが関数内のどこで宣言されていても、そのローカル変数自体は関数全体で領域が確保されて存在することになります。だから、その関数内では同一名のグローバル変数は隠蔽されてアクセスできなくなります。でも、初期化はvar文の行で初めて行われるため、それまでは未初期化でundefined値となるみたいです。変数の領域は確保されているのだけれど、値は初期化されていないという状態になってしまうということですね。
このように、変数宣言の巻き戻しは混乱するので、本文でも説明したように関数の先頭で宣言するのがベストプラクティスとされています。

suuumosuuumo 2011/04/29 10:34 「実質的に」にならObject.freezeで定数として役割は果たせるだろ。
自分で「実質的に」って書いといて、形式の話にすり替えるとかワロタ

ryoasairyoasai 2011/04/29 11:24 Object.freeze()の件は後から追記したので変になってしまいましたね。

ryoasairyoasai 2011/04/29 11:30 constによる定数の宣言とObject.freeze()の違いについて補足説明を追記しました。