JavaでわかるJavaScript入門

JavaでわかるJavaScript入門:


はじめに

「JavaができればJavaScriptできるよね?」とか言う人に実際に会ったことはないのですが、人には言ってたりします。

もちろん実のところはそう簡単にはいきません。

いきませんが、2018年も年末になってすら、ふと迷い込むとJavaScriptはJSPのおまけぐらいに思われていたりするわけです。さすがにJavaアプレットと混同している人はそんなに…いや、いるっぽいですね。私のパーソナライズの結果だといいんですが。



java アプレット javascript 違い   Google 検索.png


甚だしくは新人でない人がそのようなことをもっともらしく言ってみたり、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演算子で確認することができます。

typeof
var x = 1; 
console.log(typeof x); // number 
x = "str"; 
console.log(typeof x); // string 
JavaScriptの型の特性はおおよそJavaと似た仕組みで考えることができます。JavaScriptで特に利用頻度の高い型はstringnumberbooleanobjectfunctionの5つでしょうか。Javaと違って、intdoubleなどの数値型はnumberただ1つになることと、stringが基本型であること、function型が存在していることが大きな違いです。また、JavaScriptにはそれ以外の型もいくつかあります。それぞれ順番に見てみましょう。


string

Stringのようなものです。ただし、JavaScriptにおいて、char型は存在しないため、''""に区別は一切ありません。シェルスクリプトのように変数展開もしません。(新しいJavaScriptでは``を使うと${}で変数展開できます。)一般的なコーディングスタイルでは''""はソースコード中でどちらか片方だけを使うことが推奨されています。

stringに対して使用できるメソッドもStringとよく似ているというか、ほぼそのままです。ただし、少し前のStringなので、現代を生きるJavaプログラマからすると、Stringのサブセットになるので注意が必要です。equalsIgnoreCase()isEmpty()のような便利なメソッドはありません。しかし、startsWith()のように後から入ったものもあります。

JavaScriptならではのメソッドもありますが、ほとんど気にする必要はないメソッドばかりです。

慣れるまで少し戸惑うかもしれないのはequals()が無く、===で比較できることでしょう。==でもできますが、現代的JavaScriptでは==は使いません。

また、Javaと異なり、charAt()だけでなく配列と同様に[]で一文字ずつ取得可能です。

string
console.log('abc' === "abc"); // true 
console.log('abc'[1]); // 'b' 


number

JavaScriptにおいて、numberはJavaのdoubleに相当する唯一の数値型です。64ビットの浮動小数点型で、doubleがそうであるように、整数ももちろん扱うことができます。逆に、整数用の型というものはなく、すべてnumberです。

ただし、ビット演算するときには32ビット整数として扱われるので、ビット演算時にはちょっと注意が必要です。逆に、このことを利用して少数を整数化するテクニックもあります。

number
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(12341234123412341234); // 12341234123412340000 
もちろんこれは別に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の値
var f = function(x) { 
  console.log('f:' + x); 
} 
f('abc'); // f:abc 
f = function(x) { 
  console.log('f2:' + x); 
} 
f('xyz'); // f2:xyz 
また、functionは実際にはオブジェクトでもあります。これは後述します。


オートボクシングと型変換

numberstringbooleanにはラッパーオブジェクトNumberStringBooleanがあります。JavaScriptではオートボクシングとは言いませんが、必要に応じてオートボクシングと同じことをしてくれます。たとえば、文字列定数"abc"はstring型でStringのインスタンスではないのですが、"abc".substring(2)などとしてメソッドを使うことができます。

ラッパーオブジェクトは基本的にはユーティリティメソッドを提供してくれるものであり、new String()new Number()して使うことはありません。が、使えてしまうのでJava以上に警戒が必要です。(※Javaでも別な視点から、new String()とか、new Integer()は使わないですよね。)Stringstringではないので、同じようには使用できず、あえて使う機会もありません。

ラッパーオブジェクト
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型ですので、この分類には入りません。

また、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 


正規表現リテラル

typeofではobjectに分類されますが、正規表現リテラル(定数)があります。JavaのPattern.compile()に相当するのはnew RegExp()ですが、固定の正規表現であれば、正規表現リテラルを利用できます。

