NOTE: この記事は、弊社内のオンボーディング資料(2023-Q4)を公開したものです。
主に他言語の経験がある人向けのJS/ECMAScriptの基本文法です。
実行環境
JavaScriptの処理系にはさまざまなものが存在します。大まかに分けると
- ブラウザ上での実行
- NodeJS上での実行
に分かれます。それぞれの環境において、実行可能なECMAScriptのバージョン、利用可能ランタイム、モジュール解決方法が異なります。
- バージョンに関しては、ECMAScript2020相当のコードであればほぼすべての環境でサポートされており、2023年現在で困ることはありません。
- ランタイムに関しては、ブラウザとNodeJSで大きく異なります
- モジュール解決方法には、 CommonJS方式とECMAScript Module(ESM)方式が混在しています。将来的にESMに統一される方向性とはなるものの、時間がかかりそうです
基本的な文法
- 大文字・小文字は区別されます
- ステートメントは、セミコロンで区切ることができます
-
{}
を用いてブロックを構成します。
{
statement_1;
statement_2;
⋮
statement_n;
}
- ブロックは変数のスコープとして機能し、
let
またはconst
を用いて変数を宣言できます。
{
let var1 = 'text'; // 上書き可能
var1 = 'text2';
const var2 = new Date(); // constの場合、再宣言は不可
}
基本的なデータ型
JSにおいては、プリミティブと呼ばれる7種類のスカラと、オブジェクトという構造のデータ型が用意されています。
基本となるプリミティブ
データ型 | 概要 |
---|---|
boolean |
true または false
|
string |
文字列型 |
number |
整数及び浮動小数点数 |
特殊なプリミティブ
データ型 | 概要 |
---|---|
null |
null値を示す特殊なキーワード |
undefined |
値が未定義であることを示す |
-
undefined
は他の言語にない概念で、その実態はグローバルスコープの変更不可のプロパティ です。変数に値・参照のいずれも代入されていないこと、もしくは関数が何も返却しないことを表す場合に用いられます。 - 意図的に変数が空であることを示すには
null
を用いることになります。NodeJSではnull
を渡すことで、ストリームの末端を示すなど特殊な役割を担う場合もあります。 -
null
はtypeof
演算子を用いると、"object"
が返ってくるため、場合わけには注意が必要です。
他言語との互換性を考慮する場合には、null
を用いた方が良いケースが多いです。ただ、実際のJSの実装において、この2つを細かに使い分けるシーンはそれほど多くはありません。
近年導入されたプリミティブ
データ型 | 概要 |
---|---|
bigint |
自由精度整数値。大きな整数を取り扱うために用います。ES2020で導入されました。 |
symbol |
インスタンスが固有で不変となるデータ型。ES2015(ES6)で導入されました。 |
symbol
は、RubyのSymbolクラスと大きく異なることに注意してください。
- Rubyでは唯一性が担保されたimmutableなオブジェクトで「シンボルが同じであれば同じオブジェクト」ですが、JSではその逆で「シンボルが同じであっても絶対に異なるオブジェクト」 を生成します。
- 主にプロパティのキーとして、他のいかなるプロパティとも絶対に衝突しないキーを設定する際に用いられます。
Object型
JavaScript のほぼすべてのオブジェクトが Object
データ型 のインスタンスです。
一般的なオブジェクトは、プロパティを (メソッドを含めて) Object.prototype
から継承していますが、これらのプロパティはシャドウ化 (別名オーバーライド) されている場合があります。(後述)
オブジェクトは、JavaScriptスレッド内の何らかのデータに対する参照となっており、プリミティブと異なり、直接データを保持していません。
数値型の取り扱い
JavaScriptでは、 int
や float
のような概念はなく、基本的に数値は Number型で表現されます。
その実態は、IEEE 754 の倍精度 64ビットバイナリー形式であり、 Java や C# の double に近いものです。小数値を表すことができますが、格納できる数値の大きさと精度には制限があります。
- 小数の計算では、精度に注意
- 大型の整数計算ではBigIntを用いることを検討する(互換性や型変換に注意)
- 数値変換は次のルールに基づく
- undefined は NaN (特殊な数値定数)になる
- null は 0 になる
- true は 1 に、false は 0 になる
また、gRPC, Avroを用いる場合などでは、ライブラリによって暗黙的または明示的なデータ変換を行うことになります。
構造体・タプル・ポインタ型
ES2022までのバージョンでは、構造体やタプルのようなものは存在せず、全てオブジェクト型になります。ポインタ型も存在しません。
これらも事情もあり、JavaScriptの処理系において、コードからOSのAPIを直接コールすることは、ほぼありません。
近い将来、複合プリミティブ型として、RecordとTupleが新たに導入されるかもしれません(この記事を書いた時点では、Stage2 )。
新しいプリミティブということもあり、互換性の観点から普及までは利用を控えた方が良いかもしれません。
演算子のオーバーロード
JavaやC#、Pythonと異なり、ECMAScriptには演算子のオーバーロードが存在しません。
そのため、とあるデータ型とデータ型の加算といった処理を記述することができません。
例えば、ベクターとベクターの足し算のような表現は原則として実現できません。
計算処理を行わせる場合には、その点を考慮し、他処理系の利用も検討してください。
組み込みオブジェクト
いわゆる論理データ型であり、基本的なデータ型を組み合わせたオブジェクトとして表現されます。
あらかじめ、JSのランタイムとして多くの組み込みオブジェクトが存在しています。また、NodeJS環境では、NodeJSランタイムによる固有の組み込み型があります。
ここでは、代表的なもののみ列挙します。
オブジェクト | 概要 |
---|---|
Array | 配列 |
Object | オブジェクト |
Date | 日付 |
RegExp | 正規表現 |
Error | エラー型 |
Set | 値コレクション |
Map | Key-Valueコレクション |
Math | 数学計算 |
ArrayBuffer | バイト配列(操作不可) |
TypedArray | バイナリデータビュー |
Buffer | NodeJSにおけるバイト配列 |
Promise | 非同期オブジェクト |
バイナリデータの取り扱い:その1
単純なバイト配列は ArrayBuffer
によってメモリ上に配置することができます。
// 8バイトのバッファ
const buffer = new ArrayBuffer(8);
ArrayBufferを含むいくつかの組み込みデータ型オブジェクトは、直接メモリ上のデータを保有するため、移譲可能(Transferable)なオブジェクトとなっています。
移譲可能オブジェクトは、別コンテキスト(ワーカーなど)に高速にコピーすることができます。
バイナリデータの取り扱い:その2
サーバサイド実装などにおけるNodeJSにおいては、NodeJSランタイムから提供されるBuffer
型を用います。
import { Buffer } from 'node:buffer';
Buffer型は固定長バイト配列であり、文字列や数値配列から変換することも可能です。
// 8バイト
Buffer.alloc(8);
// <Buffer 00 00 00 00 00 00 00 00>
// 他データからの変換
Buffer.from('my-text','utf8');
Buffer.from([1, 64, 255, 256]); // <Buffer 01 40 ff 00>
バイナリデータの取り扱い:その3
一方、ブラウザや非NodeJSの実行処理系では、Bufferは利用することはできません。
ArrayBuffer
型は、メモリ操作を許していないため、そのデータビューに相当する TypedArray
型を用います。
const buffer = new ArrayBuffer(8);
// 4バイト(long型相当)への変換
const view = new Int32Array(buffer);
TypedArrayには次のようなものが用意されています。
型 | 値の範囲 | サイズ (バイト数) | Web IDL型 |
---|---|---|---|
Int8Array | -128 から 127 | 1 | byte |
Uint8Array | 0 から 255 | 1 | octet |
Uint8ClampedArray | 0 から 255 | 1 | octet |
Int16Array | -32768 から 32767 | 2 | short |
Uint16Array | 0 から 65535 | 2 | unsigned short |
Int32Array | -2147483648 から 2147483647 | 4 | long |
Uint32Array | 0 から 4294967295 | 4 | unsigned long |
Float32Array | -3.4E38 から 3.4E38 および 1.2E-38 (最小の正の数) | 4 | unrestricted float |
Float64Array | -1.8E308 から 1.8E308 および 5E-324 (最小の正の数) | 8 | unrestricted double |
BigInt64Array | -263 to 263 - 1 | 8 | bigint |
BigUint64Array | 0 to 264 - 1 | 8 | bigint |
それぞれの格納にあたってのエンコードがどのようになるかは、MDNのドキュメントを参照してください。
バイトオーダーを意識する必要がある場合
DataView
という異なる組み込みオブジェクトも存在します。
JS処理系は、通常、バイナリの取り扱いはホストバイトオーダに従います。エンディアンを意識する場合には、こちらの方が便利でしょう。
結局どれを使えば・・・?
最も使いやすいのは、フロントエンドではUint8Array
、サーバサイドではBuffer
を使い分ける方法かなと(個人的には)思います。
すでにNodeJSのBufferもUint8Arrayのインスタンスですので、Uint8Arrayに統合しても良いでしょう。
標準入出力
print
に該当するものとして、 console
オブジェクトが用意されています
console.log('Hello, world');
console.error('Error!); // NodeJSにおいては、標準エラー出力
標準入力については基本的にNodeJSでのみ利用できます。記述はストリームを用いることが前提のため、ここでは省略します。
オブジェクト
JavaScriptのほとんどは、オブジェクト操作による処理を記述することにあります。
オブジェクトは、先に記載の通り、メモリ上のデータ参照を構造化したものであり、参照渡しとなっています。
また、ポインタに該当する概念がありません。
オブジェクトは {}
によって宣言します。
内容が同一であっても異なるメモリ上のデータを参照しているため、比較演算子ではfalseが返ります。
要素アクセスは 、.
または []
による添え字を用いてアクセスします。
const obj1 = { name: 'John Smith' }
cosnt obj2 = { name: 'John Smith' }
obj1 === obj2 // false
console.log(obj1.name) // 'John Smith'
obj2.name = 'Kevin mitonic' // const宣言であっても、プロパティの値の入れ替えは可能
console.log(obj2['name']) // Kevin mitonic'
配列は、[]
によって宣言します。
要素アクセスは数字の添え字を用いて行います。
const ary = ['john', 'smith'];
console.log(ary[0]); // `john`
異なるデータ型を混在させることもできます。
ただし、使い勝手が悪くデータ型の推測もできないため、用いることは多くはありません。
cosnt ary = ['text', 0 , false]
オブジェクトと配列は組み合わせて、複雑な構造をとることができます。
const obj1 = {
message: 'Hello, world',
isDisabled: false,
profile: {
name: 'John Smith',
age: 20
},
tags: ['user','member' ]
}
JSON
オブジェクトは、JSONという文字列データにエンコードすることができます。
cosnt str = JSON.stringify(obj1);
console.log(str);
変換後は下記のようなテキストデータが得られます。
{"message":"Hello, world","isDisabled":false,"profile":{"name":"John Smith","age":20},"tags":["user","member"]}
文字列からオブジェクトにデコードし直すには、 JSON.parse
を用います。
const obj = JSON.parse(str);
なお、JSONは記述が冗長であるものの、その仕様は厳密であるため、 JSON.parse
の処理速度は実はそこそこ早いという特徴があります。
組み込みオブジェクトのJSON化
特に注意しなくてはならないのは、組み込みオブジェクトのJSON.stringify
時の挙動です。
どのような値にエンコードされるかは、その組み込みオブジェクトによって異なります。
デコード時には元の組み込みオブジェクトに自動変換はできないため、JSON.stringify
/JSON.parse
を対照的なシリアライザ・デシリアライザとして用いることはできません。
const obj = { date: new Date() }
// { date: 2023-12-03T03:40:29.401Z } であり、dateプロパティにはDate型オブジェクトが格納されている
const str = JSON.stringify(obj);
const parsedObj = JSON.parse(str);
// { date: '2023-12-03T03:40:29.401Z' } が得られる。dateプロパティはstring型になっている。
循環参照を持つJavaScriptオブジェクト
JSONに変換しようとするとエラーになりますので、注意してください。
これらを回避できるライブラリが多くあります。
- https://www.npmjs.com/package/safe-stable-stringify
- https://www.npmjs.com/package/fast-json-stable-stringify
- https://www.npmjs.com/package/json-stable-stringify
構文
JavaやC#にも似た文法を持っています。
関数
関数は、function
ステートメントで宣言します。
複数の引数を定義したり、デフォルト引数などを指定することができます。
関数の中身はブロックになっており、内部で変数を宣言し、用いることができます。
function myFunc(arg1, arg2, arg3='defaultValue') {
// 変数宣言は原則として const (定数宣言)を用いる
const var1 = 'text';
const var2 = arg2;
return var1 + var2; // 返り値は
}
匿名関数はアロー表記(=>)を用いることで表現できます。
const arrowFunc = (arg) => {
return arg.toString()
}
関数の引数の処理
実際に変数が何を指し示しているか(値そのものか、値への参照であるのか)は、普段意識することはありません。
関数の引数においては、オブジェクトや配列である場合には、参照の値渡しと呼ばれるような処理が発生します。
ポインタ型が存在しないため、例えば、swap関数(値の入れ替え)などを実装する場合には、注意が必要です。普段の実装としては、プリミティブは値渡しのように、オブジェクトや配列は参照渡しのように振る舞います。
より細部の振る舞いは、V8エンジンの領域となるため、本文書では割愛します。
try-catch構文
try {
otherFunc();
} catch(e) {
console.error(e);
} finally {
console.log('end');
}
条件分岐
if, else if, else構文
if ( a === 'test' ) {
console.log('ok');
else if ( a === 'not test') {
console.warn('warning!');
} else {
console.error('error!');
}
三項演算子
const result = a === 'test' ? 'ok' : 'error';
Switch構文
Cっぽい形です。
switch (b) {
case 'test':
console.log('ok');
break;
// C同様、複数のcase文を重ねることができる
case 'not test':
case 'not test2':
console.warn('warning!');
break;
default:
console.error('error!');
}
}
繰り返し
for文
Cによく似ています
for (let index = 0; index < array.length; index++) {
const element = array[index];
// breakやcontinueも利用できます。
}
while文
while, do-whileのどちらも利用できます。
while (condition) {
/** ループ処理 */
}
do {
/** ループ処理 */
} while (condition);
イテレイティング処理
列挙可能(Iteratable)なオブジェクトである配列やオブジェクトの要素に対する繰り返し処理構文も用意されています。
// 要素の列挙
for (const iterator of object) {
}
// キーの列挙
for (const key in object) {
const element = object[key];
}
文字列操作
テンプレート
const username = 'john';
const age = 20;
console.log(`${username} ${20}`) // 'john 20' と表示される
分割と結合
const ary = 'a,b,c'.split(',');
const txt = ary.join('-'); // 'a-b-c'となる
切り出し
const str = 'Mozilla';
console.log(str.substring(1, 3)); // oz
console.log(str.substring(2)); // zilla
検索
'abcd'.indexOf('cd'); // 2
'abcd'.match(/ABC/i); // [ 'abc', index: 0, input: 'abcd', groups: undefined ]
'abcd'.search(/cd/i); // 2
'abcd'.includes('cd'); // true
配列操作
一般的に、JavaScriptにおいては配列の処理速度は速くはありません。
特に大規模なデータセットではその処理が遅くなる傾向があります。使い所をよく検討してください。
配列そのものへの追加と削除
可変長配列であるため、要素の追加と取り出しが可能です。
const ary = ['b','c']
// 先頭の処理
ary.unshift('a') // ary => ['a','b','c']
ary.shift() // ary => ['b','c']
// 末尾の処理
ary.push('d') // ary => ['b','c','d']
ary.pop() // ary => ['b','c']
また、配列の操作では、より複雑な処理ができる splice
が用意されています。
const ary = ['月', '水', '木', 'June'];
// index=1 に挿入
let index = 1;
ary.splice(index, 0, '火'); // ary => ["月", "火", "水", "木", "土"]
// index=4から変更(1つ削除し、挿入)
index = 4;
ary.splice(index, 1, '金'); // ary => ["月", "火", "水", "木", "金"]
直感的な引数ではありませんが 第1引数のi番目からの要素を変更する
と覚えておきましょう。
配列の再生成・切り出し
配列そのものの操作を行わず、新たに配列を作成するものもあります。
なお、新たな配列であっても、中の要素がオブジェクトの場合には同じデータを参照していることを忘れないでください。
再生成処理
フィルタリング
[1,2,3,4,5].filter(n => 3 < n); // [4,5]
抽出
// 2番目の次の要素から、-1番目(最後から1番目の手前)まで取り出す
["月", "火", "水", "木", "金"].slice(2, -1); // ["水","木"]
再加工
[1,2,3,4,5].map(a => a * 2); // => [2,4,6,8,10]
並び替え
[3,1,4,2,5].sort(); // => [1,2,3,4,5]
// 並び替えのための評価式を追加する。
[3,1,4,2,5].sort((a,b) => a - b); // => [1,2,3,4,5]
[3,1,4,2,5].sort((a,b) => b - a); // => [5,4,3,2,1]
反転
[1,2,3,4,5].reverse(); // => [5,4,3,2,1]
プロトタイプ指向
JavaScriptはプロトタイプ指向が採用されています。
詳細は別記事であるこちらを参照してください。
タイマー
歴史的背景から、JavaScriptにはタイマーが内蔵されています。
代表的なものはsetTimeout
です。
指定したミリ秒待機してから一度だけコールバックを実行してくれます。
setTimeout(() => {
console.log('waited!')
}, 1000);
定期的なコールバック呼び出しを行う setInterval
も存在します。
const timer = setInterval(() => {
console.log('hello!');
}, 1000);
setInterval
は、一度定義すると、その後、ずっと実行され続けます。タイマーを取り消すには、clearInterval
を用います。
clearInterval(timer)
このように、明示的に削除しない限り継続してしまうsetIntervalはメモリリークを引き起こしやすいため、取り扱いは慎重になるべきです。
Promise
Promiseは、外部HTTP APIの実行やディスクアクセスのような非同期の処理を記述するために導入されたものです。
C#のPromiseとよく似ており、JavaではCompletableFutureが似た概念になります。
Promiseでは、将来、実行される処理をthen/catch/finalyコールバックの形で定義することができます。
const myPromise = new Promise((resolve, reject) => {
try {
const result = someHeavyProcess();
resolve(result); // 完了したら呼び出すのがresolve
} catch(e) {
reject(e); // 失敗したら呼び出すのがreject
}
});
myPromise
.then(() => console.log('finished')) // 完了時コールバック
.catch((e) => console.error(e)); // エラー発生時コールバック
console.log('ok');
Promiseの動作順序
- 上記の例では、Promiseインスタンス
myPromise
を作成しています。 - このインスタンスの作成は瞬時におわり、
ok
が表示されます。 - Promiseの内部で定義した処理は、バックグラウンドで実行され続け、完了または失敗、いずれかの段階で、
resolve
またはreject
を呼び出します。 - すると、あらかじめ
then
やcatch
で登録していたコールバックが実行される仕組みとなっています。
これらの処理の間にもJavaScript本体の処理は進んでいきますので、一般的に、Promiseは非同期処理に用いられます。
Promiseをあえて待つ
場合によっては、Promiseをあえて待ちたい場合もあるでしょう。その場合には、 await
を用います。
const myPromise = new Promise((resolve, reject) => {
try {
const result = someHeavyProcess();
resolve(result); // 完了したら呼び出すのがresolve
} catch(e) {
reject(e); // 失敗したら呼び出すのがreject
}
});
try {
await myPromise
console.log('ok');
} catch(e) {
console.error('failed')
}
awaitを組み合わせることで、通常のJavaScript同様の順序での記述ができます。
「あれ?待たなくてもいいようにPromiseを導入するのに、await
でまつの?それって元々の書き方と同じじゃないの?」と考えるかもしれません。
このPromise化の威力は、複数の非同期関数を多数組み合わせて実行する場合に、可読性が大幅に向上する点にあります。次のセクションで例を示します。
非同期関数 async function
ECMAScriptはawaitを利用できる範囲を非同期関数の内部のみと定めています(将来、変更される可能性あり)。
一度でも await
を用いると、その部分を含むブロック全てが非同期な実行であるとみなされるからです。
そのため、非同期関数を定義するための async function
ステートメントが存在します。
async function fetch(){
const userId = await getUserIdFromAPI();
const data = await fetchUserDataFromAPI(userId);
return data;
}
fetch();
これは、次の記述と同じ意味になります。
function fetch(){
return new Promise((resolve, reject) => {
getUserIdFromAPI()
.then(userId => {
return fetchUserDataFromAPI(userId)
.then(data => {
resolve(data);
})
.catch(e => {
reject(e)
})
});
}
fetch();
awaitを用いることで、非常にシンプルに書けることがわかりますね。
asyncとawaitは癖がありますが、特にNodeJSにおいては、コールバック地獄を避けた上でブロッキングのない処理を記述できますので、よく使い方に習熟しておく必要があります。
そのほかの重要な概念
さて、ここまでで基本的なJavaScriptの文法や概念を学習できたかと思います。
より詳しくはMDNから提供されているJavaScriptリファレンスを熟読してください。
ここでは、さらに追加の重要な概念に関して列挙しておきます。
NodeJSにおけるEventEmitter
ブラウザにはない概念の1つとして、NodeJSにおけるEventEmitterがあります。
ノンブロッキング処理を実現するための重要な組み込み型オブジェクトであり、Promiseよりも柔軟かつ強力な非同期処理の仕組みを提供します。
Promise, EventEmitter, StreamはNodeJSのコアであるlibuvを最大限活かす概念ですので、ぜひ習得しましょう。
TypeScript
JavaScriptを拡張したTypeScriptは、型宣言をアノテーションによって実施可能な言語です。JavaScriptに変換することが前提となっており、TypeScriptそのものは処理系を持っていません(Denoなど直接実行可能な処理系もあります)。
JavaScriptそのものが静的型付ではないため、TypeScriptが提供するのはあくまでアノテーションにすぎませんが、緩やかな型定義によって、より大規模なアプリケーション開発を容易にしてくれます。
AbortController
Promiseなどの非同期処理を中断するための処理を記述できる組み込み型オブジェクトです。
JSアプリケーションにおいては、外部APIをHTTPで実行することが多いため、中止や再実行のためにはAbortControllerの概念が必要不可欠です。
終わりに
これらに習熟できれば、JavaScriptの基本的な実装は可能かと思います。
より実践的なコーディングのためには、プロトタイプ指向やクラス表現と設計に関する知識、TypeScript、NodeJSといった周辺エコシステムの知識が欠かせません。ぜひ習得してください。