JavaScript 言語概要
JavaScript はマルチパラダイムの動的言語であり、型や演算子、標準組み込みオブジェクト、メソッドがあります。その構文は Java や C 言語に由来するので、それらの言語の多くの構造が JavaScript にも同様に適用できます。 JavaScript は、オブジェクトプロトタイプやクラスによるオブジェクト指向プログラミングに対応しています。また、JavaScript は関数型プログラミングもサポートします。関数が第一級オブジェクトであり、式から容易に作成し、他のオブジェクトと同じように受け渡しすることができます。
このページは、 JavaScript のさまざまな言語機能の概要で、 C や Java など、他の言語のバックグラウンドがある読者のために書かれたものです。
データ型
まずはあらゆる言語の構成要素、「型」を見ることから始めましょう。 JavaScript のプログラムは値を操作し、それらの値はすべて型に属しています。JavaScript の型は次の通りです。
- 数値型: 非常に大きな整数を除くすべての数値(整数、浮動小数点数)で使用します。
- 長整数型: 任意の長さの大きな整数に使用します。
- 文字列型: テキストを格納するために使用されます。
- 論理型:
true
とfalse
- 通常は条件の論理に使用します。 - シンボル型: 衝突しない固有の識別子を作成するために使用します。
- Undefined: 変数に値が割り当てられていないことを示します。
- Null: 意図的に値がないことを示します。
他のすべてのものはオブジェクト型と呼ばれます。主なオブジェクト型には次のものがあります。
JavaScript では、関数は特別なデータ構造ではありません。呼び出すことができるオブジェクトの特別な型にすぎません。
数値
JavaScript には 2 つの組み込み数値型があります。 数値型 (Number) と 長整数型 (BigInt) です。
数値型はIEEE 754 倍精度 64 ビットバイナリー値です。これは整数の場合は-(253 − 1) と 253 − 1 の間の数ならば精度が落ちることがなく、浮動小数点数は 1.79 × 10308 まで格納できます。 JavaScript では、数値型の中で浮動小数点数と整数を区別しません。
console.log(3 / 2); // 1.5, 1 ではない
つまり、整数のように見えるものは、実は暗黙のうちに浮動小数点です。 IEEE 754 のエンコード方式では、浮動小数点数で演算が不正確になることがあります。
console.log(0.1 + 0.2); // 0.30000000000000004
ビット演算など整数を想定した処理を行う場合、数値は 32 ビット整数に変換されます。
数値リテラルには、基数(2 進数、8 進数、10 進数、16 進数)を示す接頭辞や、指数接尾辞もあります。
console.log(0b111110111); // 503
console.log(0o767); // 503
console.log(0x1f7); // 503
console.log(5.03e2); // 503
長整数型は任意の長さの整数です。その動作は C の整数型に似ていますが (例えば除算は 0 で切り捨てられます)、無限に大きくなることができます。長整数型は数値リテラルと接尾辞 n
で指定します。
console.log(-3n / 2n); // -1n
標準の算術演算子は、加算、減算、剰余演算などを含めて対応しています。長整数型と数値型を混合して演算処理を行うことはできません。
Math
オブジェクトは、標準数学関数と定数を提供します。
Math.sin(3.5);
const circumference = 2 * Math.PI * r;
文字列を数値に変換する方法は 3 つあります。
parseInt()
は、文字列を整数として解釈します。parseFloat()
は、文字列を浮動小数点数として解釈します。Number()
関数は、 文字列を数値リテラルであるかのように解釈でき、さまざまな数値表現に対応しています。
また、単項 +
を Number()
の短縮形として使用することもできます。
数値の値には、 NaN
("Not a Number" の略)と Infinity
も含みます。例えば、数値以外の文字列を解釈しようとした場合や、負の値に対して Math.log()
を使用した場合など、多くの「不正な演算」は NaN
を返します。ゼロによる除算は Infinity
(正の値または負の値)を返します。
NaN
は伝染します。どんな数学処理にもオペランドとして提供すると、結果も NaN
になります。 NaN
は JavaScript で唯一それ自身と等しくない値です(IEEE 754 仕様による)。
文字列
JavaScript での文字列は Unicode 文字の列です。これは国際化に携わったことのある人にとっては歓迎すべきニュースでしょう。より正確な言い方をすれば、文字列は UTF-16 エンコード方式です。
console.log("Hello, world");
console.log("你好,世界!"); // ほぼすべての Unicode 文字は文字列リテラルで書くことができます。
文字列は一重引用符でも二重引用符でも書くことができます。 JavaScript には文字と文字列の区別はありません。単一の文字を表したい場合は、その単一の文字からなる文字列を使用するだけです。
console.log("Hello"[1] === "e"); // true
文字列の長さ(コード単位)を調べるには、そのlength
プロパティにアクセスします。
文字列には、文字列を操作したり、文字列に関する情報にアクセスしたりするための ユーティリティメソッドがあります。設計上、すべてのプリミティブは不変なので、これらのメソッドは新しい文字列を返します。
演算子 +
は文字列に対してオーバーロードされています。演算子の 1 つが文字列の場合、数値の加算の代わりに文字列の連結を行います。特別なテンプレートリテラル構文により、式を埋め込んだ文字列をより簡潔に書くことができます。 Python の f-strings や C# の補完文字列とは異なり、テンプレートリテラルは(単一引用符や二重引用符ではなく)逆引用符を使用します。
const age = 25;
console.log("私は " + age + " 歳です。"); // 文字列へ変換
console.log(`私は ${age} 歳です。`); // テンプレートリテラル
その他の型
JavaScript では、意図的に値がないことを示す null
(null
のキーワードでのみアクセスできます)と、値がないことを示す undefined
を区別します。 undefined
を取得する方法はたくさんあります。
return
文に値がない場合 (return;
) は、暗黙にundefined
を返します。- 存在しないオブジェクトのプロパティへアクセスすると (
obj.iDontExist
)、undefined
を返します。 - 初期化を伴わない変数宣言 (
let x;
) は、暗黙のうちにその変数をundefined
に初期化します。
JavaScript には true
と false
(これらはともにキーワードです)を取りうる値とする論理型があります。どんな値でも以下の規則に基づいて論理値に変換できます。
false
、0
、空文字列 (""
)、NaN
、null
、undefined
は、すべてfalse
になる。- その他の値はすべて
true
になる。
Boolean()
関数を使うことで、明示的にこの変換を行うことができます。
Boolean(""); // false
Boolean(234); // true
しかし、これはほとんど必要ありません。 JavaScript は、 if
文の中など(制御構造を参照)のように論理値が期待される場面では、暗黙にこの変換を行うからです。このため、ときどき "真値 (truthy)" および "偽値 (falsy)" と言うことがありますが、これは論理値に変換されるときにそれぞれ true
または false
になる値という意味です。
&&
(論理 AND) や ||
(論理 OR)、!
(論理 NOT) などの論理演算がサポートされています。演算子を参照してください。
シンボル (Symbol) 型は固有の識別子を作成するために多く使用されます。 Symbol()
関数で作成するシンボルは固有のものであることが保証されています。さらに、共有定数である登録シンボルや、特定の演算を実行するための「プロトコル」として言語が利用する公知のシンボルがあります。シンボルリファレンスで詳しく説明されています。
変数
JavaScript では、新しい変数を宣言するのに let
、const
、var
の 3 つのキーワードのいずれかを使用します。
let
は、ブロックレベルの変数を宣言できます。宣言した変数は、変数を包含するブロックから使用できます。
let a;
let name = "Simon";
// ここでは myLetVariable が *見えません*
for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
// ここでだけ myLetVariable が見えます
}
// ここでは myLetVariable が *見えません*
const
は、値を変更することを意図しない変数を宣言することができます。宣言した変数は、変数を宣言したブロックから使用できます。
const Pi = 3.14; // 変数 Pi を設定
console.log(Pi); // 3.14
const
で宣言された変数は再代入できません。
const Pi = 3.14;
Pi = 1; // エラーが発生します。定数変数は変更できないからです。
オブジェクトの場合、 const
宣言は変数の値の 再代入 を防ぐだけで、変数の値の 変更 を防ぐことはできません。
const obj = {};
obj.a = 1; // エラーなし
console.log(obj); // { a: 1 }
var
宣言は意外な挙動をすることがあり (例えばブロックのスコープにならない)、現代の JavaScript コードでは推奨されません。
値を割り当てずに変数を宣言した場合、その値は undefined
となります。初期化子なしで const
変数を宣言することはできません。
let
と const
で宣言された変数は、定義したスコープ全体で存在しますが、実際の宣言行の前には一時的なデッドゾーンと呼ばれる領域で存在します。これは他の言語には見られない、変数の影ともいえる興味深い作用が発生します。
function foo(x, condition) {
if (condition) {
console.log(x);
const x = 2;
console.log(x);
}
}
foo(1, true);
他のほとんどの言語では、const x = 2
の行の前では x
はまだ上位スコープの引数 x
を参照しているはずなので、これは "1" と "2" をログ出力することになります。 JavaScript では、それぞれの宣言がスコープ全体を占めるため、最初の console.log
で "Cannot access 'x' before initialization" というエラーが発生します。詳しくは、let
のリファレンスページを参照してください。
JavaScript は動的型付けを行います。(前の節で記述されているように)型は値にのみ関連付けられますが、変数には関連付けられません。 let
宣言された変数については、常に再代入によって型を変更することができます。
let a = 1;
a = "foo";
演算子
JavaScript の算術演算子は、+
、-
、*
、/
、そして剰余演算子の %
(モジュロと同じです) です。値は =
を使って代入されます。また +=
や -=
のような複合代入文もあります。これらは x = x 演算子 y
と展開できるものです。
x += 5;
x = x + 5;
++
や --
を使ってインクリメントやデクリメントできます。これらは前置あるいは後置演算子として使うことができます。
+
演算子は文字列の結合も行います。
"hello" + " world"; // "hello world"
文字列を数字(や他の値)に足すと、すべてのものが最初に文字列に変換されます。このことはミスを誘うかもしれません。
"3" + 4 + 5; // "345"
3 + 4 + "5"; // "75"
空文字列を足すのは、何かを文字列に変換する便利な方法です。
JavaScript における 比較 は、<
や >
、<=
、>=
を使って行うことができます。これらは文字列と数値のどちらでも機能します。等価の場合、二重等号演算子は異なる型を与えると型の変換が行われ、時には興味深い結果をもたらします。一方、三重等号演算子は型の強制を行わないので、通常はこちらを推奨します。
123 == "123"; // true
1 == true; // true
123 === "123"; // false
1 === true; // false
二重等号と三重等号には、不等号にも対応する !=
と !==
があります。
JavaScript にはビット演算子や論理演算子もあります。特に、論理演算子は論理値だけで動作するわけではありません。値の「真偽度」によって動作します。
const a = 0 && "Hello"; // 0 は偽値なので 0
const b = "Hello" || "world"; // "Hello" と "world" は共に真値なので "Hello"
演算子 &&
と ||
は短絡評価を使用します。つまり、 2 つ目の演算子を実行するかどうかは最初の演算子に依存します。これは、オブジェクトの属性にアクセスする前に null かどうかを調べるのに有益です。
const name = o && o.getName();
また、値のキャッシュにも使えます(偽値の値が無効である場合)。
const name = cachedName || (cachedName = getName());
演算子の包括的なリストについては、ガイドページまたはリファレンスの章を参照してください。特に演算子の優先順位に関心があるかもしれません。
文法
JavaScript の文法は C 言語にとてもよく似ています。特筆すべき点がいくつかあります。
- 識別子には Unicode 文字を入れることができますが、予約語のいずれかにすることはできません。
- コメントは共通の
//
または/* */
ですが、他の多くのスクリプト言語、例えば Perl, Python, Bash は#
です。 - JavaScript でセミコロンは省略可能で、必要なときに自動的に挿入されます。しかし、 Python とは異なり、セミコロンは構文の一部であるため、注意すべき点があります。
JavaScriptの文法について詳しく見ていくには、字句文法のリファレンスページを参照してください。
制御構造
JavaScript は C 言語ファミリーの他の言語とよく似た制御構造セットを持っています。条件文は if
と else
で対応しています。必要ならこれらを連鎖させることもできます。
let name = "kittens";
if (name === "puppies") {
name += " woof";
} else if (name === "kittens") {
name += " meow";
} else {
name += "!";
}
name === "kittens meow";
JavaScript には elif
がなく、 else if
は実際には単一の if
文からなる else
分岐にすぎません。
JavaScript には while
ループと do...while
ループがあります。前者は普通のループ処理に適しており、後者はループの本体が少なくとも 1 回は実行されるようにしたいときのループです。
while (true) {
// 無限ループ!
}
let input;
do {
input = get_input();
} while (inputIsNotValid(input));
JavaScript の for
ループは C や Java のそれと同じです。これはループの制御情報を 1 行で与えることができます。
for (let i = 0; i < 5; i++) {
// 5 回実行されます
}
JavaScript にはこの他に、特徴的な for ループが 2 つあります。 for...of
は反復可能オブジェクトや多くの特徴的な配列を反復処理し、 for...in
はオブジェクトのすべての列挙可能プロパティを反復処理します。
for (const value of array) {
// 値に関する処理
}
for (const property in object) {
// オブジェクトのプロパティに関する処理
}
switch
文はある数値や文字列を元にした複数分岐に使われます。
switch (action) {
case "draw":
drawIt();
break;
case "eat":
eatIt();
break;
default:
doNothing();
}
C 言語と同様、 case 句は概念的にはラベル付けと同じなので、 break
文を追加しなければ、実行は次のレベルに「落下」します。しかし、実際にはジャンプ表ではありません。文字列や数値リテラルだけでなく、どのような式でも case
句には属せず、照合する値と等しくなるまで 1 つずつ評価されます。比較は ===
演算子を用いて行われます。
Rust など一部の言語とは異なり、制御構造は JavaScript では文ですので、 const a = if (x) { 1 } else { 2 }
のように変数に代入することはできません。
JavaScript のエラーは try...catch
文を使用して処理します。
try {
buildMySite("./website");
} catch (e) {
console.error("サイトの構築に失敗しました:", e);
}
エラーは throw
文を使用して発生させる(投げる)ことができます。多くの組み込み処理も、同様にしてエラーを発生させる可能性があります。
function buildMySite(siteDirectory) {
if (!pathExists(siteDirectory)) {
throw new Error("サイトのディレクトリーが存在しません");
}
}
一般に、 throw
文からは何でもエラーとして投げることができるため、捕捉するエラーの型を指示することはできません。しかし、例えば上の例のように、通常は Error
のインスタンスであると想定することができます。 Error
には TypeError
や RangeError
のような組み込みのサブクラスがいくつかあり、エラーに関する特別な意味づけを提供するために使用することができます。 JavaScript では条件付きの catch はありません。 1 つの種類のエラーだけを処理したい場合は、すべてを catch し、 instanceof
を使用してエラーの型を特定し、他の用途のものは throw しなおす必要があります。
try {
buildMySite("./website");
} catch (e) {
if (e instanceof RangeError) {
console.error("引数が範囲を超えているようです:", e);
console.log("再試行中...");
buildMySite("./website");
} else {
// 他の種類のエラーをどのように処理すればよいのかわからない場合、呼び出す
// スタックの上位にある何かで捕捉して処理できるように throw します。
throw e;
}
}
エラーがコールスタック内の try...catch
で捕捉されなかった場合、プログラムは終了します。
制御フロー文の包括的なリストは、リファレンスの該当部分を参照してください。
オブジェクト
JavaScript のオブジェクトは、名前と値のペアの単純な集合であると考えることができます。これは以下のものに似ています。
- Python の辞書型
- Perl や Ruby のハッシュ
- C や C++ のハッシュテーブル
- Java の HashMap クラス
- PHP の連想配列
JavaScript でオブジェクトはハッシュです。静的型付け言語のオブジェクトとは異なり、 JavaScript でオブジェクトは固定された形状を持ちません - プロパティはいつでも追加したり、削除したり、並べ替えたり、変更したり、動的に問い合わせたりすることができます。オブジェクトのキーは常に文字列またはシンボル。配列の添字でさえ、正規的には整数ですが、その基盤は文字列です。
空のオブジェクトを生成する 2 つの基本的な方法があります。
const obj = {
name: "Carrot",
for: "Max",
details: {
color: "orange",
size: 12,
},
};
オブジェクのプロパティにアクセスするには、ドット (.
) または角括弧 ([]
) を使用することができます。ドット記法を使用する場合、キーは有効な識別子でなければなりません。一方、角括弧を使用すると、動的なキー値でオブジェクトのインデックスを指定することができます。
// ドット記法
obj.name = "Simon";
const name = obj.name;
// ブラケット記法
obj["name"] = "Simon";
const name = obj["name"];
// 変数をキー定義に使用できる
const userName = prompt("キーは何ですか?");
obj[userName] = prompt("値は何ですか?");
プロパティのアクセスは連鎖させることができます。
obj.details.color; // orange
obj["details"]["size"]; // 12
オブジェクトは常に参照なので、何かが明示的にオブジェクトをコピーしない限り、オブジェクトへの変更は外部から見えることになります。
const obj = {};
function doSomething(o) {
o.x = 1;
}
doSomething(obj);
console.log(obj.x); // 1
これはまた、別個に作成された2つのオブジェクトは異なる参照であるため、決して等しくならない (!==
) と いう意味も含んでいます。同じオブジェクトの参照を2つ持っている場合、片方を変更するともう一方のオブジェクトから観察することができます。
const me = {};
const stillMe = me;
me.x = 1;
console.log(stillMe.x); // 1
オブジェクトとプロトタイプの詳細については、 Object
のリファレンスページを参照してください。オブジェクト初期化構文の詳細については、リファレンスページを参照してください。
このページでは、オブジェクトのプロトタイプと継承についての詳細はすべて省略しました。というのも、通常は(難解に聞こえるかもしれない)基盤のメカニズムに触れることなく、クラスで継承を実現できるからです。これらについては、継承とプロトタイプチェーンを参照してください。
配列
JavaScript における配列は、実はオブジェクトの特殊型です。普通のオブジェクトとほとんど同じように働きます(数値のプロパティは当然 []
の構文でのみアクセスできます)が、しかし配列は length
という魔法のプロパティを持っています。これは常に配列の一番大きなインデックスより 1 だけ大きい値を取ります。
配列を生成する方法のひとつは以下の通りです。
const a = ["dog", "cat", "hen"];
a.length; // 3
JavaScript の配列はオブジェクトでもあり、任意の数値を含めたあらゆるプロパティを配列に割り当てることができます。唯一の「魔法」は、特定のインデックスを設定すると、 length
が自動的に更新されることです。
const a = ["dog", "cat", "hen"];
a[100] = "fox";
console.log(a.length); // 101
console.log(a); // ['dog', 'cat', 'hen', 空 × 97, 'fox']
上記で得られた配列は、途中に空きスロットがあるため疎配列と呼ばれ、エンジンは配列としての最適化を行わなくなりハッシュ表になります。配列が密に配置されていることを確認してください。
範囲外のインデックスを使用しても例外は発生しません。配列の存在しないインデックスを求めようとすると、 undefined
の値が返ります。
const a = ["dog", "cat", "hen"];
console.log(typeof a[90]); // undefined
配列は任意の要素を持つことができ、任意に増減することができます。
const arr = [1, "foo", true];
arr.push({});
// arr = [1, "foo", true, {}]
配列は他の C 言語風の言語と同じように、 for
ループで反復処理することができます。
for (let i = 0; i < a.length; i++) {
// a[i] について何かする
}
あるいは、配列は反復処理可能なので、 C++/Java の for (int x : arr)
構文と同義の for...of
ループを使用することもできます。
for (const currentValue of a) {
// currentValue (現在の値) で何かをする
}
配列には、たくさんの配列メソッドがあります。例えば map()
はすべての配列要素にコールバックを適用し、新しい配列を返します。
const babies = ["dog", "cat", "hen"].map((name) => `baby ${name}`);
// babies = ['baby dog', 'baby cat', 'baby hen']
関数
関数は、オブジェクトとともに JavaScript を理解するうえで核となる構成要素です。基本的な関数は極めてシンプルです。
function add(x, y) {
const total = x + y;
return total;
}
これは基本的な関数を例示しています。JavaScript の関数は 0 以上の名前のついた引数を取ることができます。関数の本体は好きなだけたくさんの文を含ませることができ、またその関数内で局所的な変数を宣言することができます。 return
文は好きなときに関数を終了し値を返すために使うことができます。もし return 文が使われなかったら(あるいは値を持たない空の return が使われたら)、JavaScript は undefined
を返します。
関数は、その関数を指定する引数の数より多くても少なくても呼び出すことができます。関数が期待している引数を渡さずに呼び出すと、その引数は undefined
に設定されます。もし期待する以上の引数を渡すと、関数は余分な引数を無視します。
add(); // NaN
// Equivalent to add(undefined, undefined)
add(2, 3, 4); // 5
// 第 1、第 2 引数を加算。4 は無視される
他にも利用できる引数の構文があります。例えば、残余引数構文は、 Python の *args
と同じように、呼び出し側から渡された余分な引数をすべて配列に集合させることができます。(JS は言語レベルで名前付き引数を持っていないので、 **kwargs
はありません。)
function avg(...args) {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
上のコードでは、変数 args
に関数に渡された値をすべて格納しています。
残余引数は、宣言された引数以降をすべて格納しますが、宣言される前の引数は格納しません。言い換えれば、 function avg(firstValue, ...args)
は関数に渡された最初の値を firstValue
変数に格納し、残りの引数を args
に格納します。
関数が引数のリストを受け入れ、それらがすでに配列にある場合、関数呼び出しの中でスプレッド構文を使って、配列を要素のリストとして展開することができます。例えば avg(...numbers)
のようにします。
JavaScript には名前付き引数がないと述べました。しかし、オブジェクトを便利にパックしたり展開したりできるオブジェクト分割代入を使用して実装することは可能です。
// { } 中括弧はオブジェクト野分割代入する
function area({ width, height }) {
return width * height;
}
// { } 中括弧はここでは新しいオブジェクトを作成する
console.log(area({ width: 2, height: 3 }));
また、デフォルト引数構文もあります。これは、省略した引数(または値がdefinedとして渡された引数)に既定値を持たせるものです。
function avg(firstValue, secondValue, thirdValue = 0) {
return (firstValue + secondValue + thirdValue) / 3;
}
avg(1, 2); // 1, instead of NaN
無名関数
JavaScript では無名関数、つまり名前のない関数を作成することができます。実際には、無名関数は他の関数の引数として使用されたり、関数を呼び出すために使用できる変数に代入されたり、他の関数から返されたりします。
// 括弧の前に関数名がないことに注意
const avg = function (...args) {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
};
これにより、引数を指定して avg()
を呼び出すことで、無名関数を呼び出すことができるようになります。つまり、意味づけとしては function avg() {}
という宣言構文を使用して関数を宣言することと同じになります。
無名関数を定義する方法は、他にもアロー関数式を使用する方法があります。
// 括弧の前に関数名がないことに注意
const avg = (...args) => {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
};
// 単に式を返す場合は `return` を省略できる
const sum = (a, b, c) => a + b + c;
アロー関数は意味的に関数式と等価ではありません。詳しくは、そのリファレンスページを参照してください。
関数式が役に立つ他の場面もあります。単一の式で関数の宣言と起動を同時に行う仕組みがあります。これは IIFE (Immediately invoked function expression) と呼ばれます。
(function () {
// …
})();
IIFE の用途については、クロージャでプライベートメソッドを模倣するで見ることができます。
再帰関数
JavaScript では関数を再帰的に呼び出すことができます。これは特にブラウザーの DOM などにみられる木構造を取り扱うときに便利でしょう。
function countChars(elm) {
if (elm.nodeType === 3) {
// TEXT_NODE
return elm.nodeValue.length;
}
let count = 0;
for (let i = 0, child; (child = elm.childNodes[i]); i++) {
count += countChars(child);
}
return count;
}
関数式には名前を付けることができるので、同様に再帰にすることができます。
const charsInBody = (function counter(elm) {
if (elm.nodeType === 3) {
// TEXT_NODE
return elm.nodeValue.length;
}
let count = 0;
for (let i = 0, child; (child = elm.childNodes[i]); i++) {
count += counter(child);
}
return count;
})(document.body);
上記のように関数式に与えられた名前は、関数自身のスコープ内でのみ有効です。これはエンジンによる高度な最適化を実現して、結果的に可読性が高いコードになります。この名前はデバッガーやスタックトレースにも表示されますので、デバッグにかかる時間を節約できます。
関数型プログラミングを使用している場合、 JavaScript での再帰のパフォーマンスへの影響に注意してください。言語仕様では末尾呼出し最適化を指定していますが、スタックトレースの復元やデバッガビリティが難しいため、JavaScriptCore(Safariで使用されています)のみ実装しています。深い再帰では、スタックオーバーフローを避けるために代わりに反復処理を用いることを検討してください。
関数は第一級オブジェクト
JavaScript の関数は第一級のオブジェクトです。これは、関数が変数に割り当てることができ、他の関数に引数として渡すことができ、他の関数から返すことができることを意味しています。さらに JavaScript では、明示的にキャプチャすることなくすぐにクロージャに対応しているため、関数型プログラミングのスタイルを便利に適用することができます。
// 関数を返す関数
const add = (x) => (y) => x + y;
// 関数を受け入れる関数
const babies = ["dog", "cat", "hen"].map((name) => `baby ${name}`);
JavaScript の関数はそれ自体がオブジェクトであり、 JavaScript で他のものと同様に、オブジェクトの節で見てきたような、プロパティの追加や変更ができることに注意してください。
内部関数
JavaScript での関数宣言は他の関数内でも行うことができます。 JavaScript で関数を入れ子にすることの重要なことは、内部関数内で親関数スコープの変数にアクセスできることです。
function parentFunc() {
const a = 1;
function nestedFunc() {
const b = 4; // parentFunc はこれを使用できない
return a + b;
}
return nestedFunc(); // 5
}
内部関数は保守しやすいコードを書くときに多大な利便性をもたらします。ある関数が他の部分のコードでは役立たない関数を 1 つか 2 つ使っているなら、これらのユーティリティ関数を他から呼び出される関数の入れ子にすることができます。内部関数はグローバルスコープでなくなるので、いいことです。
内部関数はグローバル変数を使うという誘惑に対する対抗措置です。複雑なコードを書くとき、複数の関数間で値を共有するためにグローバル変数を使いたくなります。しかし、これでは保守がしづらくなります。内部関数は親関数の変数を共有できるので、グローバルな名前空間を消費せずに複数の関数をまとめることができます。
クラス
JavaScript には class 構文があり、これは Java などの言語にとてもよく似ています。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name}!`;
}
}
const p = new Person("Maria");
console.log(p.sayHello());
JavaScript のクラスは単なる関数であり、 new
演算子でインスタンス化する必要があります。クラスはインスタンス化されるたびに、そのクラスが指定したメソッドやプロパティを格納したオブジェクトを返します。クラスはコードの整理を強制しません。例えば、クラスを返す関数を持つこともできますし、ファイルごとに複数のクラスを持つこともできます。例えば、クラスの生成はアロー関数から発生した式に過ぎません。このパターンはミックスインと呼ばれます。
const withAuthentication = (cls) =>
class extends cls {
authenticate() {
// …
}
};
class Admin extends withAuthentication(Person) {
// …
}
静的プロパティは先頭に static
を付けて作成します。プライベートプロパティは先頭にハッシュ #
を付けて作成します(private
ではありません)。ハッシュはプロパティ名の一部です。(#
は Python の _
と考えてください。)他の多くの言語とは異なり、派生クラスであっても、クラス本体の外でプライベートプロパティを読み取る方法はありません。
様々なクラス機能の詳細なガイドについては、ガイドページを参照してください。
非同期プログラミング
JavaScript は本質的に単一スレッドです。並列処理はなく、並行処理のみです。非同期プログラミングはイベントループによって行われ、設定するにはタスクの集合をキューに入れ、完了をポーリングします。
JavaScript で非同期コードを書く慣用的な方法は 3 つあります。
- コールバックベース(
setTimeout()
など) - プロミス (
Promise
) ベース - プロミスの糖衣構文である
async
/await
例えば、 JavaScript でファイル読み込み処理をすると次のようになります:
// コールバックベース
fs.readFile(filename, (err, content) => {
// このコールバックは、ファイルが読み込まれたときに呼び出される
if (err) {
throw err;
}
console.log(content);
});
// このコードは、ファイルが読み込まれるのを待っている間に実行される
// プロミスベース
fs.readFile(filename)
.then((content) => {
// ファイルが読み取られたときに実行されること
console.log(content);
})
.catch((err) => {
throw err;
});
// このコードは、ファイルが読み込まれるのを待っている間に実行される
// Async/await
async function readFile(filename) {
const content = await fs.readFile(filename);
console.log(content);
}
コア言語では非同期プログラミング機能を特に指定していませんが、外部環境と対話する際には非常に重要です。ユーザーの許可を依頼したり、データを取得したり、ファイルを読み取ったりするときなどです。長時間実行される可能性のある処理を非同期にしておくことで、この処理が待機している間も他の処理を実行できることを保証します。例えば、ユーザーが許可を与えるボタンをクリックするのを待っている間にブラウザーがフリーズしないようにします。
非同期の値がある場合、その値を同期的に取得することはできません。例えば、プロミスがある場合、最終的な結果にアクセスするには then()
メソッドを使用するしかありません。同様に、 await
は非同期コンテキスト(通常は非同期関数やモジュール)でしか使用できません。プロミスは決してブロッキングされません。プロミスの結果に依存するロジックだけが遅延され、他のすべてはその間に実行され続けます。関数型プログラマーであれば、プロミスは then()
で割り当てられたモナドであると認識するかもしれません(しかし、これらは自動平坦化されるため、完全なモナドではありません。つまり Promise<Promise<T>>
を作ることはできません)。
実際、シングルスレッドモデルであるにもかかわらず、 Node.js はノンブロッキング IO のため、多数のデータベースやファイルシステムリクエストを処理してもとてもパフォーマンスが高く、サーバーサイドプログラミングによく使われています。しかし、純粋な JavaScript である CPU バウンド(計算集約的な)タスクはメインスレッドをブロックします。本当の並列化を実現するには、ワーカー を使用する必要があるかもしれません。
非同期プログラミングについてもっと学ぶには、プロミスの使用を読むか、非同期 JavaScript チュートリアルに従ってください。
モジュール
JavaScript はほとんどのランタイムで対応しているモジュールシステムも指定しています。モジュールは通常ファイルであり、ファイルパスまたは URL で識別されます。モジュール間でデータを交換するために import
および export
文を使用することができます。
import { foo } from "./foo.js";
// エクスポートされていない変数はモジュールにローカル
const b = 2;
export const a = 1;
Haskell、Python、Java などとは異なり、 JavaScript のモジュール解像度は完全にホスト定義です。通常は URL やファイルパスに基づいているので、相対ファイルパスは「うまく行き」、プロジェクトのルートパスではなく現在のモジュールのパスからの相対パスとなります。
しかし、 JavaScript 言語は標準ライブラリーモジュールを提供していません。すべてのコア機能は、代わりに Math
や Intl
のようなグローバル変数によって動いています。これは、 JavaScriptが長い間モジュールシステムを欠いてきた歴史と、モジュールシステムを選ぶとランタイムのセットアップにいくつかの変更を伴うという事実によるものです。
ランタイムが異なれば、使用するモジュールシステムも異なります。例えば、 Node.js はパッケージマネージャ npm を使用し、ほとんどの場合ファイルシステムベースですが、 Deno やブラウザーは完全に URL ベースで、 HTTP URL からモジュールを解決することができます。
詳しい情報はモジュールガイドページを参照してください。
言語とランタイム
このページを通して、ある機能は「言語レベル」であり、他にも「ランタイムレベル」の機能があることを常に述べてきました。
JavaScript は汎用スクリプト言語です。コア言語仕様は、純粋な計算ロジックに焦点を当てています。入出力は扱いません。実際、特別なランタイムレベルの API (最も有名なものが console.log()
)がなければ、 JavaScript プログラムの動作は完全に観察不可能です。
ランタイム、またはホストとは、 JavaScript エンジン(インタープリター)にデータを供給し、特別なグローバルプロパティを提供し、エンジンが外の世界と対話するための仕掛けを提供するものです。モジュール解決、データの読み込み、メッセージの出力、ネットワークリクエストの送信などはすべてランタイムレベルの処理です。 JavaScript はその誕生以来、 DOM のような API を提供するブラウザー、ファイルシステムアクセス などの API を提供する Node.js など、さまざまな環境で採用されてきました。 JavaScript はウェブ(これが本来の目的でした)、モバイルアプリ、デスクトップアプリ、サーバーサイドアプリ、サーバーレス、組み込みシステムなどでうまく統合されています。 JavaScript のコア機能を学ぶ一方で、知識を使用するためにはホスティング提供された機能を理解することも重要です。例えば、ウェブプラットフォーム API はすべて読むことができ、これらはブラウザー、時にはブラウザー以外によって実装されます。
さらなる探究
このページでは、 JavaScript の様々な機能が他の言語と比較してどう異なるのかという観点で、とても基本的なことを説明しています。言語そのものや各機能の細部についてもっと知りたい場合は、JavaScript ガイドや JavaScript リファレンスを参照してください。
言語の本質的な部分については、紙面や 複雑さのために省略した部分もありますが、自分自身で探求してください。