RegExp
var regexp = /.+/; // regexp = new RegExp('.+'); と同等 
console.log(regexp instanceof RegExp); // true 


booleanへの型変換

JavaScriptではすべての型はbooleanとして評価できます。0や空文字''nullundefinedfalseとして扱われます。

boolean変換
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

すべてのオブジェクトがObjectというルートを持っているのは同じですが、JavaとJavaScriptでObjectObjectの利用感はまったく違います。JavaScriptのオブジェクトはすべて単なる連想配列であり、機能としてはJavaで言うところのMap<String, Object>です。これがJavaScriptの言語として重要な要素となっています。

順を追って見ていきましょう。


基本的性質

JavaのObjectインスタンスはあまり直接的に使用しませんが、JavaScriptのObjectは連想配列やPOJO、場合によっては関数を持たせてクラスオブジェクトのように使うなど、さまざまな用途で利用するため、使用頻度の高いオブジェクトです。
Objectのインスタンスはnew Object()で生成することもできますし、単に{}で生成することもできます。一般的にはコーディングルールにより{}で生成することが推奨されていることが多いです。このときに、JSON風に初期値を与えることもできます。むしろJSONがここから生まれたのだから、JSON風っておかしくないかと思うかもしれませんが、JSONのように仕様に縛られておらず、JSONよりも柔軟です。

Objectの使用例
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ができます。

文法上の制約により、キーが空白や記号の場合、.でアクセスすることはできませんが、値の出し入れでは両者は同じように扱うことができます。

Objectの使用例もう少し
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においては普通の配列です。objectnew Object()で生成できるように、new Array()で生成できますが、一般的には単なる[]が好まれます。

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の時点で、自動伸長できる配列のように取り扱うことができるというわけです。

ObjectとArray(違いはない)
// 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になる性質を持っています。

Arrayとlength
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群にキーをセットするときは、一つ目のMapputするものとします。

こうすると、先頭のMapは、すべての値を持っているように見えますし、値を変更することもできます。しかし、裏側のMapの値は実際には変化しません。

Javaによる擬似コード
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) { 
    super.put(key, v); 
  } 
} 
JavaScriptのオブジェクトは自動的にこれをやってくれる仕組みを持っていて、そのオブジェクトの連なりをプロトタイプチェーンと呼び、一つ目のMapすなわち対象のオブジェクトから見た二つ目のMapをプロトタイプと呼びます。

実際にも、node.jsやChromeのJavaScriptエンジンの場合、__proto__という特殊な変数(キーと値)があり、これがプロトタイプを指しています。

あるキーで問い合わせがあったとき、そのキーが自分自身になければ、__proto__に問い合わせを行います。
__proto__のObjectでも同じように自分自身にあれば返し、無ければ__proto__を辿るということを繰り返します。

値.や[]で代入するコンテキストの場合はそれと異なり、そのオブジェクトのみにput()されます。

Javaによる擬似コード
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 Object put(String key, Object v) { 
    myValue.put(key, v); 
  } 
} 


プロトタイプの設定とインスタンス

__proto__という名前から感じられるとおり、__proto__は直接触れることのない変数ですし、触ることのできない処理系もあります。では通常はプロトタイプをどうやって設定するかというと、prototypeというキーとnew演算子を使います。

newとprototype
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.__proto__ === X.prototype(または再帰的にZ.__proto__.__proto__ === X.prototypeZ.__proto__.__proto__.__proto__ === X.prototype…)を満たしている場合であり、instanceof演算子がそれをやってくれていると考えることができます。

ちなみに、__proto__X.prototypeを代入するのは本当に単なる代入なので、次のようなことにもなります。

newとprototype(順番入れ替えたもの)
var x = new Date(); // xの__proto__はDate.prototype 
Date.prototype.test = 'abc'; 
console.log(x.__proto__ === Date.prototype); // true 
console.log(x.test); // 'abc' 


コンストラクタ

さて、既存のDateや適当なX()でごまかしましたが、JavaScriptにおけるクラスのように見えるもの、newの後ろの名前は何でしょうか?

困ったらtypeof
console.log(typeof Date); // function 
functionです。Javaにおけるクラスという概念はいったん忘れてください。JavaScriptにはJavaにおけるクラスはありません。クラスのようなものを実現するために、new演算子とプロトタイプチェーンがあると思ってもらった方がわかりやすいと思います。(これは逆から見ると、プロトタイプのようなものを実現するために余計なクラス機構を取り入れてるJavaという言語、と考えることもできます)

