はじめに
「JavaができればJavaScriptできるよね?」とか言う人に実際に会ったことはないのですが、人には言ってたりします。
もちろん実のところはそう簡単にはいきません。
いきませんが、2018年も年末になってすら、ふと迷い込むとJavaScriptはJSPのおまけぐらいに思われていたりするわけです。さすがにJavaアプレットと混同している人はそんなに…いや、いるっぽいですね。私のパーソナライズの結果だといいんですが。
甚だしくは新人でない人がそのようなことをもっともらしく言ってみたり、90年代の知識で(今は2010年代の後半なんですよ?驚きですよね…)、JavaScriptをちょっとしたhtmlの<marquee>アクセント</marquee>程度にしか考えてなかったりするわけです。
JavaScriptがJavaプログラマにとって絶妙な加減で難しい位置にいるのも確かです。JavaScriptは頭にJavaと付いているだけあって、一応にもJavaの親の友達の子供ぐらいの関係にはあります。要するに他人なんですが、他人でありながらもJavaの幼馴染のようなもので、Javaプログラマならなんとなくで結構書けてしまううぐらいには似ています。しかし本質的には赤の他人なので、そういう立場で接していると地雷を踏んでしまい、よくわからないので深入りはしないようにしよう、となってしまうわけです。
この記事ではなんとなくJavaScriptできるんだけど、なんとなくでしかないJavaプログラマに対して、JavaScriptを学ぶ際に知っておけるとよいことを並べています。これだけでJavaScriptができるようになるとは思いませんが、その助けになれば幸いです。
なお、この記事の原本はけっこう昔に書いたので、多少古く、Symbolやasync/awaitなどの最近のJavaScriptらしいものは端折っています。来年だともうそこに触れないわけにはいかないので、これがJavaの知識ですんなりJavaScriptに入れる最後のチャンスです、ということにしておいてください。個人的には、JavaScriptのコア部分さえ分かれば最新のJavaScriptの理解は容易だと思います。
JavaScriptとか本当に何も知らないんだけど、という人はごめんなさい。とりあえず文法ぐらいはまあわかるよ、という人向けです。技術的におかしいものがあれば遠慮なく突っ込んでください。あと、本当かよと思ったらnode.js環境とか用意して実際にやってみてください。Javaに慣れると(Javaにも今やJShellがありますが)REPL環境のことを忘れがちですよね。
型
まずは型の話からスタートしましょう。JavaScriptは動的型付けです。動的型付けというのは、型がないのではなく型をあらかじめ固定しておかないことですので、型自体はあります。主要な型(のようなもの)はtypeof
演算子で確認することができます。
var x = 1;
console.log(typeof x); // number
x = "str";
console.log(typeof x); // string
x = function() {};
console.log(typeof x); // function
JavaScriptの型の特性はおおよそJavaと似た仕組みで考えることができます。JavaScriptで特に利用頻度の高い型はstring
、number
、boolean
、object
、function
の5つでしょうか。
このうちfunction
は実は型ではなく、object
の一つなのですが、ここでは便宜上の型として扱っています。また、実際の型名は仕様書(5.1 Edition)やMozilla Developer Network上はすべて先頭大文字(String、Number、Boolean、Objectなど)ですが、Stringという型とそれをラップするObject型のStringオブジェクトがあって非常に紛らわしいので、型名についてはtypeof
の判定値であるstring
のように表記しています。この表記はJSDocやTypeScriptなどでは一般的です。
JavaScriptの型または便宜上の型と、Javaの型との大きな違いは、intやdoubleなどの数値型はnumber
ただ1つになることと、string
が基本型であること、function
が存在していることです。また、JavaScriptにはそれ以外の型もいくつかあります。それぞれ順番に見てみましょう。
string
JavaのStringのようなものです。ただし、JavaScriptにおいて、char型は存在しないため、''
と""
に区別は一切ありません。シェルスクリプトのように変数展開もしません。(新しいJavaScriptでは``を使うと${}
で変数展開できます。)一般的なコーディングスタイルでは''
と""
はソースコード中でどちらか片方だけを使うことが推奨されています。
string
に対して使用できるメソッドもStringとよく似ているというか、ほぼそのままです。ただし、少し前のStringなので、現代を生きるJavaプログラマからすると、Stringのサブセットになるので注意が必要です。*equalsIgnoreCase()やisEmpty()*のような便利なメソッドはありません。しかし、startsWith()
のように後から入ったものもあります。
JavaScript独自のメソッドもありますが、古いものはほとんど気にする必要はないメソッドばかりです。新しいものはいくつか、Javaと異なるメソッドに分岐しています。
→MDNのString.prototype
慣れるまで少し戸惑うかもしれないのは*equals()*が無く、===
で比較できることでしょう。==
でもできますが、現代的JavaScriptでは==
は使いません。
また、Javaと異なり、charAt()
だけでなく配列と同様に[]
で一文字ずつ取得可能です。
console.log('abc' === "abc"); // true
console.log('abc'[1]); // 'b'
number
JavaScriptにおいて、number
はJavaのdoubleと同等の機能を持つ唯一の数値型です。64ビットの浮動小数点型で、doubleがそうであるように、整数ももちろん扱うことができます。逆に、JavaScriptにはintのような整数用の型というものはなく、すべてnumber
です。
ただし、ビット演算するときには32ビット整数として扱われるので、ビット演算時にはちょっと注意が必要です。逆に、このことを利用して少数を整数化するテクニックもあります。
var x = 0.1;
console.log(x); // 0.1
x = (0.1 | 0);
console.log(x); // 0
また、結局はdoubleですので、32ビット整数であるint範囲は表現できますが、64ビット整数であるlong範囲は単体のnumber
で扱うことはできず、丸めた値として扱われてしまいます。このため、たとえばIDが64ビット数値になってしまったTwitter APIなどでは、JavaScriptのために文字列版のIDが用意されていたりします。
console.log(1234123412341234123); // 123412341234123200
System.out.println(1234123412341234123L); // 1234123412341234123
もちろんこれは別にJavaScriptの限界を示すものではなく、必要に応じて*java.math.BigDecimal*のようなライブラリを使うか作れば、任意精度演算できます。
boolean
boolean
はJavaのbooleanとだいたい100%ぐらい同じです。*equals()*がないぐらい。
object
Javaでも参照型と呼ばれてる、いわゆるオブジェクトの型です。JavaのObjectに相当するのはJavaScriptでもObject
であり、JavaのようにすべてのオブジェクトはObject
を継承しています。
しかし、JavaのObjectよりも重要な性質があります。Object
については後述します。
function
関数の"型"です。Javaにもjava.util.functionやラムダ式ができたので、大雑把なところでは理解が早いと思いますが、ああいうのの、もうちょっとネイティブなものだと思ってください。Javaのラムダ式はインターフェースありきでしたが、JavaScriptの場合はもっとフリーダムです。
関数という"型"があるということは、変数に関数型の値を代入できるということです。関数型の値はfunction宣言またはfunction式で生成できます。
function hoge(x) {
console.log('xx:' + x);
}
var f = function(x) {
console.log('f:' + x);
};
f('abc'); // f:abc
f = function(x) {
console.log('f2:' + x);
};
f('xyz'); // f2:xyz
なお、function
は、実際には仕様上は型ではなく、実行できるコードを持つobject
という特殊な位置づけになっています。そのため、Functionオブジェクト(Java風に言えば、Functionのインスタンス)というのが正確なところで、function
もobject
の持つ特性をすべて持っています。
var f = function() {
};
console.log(f instanceof Function); // true
console.log(f instanceof Object); // true
オートボクシングと型変換
number
やstring
、boolean
にはラッパー関数Number
やString
、Boolean
があります。JavaScriptではオートボクシングとは言いませんが、必要に応じてオートボクシングと同じことをしてくれます。たとえば、文字列定数"abc"
はstring型でStringのインスタンスではないのですが、"abc".substring(2)
などとしてメソッドを使うことができます。
ラッパー関数は基本的にはユーティリティメソッドを提供してくれるものであり、new String()
やnew Number()
して使うことはありません。が、使えてしまうのでJava以上に警戒が必要です。(※Javaでも別な視点から、new String()とか、new Integer()は使わないですよね。)new
してできたString
オブジェクトはstring
型ではないので、同じようには使用できず、あえて使う機会もありません。
var str = 'abc';
var STR = new String(str);
console.log(str === 'abc'); // true
console.log(STR === 'abc'); // false
console.log(typeof str); // 'string'
console.log(typeof STR); // 'object'
console.log(str instanceof String); // false;
console.log(STR instanceof String); // true;
function
は実はすでにFunctionオブジェクトですので、この分類には入りません。
var f1 = new Function();
var f2 = function() {};
console.log(f1 instanceof Function); // true
console.log(typeof f1); // function
console.log(f2 instanceof Function); // true
console.log(typeof f2); // function
ただ、やはりnew Function()
を使う機会も通常はありません。
また、String()
やNumber()
、Boolean()
などは型変換する関数としても使えます。が、どの変換も別な変換イディオムがあるため、直接的にはあまり使われません。
var str = '100';
console.log(str + 200); // 100200
console.log(Number(str) + 200); // 300
console.log(String(1+1) + 0); // 20
console.log(Boolean(0)); // false
// 代替イディオムの一例
console.log(+str + 200); // 300
console.log('' + (1+1) + 0); // 20
console.log(!!0); // false
正規表現リテラル
typeof
ではobject
に分類されますが、正規表現リテラル(定数)があります。Javaの*Pattern.compile()*に相当するのはnew RegExp()
ですが、固定の正規表現であれば、正規表現リテラルを利用できます。
var regexp = /.+/; // regexp = new RegExp('.+'); と同等
console.log(regexp instanceof RegExp); // true
booleanへの型変換
JavaScriptではすべての型はbooleanとして評価できます。0
や空文字''
、null
、undefined
がfalse
として扱われます。
if (0) {
// 実行されない
}
論理演算子による記法
論理演算子 ||
、&&
は値に対して型変換を行ってbooleanで評価を行い、型変換を行う前の値を結果として返します。
このことを利用して、論理演算子によって、値が設定されていない場合のデフォルト値を設定することができます。
var a;
...
var b = a || 'test';
console.log(b); // aが0、''、null、undefinedでなければその中身、そうでなければ'test'
a && console.log(a); // ショートサーキット(短絡評価)も働きます。aがfalsyでなければ中身を表示します。
ただし、後者のような、if文の代わりに使うような使い方は一般的に嫌われるスタイルです。(前者のような値同士の組み合わせでも、好まれない場合はあります。)
Object
JavaとJavaScriptで、すべてのオブジェクトがObjectというルートを持っているのは同じですが、ObjectとObject
の利用感はまったく違います。JavaScriptのオブジェクトはすべて単なる連想配列であり、機能としてはJavaで言うところの*Map<String, Object>*です。これがJavaScriptの言語として重要な要素となっています。
順を追って見ていきましょう。
基本的性質
JavaのObjectインスタンスはあまり直接的に使用しませんが、JavaScriptのObject
は連想配列やPOJO、場合によっては関数を持たせて無名クラスオブジェクトのように使うなど、さまざまな用途で利用するため、使用頻度の高いオブジェクトです。
Object
のインスタンスはnew Object()
で生成することもできますし、単に{}
で生成することもできます。一般的にはコーディングルールにより{}
で生成することが推奨されていることが多いです。このときに、JSON風に初期値を与えることもできます。むしろJSONがここから生まれたのだから、JSON風っておかしくないかと思うかもしれませんが、JSONのように仕様に縛られておらず、JSONよりも柔軟です。
var x = {}; // x = new Object();
x = {
'a b c': 'xy' + 'z',
'test': 123,
example: /.+/
};
console.log(x['test']); // 123
console.log(x['a b c']); // xyz
console.log(x['example']); // /.+/
オブジェクトリテラルのキーは、自動的に文字列として扱われます(上記のexample
がそうです)。キーは必ず文字列ですので、記号や空白が入って妙な解釈になるのでなければ''
は不要なケースが多いのですが、通常はコーディングルールによって使い分けがなされるか、どちらかに偏らせます。
また、JavaScriptのオブジェクトは[]
または.
を使うことでその連想配列からget/putができます。
文法上の制約により、キーが空白や記号の場合、.
でアクセスすることはできませんが、値の出し入れでは両者は同じように扱うことができます。
var x = {}; // new Object();
console.log(x.test); // undefined
x.test = 123; // オブジェクトにキーがない場合でも代入すると生成されます
x['abc'] = 'xyz'; // 同じく生成されます
console.log(x.test); // 123
console.log(x['test']); // 123
console.log(x.abc); // xyz
これも一般的にはキーが変数であったり.
では使えない文字列である場合のみ[]
を使い、定数キーは.
を使うなど、使い分けを行うことでコード上の意味を統一させることが多いです。
また、一方でJavaと同じように、Object
はすべてのobject
型の祖です。したがって、上記の特性はすべてのオブジェクトで通用する事柄ですので注意が必要なシーンがあります。たとえば、Dateインスタンスのように直接Object
ではないものもこの性質をもっています。JavaScriptにおいてはすべてはオブジェクトですので、Dateそのものにキーを追加することもできます。
var x = new Date();
x.test = 'abc'; // 問題なし
Date.xyz = 123; // 問題なし
console.log(x.test); // abc
console.log(Date.xyz); // 123
しかし、通常はこのようなことはしませんし、しないように注意すべきです。
[]演算子
.
と同じと言いましたが、もちろん文法上の違いにより、[]
の中には文字列のみならず、どのような型であれ含めることが可能です。ただし、Object
はあくまで*Map<String, Object>*ですので、[]
の中の値はString()
されて文字列として評価されます。(String()
すると、null
は'null'
になり、undefined
は'undefined'
になり、それ以外の場合はtoString()
かvalueOf
が呼び出されます)
従って、次のような挙動を示します。
var x = {'1': 9, 'a': 3, 'y': 2, 'test': 4, 'null': 8 };
console.log(x[1]); // 9
console.log(x['1']); // 9
var y = { toString: function() { return 'test'; } }
console.log(x[y]); // 4
console.log(x[null]); // 8
仕組みが分かっていれば単純なルールですが、必ずしも直感的ではないので落とし穴になりやすい部分です。
var x = {};
var y = { a: 3 }
x[y] = 5;
console.log(x[y]); // 5
console.log(x[{a: 3}]); // 5
// まるで、オブジェクトをキーにできているように見えますが、、、
console.log(x[{a: 1000}]); // 5
console.log(x['[object Object]']); // 5
console.log(y.toString()); // '[object Object]'
console.log(x); // { '[object Object]': 5 }
// 単にString()されてるだけ
Array
Arrayは自動伸長できる配列です。あえてJavaらしく言うとjava.util.ArrayListみたいなものですが、JavaScriptにおいては普通の配列です。object
がnew Object()
で生成できるように、new Array()
で生成できますが、一般的には単なる[]
が好まれます。
var x = []; // x = new Array();
var y = ['a', 'b', 'c'];
console.log(y[1]); // b
しかしJavaScriptを正しく理解するためには、Arrayが我々の知っている配列であるという意識はいったん捨ててください。
Arrayは配列のように使えるobject
であり、[]
の効果も同じです。Objectの時点で*Map<String, Object>*であることはすでに見ました。
*Map<String, Object>*であり、[]
に含めた数値がtoString()されて検索されるということは、そもそもobject
であれば、メモリが許す限りいくらでもオブジェクトを詰め込めることになります。object
の時点で、自動伸長できる配列のように取り扱うことができるというわけです。
// Array
var x = [];
x[0] = 1;
x[1] = 2;
console.log(x[1]); // 2
console.log(x['1']); // 2
// Object
var y = {};
y[0] = 1;
y[1] = 2;
console.log(y[1]); // 2
console.log(y['1']); // 2
// Date(Objectを継承している適当なオブジェクト例)
var z = new Date();
z[0] = 1;
z[1] = 2;
console.log(z[1]); // 2
console.log(z['1']); // 2
ArrayがArrayとしての機能を果たすのは、Array.prototypeが揃える各関数が使えるかどうか以外には、
見た目上はlengthプロパティだけです。Arrayのlengthは、最も大きい添え字+1になる性質を持っています。
var x = [];
x[1000] = 0;
console.log(x.length); // 1001
var y = {};
y[1000] = 0;
console.log(y.length); // undefined
これだけが配列の機能です。もちろん、そのように使えるということと、そうであるということは違います。何らかのオブジェクトをArrayの代わりに使うことは避けなければなりません。
Arrayが必要なときはArrayを使い、Arrayの添え字には(非負の)整数のみを使用するべきです。これは単純にソースコードの意味の問題だけでなく、処理系への影響が大きくなります。
# Object
time node -e 'let x = {}; for (let k = 0; k < 1000; k++) for (let i = 0; i < 1000000; i++) x[i] = i;'
# Array
time node -e 'let x = []; for (let k = 0; k < 1000; k++) for (let i = 0; i < 1000000; i++) x[i] = i;'
圧倒的にx = []
のときの方が速いはずです。
Objectとプロトタイプとプロトタイプチェーン
JavaScriptはクラスベースではなくプロトタイプベースの言語であるというようなことは聞いたことがあるかと思います。JavaScriptのオブジェクトにはプロトタイプチェーンと呼ばれる仕組みがあります。プロトタイプというのは、オブジェクトにカスケードされたオブジェクトです。
と言ってもなかなか想像が付きにくいので、まず、Javaで複数のMapからキーを探すことを考えてみましょう。
複数のMapを束ねて、キーを探索するときに、一つ目のMapになければ、二つ目のMapを参照し、二つ目のMapになければ三つ目というように、見つかるか最後のMapにたどり着くまで探すようにします。
また、このMap群にキーをセットするときは、一つ目のMapにputするものとします。
こうすると、先頭のMapは、すべての値を持っているように見えますし、値を変更することもできます。しかし、裏側のMapの値は実際には変化しません。
public class CascadedMap extends HashMap<String, Object> {
private CascadedMap parent;
@Override
public Object get(String key) {
if (containsKey(key)) {
return super.get(key);
}
if (parent == null) {
return NULL;
}
return parent.get(key);
}
@Override
public Object put(String key, Object v) {
return super.put(key, v);
}
}
JavaScriptのオブジェクトは自動的にこれをやってくれる仕組みを持っていて、そのオブジェクトの連なりをプロトタイプチェーンと呼び、一つ目のMapすなわち対象のオブジェクトから見た二つ目のMapをプロトタイプと呼びます。
実際にも、node.jsやChromeのJavaScriptエンジンの場合、__proto__
という特殊なプロパティがあり、これがプロトタイプを指しています。
あるキーで問い合わせがあったとき、そのキーが自分自身になければ、__proto__
に問い合わせを行います。__proto__
のObjectでも同じように自分自身にあれば返し、無ければ__proto__
を辿るということを繰り返します。値.や[]で代入するコンテキストの場合はそれと異なり、そのオブジェクトのみに*put()*されます。
public class JSObject {
private HashMap<String, Object> myValue;
private JSObject __proto__;
public Object get(String key) {
if (myValue.containsKey(key)) {
return myValue.get(key);
}
if (__proto__ == null) {
return JS_UNDEFINED;
}
return __proto__.get(key);
}
public void put(String key, Object v) {
myValue.put(key, v);
}
}
プロトタイプの設定とインスタンス
__proto__
という名前から感じられるとおり、__proto__
は普通は直接触れることのない変数ですし、触ることのできない処理系もあります。では通常はプロトタイプをどうやって設定するかというと、prototype
というキーとnew
演算子を使います。
Date.prototype.test = 'abc';
var x = new Date();
console.log(x.__proto__ === Date.prototype); // true
console.log(x.test); // 'abc'
これはJavaな人からすると、newという概念を分解して考えた方が分かりやすいです。Javaのnewは指定されたクラスのメモリを確保して、オブジェクトを割り当て、コンストラクタを呼び出すという複合的な演算子です。
JavaScriptにおけるnew X()
あるいはnew演算子とは、空のObjectを生成し、その__proto__
にX.prototype
を代入したあと、X()というコンストラクタを呼び出す役目を持った演算子です。
また、あるオブジェクトZがXのインスタンスである(Z instanceof X
がtrueを返す)というのは、Z.__proto__ === X.prototype
(または再帰的にZ.__proto__.__proto__ === X.prototype
・Z.__proto__.__proto__.__proto__ === X.prototype
…のいずれか)を満たしている場合ということになります。
ちなみに、newが__proto__
へX.prototype
を代入するのは本当に単なる代入に相当するものなので、次のようなことにもなります。
var x = new Date(); // xの__proto__はDate.prototype
// newした後でprototypeのプロパティを追加
Date.prototype.test = 'abc';
console.log(x.__proto__ === Date.prototype); // true
// 問題なくアクセスできる
console.log(x.test); // 'abc'
prototype
はこのように割と簡単にアクセスできるので、過去には、prototype
を修正することでブラウザ間のメソッド実装状況の差異を無くすprototype.jsというライブラリが流行ったころもありました。
コンストラクタ
さて、上記では既存のDateや適当なX()でごまかしましたが、JavaScriptにおけるクラスのように見えるもの、new
の後ろの名前は何でしょうか?
console.log(typeof Date); // function
function
です。Javaにおけるクラスという概念はいったん忘れてください。JavaScriptにはJavaにおけるクラスはありません。クラスのようなものを実現するために、new
演算子とプロトタイプチェーンがあると思ってもらった方がわかりやすいと思います。(これは逆から見ると、プロトタイプのようなものを実現するために余計なクラス機構を取り入れてるJavaという言語、と考えることもできます)
new
演算子の機能はもう少し正確には、「指定された関数のprototype
プロパティをプロトタイプに持つオブジェクトを生成し、そのオブジェクトをthisとした上で、関数を実行する」ということになります。
function hoge(v) {
console.log(v);
}
var x = new hoge('abc'); // 'abc'
console.log(x instanceof hoge); // true
したがって、コンストラクタとして扱うつもりの関数のprototype
プロパティに値を入れておくと、そのインスタンスで値を取り出すことができます(プロトタイプチェーンで説明したように、書き込みはそのインスタンスになります)。その値が関数であれば、メソッドとして使うことができます。
function hoge() {
}
hoge.prototype.value = 1023;
hoge.prototype.test = function(v) {
console.log(v);
}
var x = new hoge();
x.test('abc'); // 'abc'
console.log(x.value); // 1023
なお、これはたまたまメソッドのように見えていたり、コンストラクタのエッセンスだけを取り出したりしたわけではありません。JavaScriptにおいてメソッドとはオブジェクトのプロパティとして存在する関数のことを言います。また、JavaScriptにおいてコンストラクタとは単にそのようにふるまう関数です。
ただし、通常はやはりコーディング規約により、コンストラクタ関数は、大文字で始める(Javaにおけるクラス命名規則と同じ)などが規定されています。
thisとメソッド
JavaScriptにおけるメソッドは、プロパティとして存在する関数ですが、ただの関数と異なる点があります。それがthis
です。Javaとは異なり、JavaScriptのthis
はどこにでも存在するのが話をややこしくしていますが、this
はすべての関数の0番目の引数だと考えるのが一番簡単な理解です。
もちろん、通常の関数呼び出しで0番目の引数というものは指定しません。関数として使った場合には、this
はglobal
という環境を表すオブジェクトになっています。
function hoge() {
console.log(this === global);
}
hoge(); // true
しかし、メソッドとしてオブジェクトとともに使うとthis
が設定されるようになります。
function hoge() {
console.log(this === global);
}
var v = {};
v.method = hoge;
hoge(); // true
v.method(); // false;
v['method'](); // false;
このときのthis
は何かというと、もちろん想像通りのオブジェクトが入っています。
function hoge() {
console.log(this === v);
}
var v = {};
v.method = hoge;
hoge(); // false
v.method(); // true;
v['method'](); // true;
Pythonのself
が省略されているようなものと考えるのがいいかもしれませんね。少し戸惑うであろう点は、上記のhoge
はv
の専属メソッドではなく、かつメソッドは単なる関数のプロパティでしかないということです。
function hoge() {
console.log(this === v);
}
var v = {};
v.method = hoge;
hoge(); // false
v.method(); // true;
v['method'](); // true;
var x = {};
x.method = v.method; // 呼び出しではなく値(関数)の取り出し
x.method(); // false;
おそらくJava風にthisを考えると何が起こっているか分からないと思いますが、JavaScriptの仕組みは非常に単純です。
this
は明示的には書かれない第0引数で、.
や[]
を使って関数を取り出してそのまま実行したときには、その取り出し元のオブジェクトが設定される
とみなすことができます。それ以外の特別な機能はthis
にはありません。第0引数の指定を省略するとglobal
が設定されるというわけです。
もちろんそうすると、メソッドをonClickなどのイベントハンドラに設定するのが難しくなります。イベントハンドラは関数のみしか設定できず、thisを指定した形での呼び出しができないためです。
そこで、Function.prototype.apply()やFunction.prototype.call()、
Function.prototype.bindによって、this
を設定した呼び出しや、this
を設定した呼び出しを行う関数を作っておくことができるようになっています。
function hoge() {
console.log(this === v);
}
var v = {};
// thisを設定して実行
hoge.call(v); // true
// thisをあらかじめbindした関数を作る
var b = hoge.bind(v);
b(); // true
JavaScriptのスレッドモデル
非同期でイベントドリブンなんだと言われても、想像がつきにくいのがJavaScriptの実行モデルです。setTimeoutで呼び出される関数はスレッドセーフでなくていいのか?いや、考えてみるとonClickで呼び出される関数はどうなんだ?いや、ロックとかあったっけ?処理の途中で割り込まれたら値はどうなるんだ?などと不安になったりします。シグナルハンドラや割込みプログラミング、マルチスレッドの経験があったりすると余計に混乱することだと思います。
JavaScriptにおいては、プログラマから見たときのスレッドは基本的に1つしか存在しません。またコードがスレッドセーフになっているのか心配する必要もありません。割込みやイベントはすべてキューイングされて、順序良く処理されます。
しかし、イベント駆動モデルを意識しないとまるでマルチスレッドで動いているように見えるかもしれません。また、ブラウザ環境では画面描画イベントも同じスレッドで実行されるため、スレッドが1つしかないことを忘れると、画面描画を止めてしまいがちです。
非同期あるいはJavaScriptというのはおそらく最初に想像するよりも非常に単純な仕組みです。たった1つのスレッドはmainスレッドではなく*Executors.newSingleThreadExecutor()*だと考えられます。
全てのJavaScriptプログラムは、このExecutorServiceでのみ実行されます。クリックのような画面操作イベントもタスクとして登録され、先行するタスクが終わるまでキューイングされます。
ExecutorService service = Executors.newSingleThreadExecutor();
public void setTimeout(JSFunction f, long millisec) {
new Thread(() -> {
TimeUnit.MILLISECONDS.sleep(millisec);
service.submit(f);
}).start();
}
// 画面操作も平等にsubmitされる
public void click(JSFunction onClick) {
service.submit(onClick);
}
public void main(String maincode) {
service.submit(() -> execute(maincode));
}
setTimeout
を呼び出しても、すぐに制御が戻り、時間が来ると関数fがsubmitされて実行されることがわかるかと思います。同時に、先行してsubmitされたタスクが無限ループなどで処理が終わらないと、後続の処理が並列実行されることはなく、永久に実行されないこともわかるかと思います。
実際に、JavaScriptの実装はおおまかにはだいたいこのようになっていて、背後では複数のスレッドが動いていますが、JavaScriptのコードを呼び出すのは常に同じたった一つのスレッドです。このスレッドの仕組みはExecutorServiceの中身がそうであるように、イベントループと呼ばれます。GUIプログラミングで使われているようなものとまったく同じです。
イベントループによる実行は、コードがスレッドセーフである必要はありませんが、一つの処理が終わらない限り、どのようなイベントが発生しても処理できなくなります。したがってJavaScriptが無限ループに入るとキューが消化されず、画面描画も滞ってフリーズしてしまうというわけです。
なお、現代的なJavaScriptでは、Web Workerという仕組みにより、このイベントループを複数作り、マルチスレッドで動かすこともできますが、依然としてスレッドセーフを意識する必要はありません。(逆に言えば、そういう密接な連携ができません)
戸惑いがちな、または現代的な構文など
説明上飛ばしたりしたその他の解説です。
===と!==
==
を使わない話はしましたが、==
は型変換付の比較になります。たとえば'1' == 1
ですが、'1' !== 1
です。==
を使いこなして安全なコードを書くのは楽ではなく意味もないので、現代的なJavaScriptでは一般的に===
と!==
の使用が推奨されています。
console.log('1' == 1); // true
console.log('1' === 1); // false
文末の;省略
文末の;は省略することができます。まったく書くべきでないという派閥もありますが、何が起こるか理解するまでは、しっかり書いておいた方がいいと思います。
なお、この「省略できる」という機能は、正確には;を挿入する機能なので、例えば次のような悲劇を起こします。
function x() {
return
100;
}
console.log(x()); // undefined
こういう事態を防ぐためにも、なるべく静的解析のあるエディタやlintを導入するとよいでしょう。
変数宣言
varによる変数宣言はJavaと多少似ているようでまったく違うネームスコープを持っています。varにより宣言した変数は関数の中括弧{}
内で有効です。たとえば、次のような挙動になります。
for (var i = 0; i < 100; i++) {}
console.log(i); // 100
for (var i = 0; i < 200; i++) {} // 2回目のiの宣言です
今のJavaScriptには、Javaや一般的なC言語スタイルの言語と同じように{}
内でのみ使える変数を宣言するためのconst
やlet
がありますので、新しい環境であればそちらを使ってください。
for (let i = 0; i < 100; i++) {}
console.log(i); // undefined
この記事が全面的にvar
なのは古いJavaScriptに慣れてしまった人に違和感を覚えていただかないようにであって、通常はもはやvar
を使うシーンはありません。IE対応などでどうしても直接的にvar
を使わなければならないのであれば、静的解析のあるエディタやlintを導入しましょう。
ネームスコープのための即時関数
varはこのような奇妙なスコープを持っているので、古くはネームスコープを利用するために即時関数が利用されてきました。
(function() {
...
})() // 引数が詰まってることもあります
このようなコードは特に古いコードではしょっちゅう見かけるかと思いますが、これはそのためのイディオムです。
class構文
現代的なJavaScriptでは、すでにclass構文が導入されています。
class Test {
constructor(v) {
this.value = v;
}
method1() {
console.log(this);
}
}
const t = new Test(3);
t.method1(); // Test { value: 3 }
そんなものがあるなら最初に紹介しろと思われるかもしれません。
詳細はMDNのクラスを読んでいただくのが一番早いのですが、これは別に新しい仕組みを導入したわけではなく、今までの機能をきちんと書けるようにしただけです。
Javaプログラマにとって、これはおそらく危険な構文で、Javaっぽく書けるせいで余計に理解から遠のくのではないかと思います。しかし、今から書く場合には、まずclass構文で書くようにしましょう。
[]と数値インデックス
これは単なる落とし穴ですが、[]内がtoString()されたとしても[]で普通の配列と同じように使えるので問題ないだろ、と思っていると、次のような恐ろしい挙動に遭遇したりします。
var i;
var a = [0, 1 ,2, 3, 4];
var b = [];
for (i = 0; i < 10; i++) {
b[i] = a[i / 2];
}
console.log(b); // [ 0, undefined, 1, undefined, 2, undefined, 3, undefined, 4, undefined ]
配列であっても[]によるアクセスはオブジェクトと同じであり、[]の中身はtoStringされてキーになるというルールと、数値はすべてnumberであり、intのつもりでもnumberということを思い出せばこのコードに何が起きたかはわかっていただけると思います。
a['0'], a['0.5'], a['1'], a['1.5'], a['2'], ...
となっているわけです。
JSON
JavaScriptでは簡単には正しく解釈できないにも関わらず、JSONに64ビット整数を含めることは可能です。Jacksonなどでも遠慮なく突っ込めます。RFC8259では、数値幅の規定はありませんが、整数なら[-(253)+1, (253)-1]の幅で使うといいっすよと書かれています。
コーディング規約
開発環境など
最低限のJavaScript開発環境についても触れておきます。
規格
最近のJavaScriptの規格としては、ECMAScript3、ECMAScript5.1、ECMAScript 2015(旧名ECMAScript 6)、2016、2017、2018と結構な多様性があるように見えます。実際にはブラウザごとに最新規格のどこから実装するかとInternet Explorerぐらいの違いしかないので、IEの対応を考えなければ、ECMAScript 2015ぐらいはだいたい使えるため、ECMAScript 2015が現代の最低ラインでしょうか。
とはいえIE対応とは言え、今さら2015より前に戻りたくもなければ、新しい規格はどんどん便利になっているので、悩むよりもbabelなどのトランスパイラに任せてbabelがサポートしている規格で書くということも多いと思います。
eslint
コンパイラのない(ことの多い)JavaScriptにとって静的解析ツールは非常に重要です。そんなわけでJavaにおけるSpotBugs(FindBugs)以上にデファクトスタンダードなツールがeslintです。今から環境を整えるのならeslint以外は考えられないのですが、とりあえずJava屋が使ってみるというところではもはや過去とされるjshintでも許されるのではないでしょうか。jshintであればeclipseでもNetBeansでもプラグインだけで入り、IntelliJシリーズには最初から同封されていて、node.js環境がなくても動くのでサクッと導入するのに向いているかと思います。
node.jsとnpm
node.js環境がなくてもとは言いましたが、多少なりともちゃんと開発しようと思う場合には、今どきnode.jsやnpm無しの開発というのは考えられない状況です。JDKなしでJavaを書くぐらいのものです。
babel
これも最近はJavaScriptで書くのにbabelのない開発も少ないぐらいだと思います。トランスパイラと呼ばれていますが、古語で言うところのトランスレータです。新しい規格で書かれたJavaScriptを古い規格で動くようなコードに変換できます。これでIE対応も(まあまあ)進みます。
開発時はChromeなどの新しいブラウザでネイティブで行い、最終段階で古い規格に合わせるか、すべてbabelに合わせたコードを書いて、実行時には常にトランスパイルするか、様々な手法があるのではないでしょうか。たぶん。
minify
通常は、.jsファイルをそのまま埋め込んだりせずに、一つのファイルにまとめてコメント除去やコード圧縮を図ります。個人的には、パフォーマンス性の問題よりも、minifyやbabelの過程を踏むことでコメントを除去したり、変数名を変えられるのが大きいと思います。~~特に官公庁のサイトのソースとか。~~かといって製品コードにコメントが入れると恥ずかしいから入れないというのも馬鹿らしいですし…
型を付けたい
一般的なJavaプログラマであれば、JMLとまでは言わなくても、型を付けて静的検査ぐらいはしたくなると思います。大雑把に言って、JavaScriptに型を付けた言語がTypeScriptやFacebook/Flowです。
ただ、いずれもトランスパイル前提なので、大規模なフロント開発でなければ、個人的にはJava屋の場合は、JSDocで型を書いて、WebStormにチェックしてもらうぐらいが一番だと思います。その型情報を活かしてminifyしたりトランスパイルしたり静的解析してくれるGoogle Closure Compilerという(残念ながら)マイナーなトランスパイラテクノロジーもあるのですが、こちらは存在が危ぶまれます。
それ以外のAltJS言語(死語)については、Java屋がやる仕事ならひとまず避けたほうがよいのではないでしょうか。ClojureScriptとかScala.jsとかロマンはありますね。Kotlin JavaScriptや大規模環境だとGWTもひょっとするといいかも知れません。しかし、いずれにせよ、JavaScriptが分かってないのに突っ込むのはお勧めしません。
エディタ
eclipseのJavaScript対応はJavaの対応レベルに比べるとかなり微妙です。それをJavaScriptの限界と考えてしまうともったいないのでVSCodeか、有料ですがWebStormがお勧めです。
おわりに
JavaScriptは非常にシンプルな言語であり、したがって難しい言語であり、楽しい言語です。ぜひ習得して、JavaにScriptが付いたようなものだと嘯いてください。
(ちなみにJava Advent Calendar 2018 11日目としては「真面目にJavaを書く話」的なものを予定していたんですが、遅延しまくった上にポエミィになったので差し替えました。)