はじめに
こんにちは、ECMAScriptを1週読んだだけの自称JavaScript中級者です。今回は、私のような自称JavaScript中級者が知らないであろうTipsをご紹介します。
「等値演算子 ==
ではなく、同値演算子 ===
を使いましょうね」のような実装上における初心者向けTipsではなく、言語仕様の雑学に近いものです。雑学とは言え、どれも基本的なことなので「当たり前のことしか書いていなかった」などと思うかも知れませんが、ご了承くださいませ。
1. 「JavaScriptは、すべてがオブジェクト」ではない
ネット上で見られるJavaScript解説ページでは、よく「JavaScriptは、すべてがオブジェクトだ!」なんて書かれていたりしますが、これは誤りです。
- JavaScriptは、すべてがオブジェクトである
- JavaScriptは、すべてがオブジェクトのように振る舞う
- JavaScriptは、ほぼすべてがオブジェクトのように振る舞う
この中で正しい文章は、3番目の「ほぼすべてがオブジェクトのように振る舞う」です。
1-1. プリミティブ型
まず、JavaScriptのプリミティブな型は以下の7つ。
- Undefined
- Null
- Boolean
- String
- Number
- Symbol(ES2015)
- Object
JavaScriptには関数や配列、他言語にあたる“連想配列”のような概念が存在しますが、それらはすべてObject型の値となります。JavaScriptを語る上で欠かせない特徴でもあるため、「すべてがオブジェクト」と勘違いしてしまう要因になるのかもしれません。
1-2. Wrapper Object
「プリミティブな値に対して、メソッドやプロパティをコールできるじゃないか」と思う人がいるかもしれません。例えば、以下のようなコードです。
// Stringオブジェクト
new String('hello world').length; //-> 11
// string型のプリミティブ値
'hello world'.length; //-> 11
たしかに、Object型はメソッドとプロパティを持つことができます。逆に、Object型(とSymbol型)以外は、メソッドとプロパティを持つことができません。
ではなぜ、プリミティブな文字列に対してStringオブジェクトの length
プロパティをコールできるのでしょう?
一部のプリミティブ値と一部のオブジェクトには、互換性があります。これは、プリミティブ値に対して互換性のあるオブジェクトのプロパティまたはメソッドをコールする際に、一度互換性のあるオブジェクト Wrapper Object へ変換するというものです。
先ほどの例で言えば 'hello world'
という文字列に対して、 length
というStringオブジェクトのプロパティを呼ぼうとしました。この時点で先ほどの文字列は、Stringオブジェクトへ一度変換1され、 length
プロパティが呼ばれています。
なお、Wrapper Objectは当該プロパティが呼ばれた直後に破棄され、再びプリミティブな値へと戻されます。
この挙動は、オブジェクトの放置によるGarbage Collectionの発動を防ぐ効果もあります。多くの場合はこれらの理由により、 new
演算子を用いてStringやNumberオブジェクトを生成するよりも、プリミティブ値をそのまま使うべきです。
// Stringオブジェクトを生成
// 以降、Garbage Collectionが発動するまで、Stringオブジェクトがメモリ上に残る
const str1 = new String('hello world');
// String型のプリミティブ値を生成
// Stringオブジェクトよりもメモリ領域を必要としない
const str2 = 'hello world';
// Stringオブジェクトのプロパティ等をコールしたとき、Stringオブジェクトが生成され、直後に破棄される
str2.length; //-> 11
余談ではありますが、プリミティブ値を使いたくとも使えない組み込み関数も存在します。その1つが、Function.prototype.callです。
const f = function() { console.log(this); };
f.call('hoge');
//=> [String: 'hoge']
// これは、Stringオブジェクトである
2. null
は、null型でもありobject型でもある
null
は、Null型に該当します。しかし typeof
演算子を用いると、面白い結果になります。
typeof null; //-> 'object'
なんと null
はObject型だったのです!…というのは、あまりにも有名な話ですね。
2-1. JavaScript誕生初期からの仕様
typeof null
でObject型と判別されるのは、ECMAScriptの仕様通りです。なぜこうなってしまったのかというと、それはJavaScript誕生当初にまでさかのぼる必要があります。
JavaScript の最初の実装では、値は、型を表す「タグ」と「値そのもの」で表されていました。 オブジェクトの型タグは 0 で。そして null のそれは NULL ポインタ(0x00 は殆どのプラットフォームに存在する)で示されていました。その為 Null の型タグは 0 と見做され、「typeof Null は "object"」という、悪い冗談の様な結果になったのです。
ES2015では typeof null
に対して 'null'
を返すような仕様が提案されていました。しかし「多大な範囲に対して影響を及ぼす破壊的変更」という理由で却下されてしまいました。おそらく、未来永劫 typeof null
はNull型でもあり、Object型でもあり続けることでしょう。
3. JavaScriptには、共有渡し(値渡し)しか存在しない
JavaScriptの関数における、実引数と仮引数の関係性について、以下のような説明文を目にすることは少なくないでしょう。
- プリミティブな値であれば、値そのものを複製して渡す“値渡し”が行われる
- プリミティブでない値ならば、変数そのものの参照を渡す“参照渡し”が行われる
以下のサンプルコードとコメントを見ると、上記の記述に納得してしまうかもしれません。
// プリミティブな値は、値渡し…?
//------------------------------------
function func1(arg) {
arg = 2;
}
let num = 1;
func1(num);
// コピー元(実引数)の値が変わっていない!
// これは値渡しだ!
console.log(num); //=> 1
// プリミティブでない値は、参照渡し…?
//------------------------------------
function func2(arg) {
arg.second = 3;
}
let obj = { first: 1, second: 2 };
func2(obj);
// コピー元(実引数)の値が変わっている!
// これは参照渡しだ!
console.log(obj); //=> Object {first: 1, second: 3}
しかし、これは大きな誤りです。結論から述べれば、 JavaScriptはいかなる場合においても共有渡し(値渡し) によって値がコピーされます。
3-1. 「プリミティブ値がImmutable」であることを忘れない
これは、以下の2つを理解すれば簡単です。
- JavaScriptにおけるプリミティブ値は、すべてImmutableである
- 値のコピーは、“参照値”を値渡ししている
言語によって“参照渡し”や“値渡し”の定義がバラバラなため、最も適切な言葉を使うとすれば“共有渡し(Call by sharing)”でしょうか。共有渡しは、変数の参照値という値を値渡しすることになります。言い換えれば、“とある値に対して別名を割り当てている”わけです。
これらを踏まえて、先ほどのソースコードをもう一度見てみます。
// プリミティブな値は、共有渡し!
//------------------------------------
// 仮引数argは、変数numと同じ値を参照している
function func1(arg) {
// プリミティブな値は、Immutableなので、不変。
// 値を変更することはできないため、2という新しい値に対して“arg”というラベルをつける
arg = 2;
}
let num = 1;
func1(num);
// プリミティブ値は変わりようがないので、1のまま。
console.log(num); //=> 1
// プリミティブでない値も、共有渡し!
//------------------------------------
// 仮引数argと変数objは、同じオブジェクトを参照している
function func2(arg) {
// オブジェクトのsecondプロパティを変更すれば、当然影響範囲は仮引数argと変数objまで及ぶ
arg.second = 3;
}
let obj = { first: 1, second: 2 };
func2(obj);
// プリミティブでない値は可変なので、変更されて当たり前。
console.log(obj); //=> Object {first: 1, second: 3}
3-2. 渡し方の仕様は、ECMAScriptで明確に定義されていない
実のところ、ECMAScriptには“参照渡し”や“値渡し”・“共有渡し”などといった言葉は、明確に記述されていません。どのような方法で値がコピーされているかは、処理系に依存します。
ただし、ECMAScript内の =
演算子の説明では、“バインド(割り当て)”という言葉が頻繁に用いられています。
4. 0
というプリミティブ値は存在しない
Number型のプリミティブ値は、倍精度64ビット形式によるIEE754の値 — ES2015におけるNumber.MIN_SAFE_INTEGER〜Number.MAX_SAFE_INTEGERの範囲で、自由な数値を扱うことが許されています。
実はNumber型の数値として 0
という値は存在しません。
数値の0に対応するものは -0
と +0
の2つが存在します。多くのエンジニアが記述している 0
は、 +0
のエイリアスに他ならないのです。
// +0と0、そして-0は同値である
0 === +0; //-> true
0 === -0; //-> true
// ただし、0除算時に違いが現れる
1 / +0; //-> Infinity
1 / -0; //-> -Infinity
// ES2015のObject.isでも判別可能
Object.is(+0, -0); //-> false
5. undefinedは予約語ではない
undefined
は、Undefined型の値でもあり、グローバルオブジェクト(WebブラウザならWindowオブジェクト)のプロパティでもあります。
そんな undefined
ですが、ECMAScriptの仕様としては予約語ではありません。したがって、上書きが可能です。
undefined; //-> undefined
undefined = 'a';
undefined; //-> 'a'
5-1. Undefined型の判定
この仕様から、Undefined型の値を調べるには void
演算子を用いていました。 void
演算子は、必ずUndefined型の値を返す仕様だからです。
しかし今となっては、Undefined型の値を調べるために void
演算子を使うことは、ほとんどなくなりました。なぜならば、ES5より undefined
は設定及び書き込みが不可能なプロパティとなったからです。
// ES3以前の場合は、以下のようにして判別する
if (x === void 0) { ... }
// ES5以降は、以下のようにして判別できる
if (x === undefined) { ... }
6. オブジェクトよりも配列の方が、生成コストが高い
他言語に精通していると、「オブジェクトよりも配列の方が、生成コストが低いのでは?」なんて思うかも知れません。
JavaScriptにおいては、配列よりもオブジェクトを生成するコストの方が低いです。
なぜならば配列は、Objectが持つプロパティに加え、Arrayオブジェクトで定義されたプロパティを持つオブジェクトだからです。つまり、配列の生成コストはオブジェクトよりも高くなります。
そのことを知っていれば arguments
オブジェクトが、なぜ配列 — Arrayオブジェクトではないか、少しは分かるのではないでしょうか。
7. 配列を示す仕様は存在しない
関数も配列もオブジェクトではありますが、これらはどのようにして定義されているのでしょう。
関数と呼べるオブジェクトは、ECMAScriptによって [[Call]]
という内部メソッド持つことが定義されています。逆に [[Call]]
を持つオブジェクトは、必ず関数として扱われなければいけません。
対して配列は、配列であることを定義できる概念がECMAScriptには存在しません。
8. ブロックスコープの生成に、if文などの制御文は必要ない
JavaScriptにはスコープ汚染問題を解決するための方法がいくつかあります。1つは、無名関数(関数)。もう1つは、ES2015より登場した let
や const
による変数宣言のキーワードです。
let
及び const
を用いてブロックスコープを生成したい場合、よくif文などの制御文を用いたサンプルコードが見られます。しかし、ES2015であれば {}
のみでブロックスコープは生成可能です。
// 制御文を使わなくても
if (true) {
let i = 0;
...
}
// {} だけでスコープを作れるよ
{
let i = 0;
...
}
ところで、個人的に let
や const
の登場によって最も使いやすくなったものは、for文だと感じています。今まで、特定の配列に対して線形探索をしたい場合、スコープ問題の点からArray.prototype.forEachを使わざるを得ませんでした。
Array.prototype.forEachは関数型のように記述できる反面、for文に比べてパフォーマンスの点で若干劣ります。パフォーマンスを最優先させたい場合、スコープ汚染問題を犠牲にする必要があったわけです。
const nums = [1, 2, 3, 4, 5];
// Array.prototype.forEachは、スコープを汚さずに済むが、実行速度がわずかに遅い
nums.forEach((elm) => { ... });
// for文とletを用いることで、実行速度もスコープ汚染問題も犠牲にしなくて済む
for (let i = 0, len = nums.length; i < len; i += 1) { ... }
9. 文末セミコロンは、省略すべきではない
残念ながら、「ES2015から、文末にセミコロンを記述しなくても問題ない」と勘違いしている人をよく見かけます。特定の条件下では意図しない評価をされかねないため、セミコロンの省略は推奨できません。
セミコロンを記述しなくても正常に動作するコードも存在しますが、それはECMAScriptによってセミコロンの自動挿入規則に該当するかどうかが定められているからです。
9-1. セミコロンの自動挿入規則に該当するもの
ECMAScriptでは、以下のステートメントに対しては文末セミコロンが必須とされています。
- 空のステートメント
-
let
及びconst
-
import
及びexport
- 変数のステートメント
- 式ステートメント
debugger
-
continue
及びbreak
、throw
return
}
で終了する class
や function
等においては、セミコロンの自動挿入規則に該当するため、文末セミコロンが不要となります。
func() // 単体では問題ない
(1+1) // func()(1+1); と見なされ、Uncaught TypeError
function func() {}
class User {
get() {
} // ここにセミコロンは不要、付加しても良い
find() {
}
} // ここも不要
(1+1); // エラーにならない
10. JScript/JAVAScript/Javascript/JAVA…ではなくて、JavaScriptだよ
お願いですから、“JavaScript”と表記してください2。