31
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaAdvent Calendar 2018

Day 11

JavaでわかるJavaScript入門

Last updated at Posted at 2018-12-14

はじめに

「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
x = function() {};
console.log(typeof x); // function

JavaScriptの型の特性はおおよそJavaと似た仕組みで考えることができます。JavaScriptで特に利用頻度の高い型はstringnumberbooleanobjectfunctionの5つでしょうか。

このうちfunctionは実は型ではなく、objectの一つなのですが、ここでは便宜上の型として扱っています。また、実際の型名は仕様書(5.1 Edition)Mozilla Developer Network上はすべて先頭大文字(String、Number、Boolean、Objectなど)ですが、Stringという型とそれをラップするObject型のStringオブジェクトがあって非常に紛らわしいので、型名についてはtypeofの判定値であるstringのように表記しています。この表記はJSDocやTypeScriptなどでは一般的です。

JavaScriptの型または便宜上の型と、Javaの型との大きな違いは、intdoubleなどの数値型は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()だけでなく配列と同様に[]で一文字ずつ取得可能です。

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

number

JavaScriptにおいて、numberはJavaのdoubleと同等の機能を持つ唯一の数値型です。64ビットの浮動小数点型で、doubleがそうであるように、整数ももちろん扱うことができます。逆に、JavaScriptにはintのような整数用の型というものはなく、すべて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(1234123412341234123); // 123412341234123200
Javaならlongで扱える
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の値
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のインスタンス)というのが正確なところで、functionobjectの持つ特性をすべて持っています。

functionはObject
var f = function() {
};
console.log(f instanceof Function); // true
console.log(f instanceof Object); // true

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

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

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()ですが、固定の正規表現であれば、正規表現リテラルを利用できます。

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

JavaとJavaScriptで、すべてのオブジェクトがObjectというルートを持っているのは同じですが、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による擬似コード(カスケードされた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()*されます。

Javaによる擬似コード(Object風)
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演算子を使います。

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 instanceof Xがtrueを返す)というのは、Z.__proto__ === X.prototype(または再帰的にZ.__proto__.__proto__ === X.prototypeZ.__proto__.__proto__.__proto__ === X.prototype…のいずれか)を満たしている場合ということになります。

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

newと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の後ろの名前は何でしょうか?

困ったら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などのイベントハンドラに設定するのが難しくなります。イベントハンドラは関数のみしか設定できず、thisを指定した形での呼び出しができないためです。

そこで、Function.prototype.apply()Function.prototype.call()
Function.prototype.bindによって、thisを設定した呼び出しや、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でのみ実行されます。クリックのような画面操作イベントもタスクとして登録され、先行するタスクが終わるまでキューイングされます。

Javaによる疑似コード
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では、数値幅の規定はありませんが、整数なら[-(253)+1, (253)-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にチェックしてもらうぐらいが一番だと思います。その型情報を活かして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を書く話」的なものを予定していたんですが、遅延しまくった上にポエミィになったので差し替えました。)

31
30
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?