new演算子の機能はもう少し正確には、「指定されたコンストラクタ関数のprototypeプロパティをプロトタイプに持つオブジェクトを生成し、そのオブジェクトをthisとした上で、コンストラクタ関数を実行する」ということになります。

何でもない関数をnewしてみる
function hoge(v) { 
  console.log(v); 
} 
var x = new hoge('abc'); // 'abc' 
console.log(x instanceof hoge); // true 
したがって、コンストラクタとして扱うつもりの関数のprototypeプロパティに値を入れておくと、そのインスタンスで取り出すことができます(プロトタイプチェーンで説明したように、書き込みはそのインスタンスになります)。プロパティに関数を入れておくと、メソッドとして使うことができます。

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番目の引数というものは指定できません。関数として使った場合には、thisglobalという環境を表すオブジェクトになっています。

thisは何?
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が省略されているようなものと考えるのがいいかもしれませんね。少し戸惑うであろう点は、上記のhogevの専属メソッドではなく、かつメソッドは単なる関数のプロパティでしかないということです。

呼び方いろいろ
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などのイベントハンドラに設定するのが難しくなります。そこで、Function.prototype.apply()Function.prototype.call()
Function.prototype.bindによって、thisを設定した呼び出しや、thisを設定した呼び出しを行う関数を作っておくことができるようになっています。

thisを明示的に設定
function hoge() { 
  console.log(this === v); 
} 
var v = {}; 
hoge.call(v); // true 
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により宣言した変数は関数の中括弧{}内で有効です。たとえば、次のような挙動になります。

varの範囲
for (var i = 0; i < 100; i++) {} 
console.log(i); // 100 
for (var i = 0; i < 200; i++) {} // 2回目のiの宣言です 
今のJavaScriptには、Javaや一般的なC言語スタイルの言語と同じように{}内でのみ使える変数を宣言するためのconstletがありますので、新しい環境であればそちらを使ってください。

letの範囲
for (let i = 0; i < 100; i++) {} 
console.log(i); // undefined 
この記事が全面的にvarなのは古いJavaScriptに慣れてしまった人に違和感を覚えていただかないようにであって、通常はもはやvarを使うシーンはありません。IE対応などでどうしても直接的にvarを使わなければならないのであれば、静的解析のあるエディタやlintを導入しましょう。


ネームスコープのための即時関数

varはこのような奇妙なスコープを持っているので、古くはネームスコープを利用するために即時関数が利用されてきました。

即時関数
(function() { 
  ... 
})() // 引数が詰まってることもあります 
このようなコードは特に古いコードではしょっちゅう見かけるかと思いますが、これはそのためのイディオムです。


class構文

現代的なJavaScriptでは、すでにclass構文が導入されています。

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では、数値幅の規定はありませんが、整数なら[-(2*53)+1, (2*53)-1]の幅で使うといいっすよと書かれています。


コーディング規約

JavaScriptのスタイルガイドまとめ(おすすめ4選)


開発環境など

最低限の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にチェックしてもらうぐらいが一番だと思います。Google Closure Linterという(残念ながら)マイナーなトランスパイラテクノロジーもあるのですが、そろそろ滅んでしまいそうです。

それ以外のAltJS言語(死語)については、Java屋がやる仕事なら避けたほうがよいのではないでしょうか。ClojureScriptとかScala.jsとかロマンはありますね。大規模環境だとGWTもひょっとするといいかも知れません。いずれにせよ、JavaScriptが分かってないのに突っ込むのはお勧めしません。


エディタ

eclipseのJavaScript対応はJavaの対応レベルに比べるとかなり微妙です。VSCodeか、有料ですがWebStormがお勧めです。


おわりに

JavaScriptは非常にシンプルな言語であり、したがって難しい言語であり、楽しい言語です。ぜひ習得して、JavaにScriptが付いたようなものだと嘯いてください。

(ちなみにJava Advent Calendar 2018 11日目としては「真面目にJavaを書く話」的なものを予定していたんですが、遅延しまくった上にポエミィになったので差し替えました。)

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)