はじめに
JavaScriptはウェブページにインタラクティブな要素を追加するために使用され、現代のウェブ開発において中心的な役割を果たしています。このような地位を獲得するまでには長い歴史と多くの改善や変化がありました。
この記事では、JavaScript初学者の方に向け、JavaScriptの歴史とECMAScriptとの関係について触れ、ECMAScript2015(ES6)で導入された機能について解説します。
ES6ではJavaScriptでの開発体験を向上させるために多くの機能が追加されました。しかし、ES5以前の記述で解説された記事は現在(2024年1月)でも目にすることがあります。古い手法を用いたJavaScriptコードは冗長な場合があり、可読性や保守性にも問題を孕んでいる可能性があります。
これらを学ぶことは、JavaScript初学者の方がモダンなJavaScript開発を行う上での基礎を身に付けるための助けになるかと思います。
この記事の対象者
- JavaScript初学者の方
- ウェブ開発に興味のある方
- ECMAScriptが分からない方
この記事で学べること
- JavaScriptの歴史
- ECMAScriptとは何か
- ES6で追加された機能
JavaScriptの歴史
JavaScriptは、1995年にNetscape Communicationsのブレンダン・アイク氏によって開発されました。開発期間はわずか10日間で、JavaScriptの最初のバージョンがNetscape Navigatorというブラウザで実装されました。
この新しいスクリプト言語は、当初「LiveScript」と名付けられましたが、後に当時注目されていたプログラミング言語であるJavaの人気にあやかる形で「JavaScript」と名前が変更されました。
アイク氏はJavaScriptをできるだけシンプルな言語にしたいと考え、「Self」というプログラミング言語のシンプルさを重要視する思想に影響を受けて設計を行いました。Selfがプロトタイプベースの言語であったため、JavaScriptもクラスベースではなくプロトタイプベースの言語になったと言われています。
JavaScriptの登場によりウェブサイトを通じてユーザーとの対話が可能になり、これはウェブ業界においての大きな革新をもたらしました。当時ブラウザとしてNetscape Nvigatorとライバル関係にあったMicrosoft Internet ExplorerにもJavaScriptの実装を試みる動きがありましたが、Netscape CommunicationsからJavaScriptのライセンスを得られず、結果として「JScript」というJavaScriptに似せた独自のスクリプト言語を開発しInternet Explorer3(IE3)に実装しました。
それぞれのブラウザに独自のスクリプト言語のアップデートが進められますが、これはウェブ開発者にとって大きな負担となっていきます。JavaScriptとJScriptの互換性は低く、ウェブ開発者は異なるブラウザで動作するコードを書くために苦労することとなります。
このような経緯から、通信技術の標準化を行っている機関である「ECMA International」によってJavaScriptは「ECMAScript」として標準化が行われました。これにより、異なるブラウザでも共通の言語仕様に基づいてJavaScriptが動作するようになり、ウェブ開発の互換性問題が大幅に改善されました。
その後、ECMAScriptは定期的に更新され、新しい機能が追加されていきます。 2009年にリリースされたECMAScript第5版(ES5)まではJavaScriptの進化は比較的緩やかなものでしたが、ECMAScript2015(ES6)ではletやconstによる変数宣言やクラス構文、アロー関数など多くの新機能を追加し、これによってウェブ開発におけるJavaScriptプログラムの実装方法に大きな影響を与えました。ES6以降、ECMAScriptは毎年更新されており、機能の修正や改善が行われています。
ES6で導入された機能
ES6は、JavaScriptにおける最も重要なアップデートの一つとして広く認識されています。ES6では、開発者がより読みやすいコードを書くための新しい構文と、プログラムの構造を改善するための新しい概念が導入されました。
ここでは、ES6で追加されたいくつかの主要な機能について、基本的な使い方と特徴について簡単に解説していきます。ここで解説する機能は以下です。
- letとconst
- アロー関数
- クラス構文
- テンプレート文字列
- 分割代入
- スプレッド演算子とレスト演算子
- 引数のデフォルト値
- モジュール機能
- Promiseオブジェクト
letとconst
ES5以前は変数を宣言するためにvarキーワードが使われていましたが、varによる変数の宣言は以下のような問題がありました。
- 変数のホイスティングが行われる
- 変数名の重複を許容する
- ブロックスコープを持たない
varによる変数はホイスティングが行われ、宣言より前でも変数を呼び出すことができ、その値はundefinedになります。
console.log(x);
var x = 5;
// 結果:undefined
また、varによる変数は変数名の重複を許容し、後に宣言した方の値が適用されます。
var x = 1;
var x = 2;
console.log(x);
// 結果:2
varによる変数は関数スコープのみを持ち、if文やfor文の{}内のような範囲ではスコープを持ちません。よって、ブロックスコープの外から変数にアクセスでき、意図せず値が変更される可能性がありました。
if (true) {
var x = 10;
}
x = 20;
console.log(x);
// 結果:20
このような挙動から、時に予測しにくい動作を引き起こすことがあり、JavaScriptで大規模な開発を行うには工夫が必要でした。例えば、以下はスコープを模倣する例です。
var namespace = namespace || {};
namespace.x = 10;
console.log(namespace.x);
// 結果:10
console.log(x);
// 結果:Uncaught ReferenceError: x is not defined
ES6によりletとconstによる変数宣言ができるようになり、上記の問題は改善されました。これらで宣言された変数はホイスティングされません。また、変数名の重複を許容せず、{}の範囲内でスコープを持ちます。
また、constで変数を宣言すると再代入も不可能になるため、letに比べてより安全な変数を定義することができます。constは「値を変更できない」という特徴により、変数ではなく「定数」と呼ばれることがあります(実際は配列内やオブジェクト内の値は変更できるので、当サイトではconstも変数として表現しています)。
letとconstの追加により、現在ではvarによる変数宣言は推奨されていません。letとconstを適切に使用することでコードの保守性の向上が期待されています。
アロー関数
ES6で追加されたアロー関数は従来の関数宣言よりも簡潔に記述することができます。アロー関数による関数の定義はfunctionキーワードを用いず、「=>」を使用します。また、アロー関数は基本的に変数に関数式を代入して定義します。
// アロー関数
const add = (a, b) => {
return a + b;
}
// 関数宣言
function add(a, b) {
return a + b;
}
アロー関数の呼び出しは通常の関数宣言と同じように行うことができます。
// アロー関数の呼び出し
console.log(add(3, 5));
// 結果:8
処理が一行の場合は省略記法で記述することでコードの記述量を大きく削減します。
const add = (a, b) => a + b;
アロー関数は従来の関数から複雑な機能を取り除くというアプローチで設計されています。また、アロー関数は従来の関数宣言と比べてthisの扱いが大きく異なります。呼び出し元によって動的に変化する複雑なthisを静的に扱えるようにすることで、より予測しやすいコードになる効果があります。
アロー関数については以下の記事で詳しく解説しているので、興味のある方は参考にしてください。
クラス構文
オブジェクト指向プログラミング言語で開発を行う際、同じ構造や役割を持つオブジェクトを複数の場所で定義することは保守性を低下させる要因となり、JavaScriptではコンストラクタ関数としてオブジェクトのひな形を定義することが一般的な手法でした。
JavaScriptはシンプルさを主軸に開発されたプロトタイプベースの言語です。しかし、コンストラクタ関数を用いたプロトタイプベースの構文は直感的ではない場合もあり、他のプログラミング言語でクラス構文に慣れている開発者にとって、これはJavaScriptでの開発においてのストレスの要因でした。
ES6では、クラスベースのオブジェクト指向プログラミングに慣れた開発者をサポートするためにクラス構文での記述が可能になり、開発者は使用する状況や好みに応じて選択できるようになりました。
// クラスの例
class CarClass {
// CarClassのコンストラクタ
constructor(make, model) {
this.make = make;
this.model = model;
}
// CarClassのメソッド
start() {
console.log('エンジンを始動しました。');
}
}
// CarClassのインスタンスの生成
const carClass = new CarClass('TOYOTA', 'PRIUS');
// プロトタイプの例
function CarPrototype(make, model) {
// CarPrototypeのコンストラクタ
this.make = make;
this.model = model;
}
// CarPrototypeのメソッド
CarPrototype.prototype.start = function() {
console.log('エンジンを始動しました。');
};
// CarPrototypeのインスタンスの生成
const carPrototype = new CarPrototype('TOYOTA', 'PRIUS');
コンストラクタ関数は通常の関数と同じなのでnewキーワードを使用しなくても実行でき、これは意図しない形でコンストラクタ関数が使われる可能性があることを意味します。クラス構文はより目的が明確であり、関数として実行されることがないので保守性の向上が期待できます。
ReactやNext.jsといったJavaScriptフレームワークを利用する際はクラス構文が頻繁に利用されており、現在ではコンストラクタ関数よりもクラス構文が主流となっています。クラス構文については、クラス構文を理解するための「基本編」と「継承編」、クラス構文を有効活用するための「ポリモーフィズム編」と「Setter/Getter編」に分けて記事を書いてます。JavaScriptでのクラス構文について学びたい方は参考にしていただけると幸いです。
テンプレートリテラル
ES5以前では文字列内に変数などを含めたい場合、「+」演算子を用いた文字列結合を使用しました。ES6ではテンプレートリテラルという機能により、${}内に変数名や式を記述することで自然で可読性の高い文字列を定義することができます。冗長な結合やエスケープの手間を省くことができるのでとても便利な機能です。
通常の文字列を定義するときはシングルクォートやダブルクォートを使用しますが、テンプレート文字列はバッククォート「`」を使用します。例として、変数の埋め込みは以下のように行います。
const name = 'Taro';
const age = 30;
// 変数の埋め込み
const greeting = `Hello, ${name}! You are ${age} years old.`;
${}内には式を記述することができます。
const a = 5;
const b = 10;
// 式の埋め込み
const result = `The sum of ${a} and ${b} is ${a + b}.`;
また、通常の文字列では改行を含める場合「\n」が必要になりますが、テンプレートリテラル内には改行を含めることができ、複数行の文字列を簡単に定義できます。
// 複数行文字列
const message =
`This is
amulti-line
string.`;
テンプレートリテラルを使用することで、文字列の実装がより直感的になります。冗長な文字列結合やエスケープの手間を省くことができ、メッセージやHTMLテンプレートなどの構築が容易になります。
テンプレートリテラルについては以下の記事で詳しく解説しているので、興味のある方は参考にしてください。
分割代入
分割代入は、配列やオブジェクトから部分的に値を取り出して複数の変数に代入する機能です。ES6で追加され、分割代入を使用することで可動性の向上や効率的なコーディングを行うことができます。以下は配列で分割代入を使用する例です。
// 配列の分割代入
const numbers = [1, 2, 3, 4, 5];
const [x, y, ...rest] = numbers;
console.log(x); // 1
console.log(y); // 2
console.log(rest); // [3, 4, 5]
const []内の値は、元となる配列のインデックスに対応します。上記では、変数xにはnumbersの一番目の要素の値、変数yには二番目の値が代入されていることがわかります。「…rest」については後に解説します。
続いて、以下はオブジェクトでの分割代入の例です。
// オブジェクトの分割代入
const person = {
name: 'Taro',
age: 20,
city: 'Tokyo'
};
const { name, ...rest } = person;
console.log(name); // Taro
console.log(rest); // { age: 20, city: 'Wonderland' }
分割代入は構造化されたデータから必要な値を抽出して変数に代入でき、冗長なコードを削減する効果があります。外部APIから受け取るデータを扱う場合や、オブジェクト等から必要な部分だけ利用する場合などで活用できます。
分割代入については以下の記事で詳しく解説しています。
スプレッド構文
ES6で追加されたスプレッド構文は、配列やオブジェクトを展開して個々の要素やプロパティに分解するための構文です。スプレッド構文により配列やオブジェクトを柔軟に扱うことができ、主に以下のような用途で使用されます。
- 配列の展開
- 関数の可変長引数
- 新しい配列やオブジェクトの作成
また、スプレッド構文は「…」という記述で表現しますが、これは使用上の役割として「スプレッド演算子」と「レスト演算子」に分けられます。
スプレッド演算子
スプレッド演算子は、配列やオブジェクトを展開し、新しい配列やオブジェクトを作成するために使用されます。例えば、ES5以前では配列を複製する際にconcat()メソッドを使用する必要がありました。
const numArray = [1, 2, 3];
// numArrayを複製
const newArray = numArray.concat();
console.log(newArray);
// 結果:[1, 2, 3]
スプレッド演算子を使用すると以下のようになります。
const numArray = [1, 2, 3];
// numArrayを複製
const newArray = [...numArray];
console.log(newArray);
// 結果:[1, 2, 3]
スプレッド演算子は配列の複製だけでなく、配列と配列を結合することができます。
const numArray1 = [1, 2, 3];
const numArray2 = [4, 5, 6];
const newArray = [...numArray1, ...numArray2];
console.log(newArray);
// 結果:[1, 2, 3, 4, 5, 6]
スプレッド演算子を利用することで、コードの簡潔さや可読性を向上させる効果があります。注意点もありますが、配列やオブジェクトの操作において便利な機能です。スプレッド演算子については以下の記事で詳しく解説しています。
レスト演算子
レスト演算子は、複数の値を配列としてまとめる役割を持ちます。前述した分割代入や、関数の可変長引数を表現する際に使用することができます。
まずは分割代入においてのレスト演算子について説明します。分割代入のサンプルコードにあった「…rest」がレスト演算子です。配列の例では以下のサンプルコードを挙げました。
// 配列の分割代入
const numbers = [1, 2, 3, 4, 5];
const [x, y, ...rest] = numbers;
console.log(x); // 1
console.log(y); // 2
console.log(rest); // [3, 4, 5]
上記のように、「…」を変数名の先頭に使用することで、元となるnumbersから一番目と二番目の要素を除いた残りの部分を新たな配列としてまとめることができます。
続いて、可変長引数においてのレスト演算子について見てみましょう。可変長引数の関数を定義する際、ES5以前ではargumentsオブジェクトを使用する必要がありました。
function sum() {
const rest = Array.from(arguments);
let result = 0;
rest.forEach(function(value) {
result += value;
});
return result;
}
console.log(sum(1, 2, 3, 4, 5));
// 結果:15
argumentsオブジェクトは配列に似ていますが異なるオブジェクトです。なのでforEach()メソッドを使用できません。argumentsオブジェクトを利用する際は、上記のように「Array.from(arguments)」で配列に変換するか他の手段を選ぶ必要があり、冗長な表現になることが多いです。
レスト演算子を関数の引数に使用した場合、その引数は配列となります。以下が例です。
function sum(...rest) {
let result = 0;
rest.forEach(function(value) {
result += value;
});
return result;
}
console.log(sum(1, 2, 3, 4, 5));
// 結果:15
上記の例のとおり、引数で受け取ったrestは配列なのでforEach()メソッドを使うことができます。argumentsオブジェクトを使用するよりも記述が少なく可読性が高くなります。ちなみに、上記の処理はアロー関数を使用するとより短く記述することができます。
const sum = (...rest) => {
let result = 0;
rest.forEach(value => result += value);
return result;
}
スプレッド演算子が「展開」という役割を持つことに対し、レスト演算子は「配列にまとめる」という役割を持っています。これらをまとめて「スプレッド構文」と表現されます。レスト演算子については以下の記事で詳しく解説しています。
引数のデフォルト値
関数を実行する際、引数が無い場合にデフォルト値を設定したいケースがあります。ES5以前では、関数にデフォルト値を設定したい場合は三項演算子やif文による条件分岐でundefindの確認を行う必要がありました。
// ES5でのデフォルト値の設定
function greet(name, message) {
name = typeof name !== 'undefined' ? name : 'Guest';
message = typeof message !== 'undefined' ? message : 'Hello';
console.log(message + ', ' + name + '!');
}
greet();
//結果:Hello, Guest!
greet('Taro', 'Hi there');
// 結果:Hi there, Taro!
この方法は冗長で読みにくく、引数が多い場合にデフォルト値を設定する必要がある場合ではより煩雑になります。ES6では引数にデフォルト値を設定する機能が追加され、簡潔にデフォルト値を設定することができるようになりました。
// ES6でのデフォルト値の設定
function greet(name = 'Guest', message = 'Hello') {
console.log(`${message}, ${name}!`);
}
greet();
//結果:Hello, Guest!
greet('Taro', 'Hi there');
// 結果:Hi there, Taro!
上記の二つの例を比べると、ES6で追加されたデフォルト値の設定方法のほうが可読性が高く、直感的に理解しやすいコードになっていることが分かるかと思います。
モジュール機能
モジュールとは、コードを理論的な単位にまとめて再利用性を高めるための仕組みです。大規模な開発ではコードを効果的に管理する必要があり、モジュールという単位で関連する機能やデータをまとめてコードの構造化を行うことで保守性の向上が期待されます。
ES5以前では、モジュールとしてコードの構造化を効果的に行う機能はありませんでした。また、前述のとおり変数宣言はvarでしか行うことができなかったため、なんらかの対策を行わないとグローバルスコープに変数が散在することになります。
ES6ではモジュール機能が導入され、この機能によりファイルを分割してコードを構造化し管理することが容易になりました。モジュールには変数や関数、クラスなどを含めることができ、他のファイルからそれらを使用することができます。
モジュール機能はファイルへのアクセス権限の都合によりサーバー上でないと使用できないため、試すにはローカル環境を構築する必要があります。ここではローカルサーバーが動作していることを前提にしてモジュール機能の使い方について簡単に説明します。
モジュールを使用する場合、script要素のtype属性にmoduleと記述します。
<head>
…
<script src="js/main.js" type="module"></script>
<script src="js/math.js" type="module"></script>
</head>
exportキーワードを使用し、他のファイルで読み込ませる変数や関数、クラスなどを定義します。
// モジュールの定義
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
importキーワードを使用して使用したいモジュールの機能を指定し、続けてfromキーワードを挟んでモジュールファイルのパスを指定します。
// モジュールの利用
// main.js
import { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(8, 3)); // 5
モジュール機能を利用することで、必要なコードを必要な場所でのみ扱えるようになります。これにより、大規模なプロジェクトや多くのJavaScriptライブラリを使用するケースにおいて保守性の向上が期待されます。
Promiseオブジェクト
Promiseオブジェクトについて解説する前に、簡単に「非同期処理」について簡単に触れておきます。
ウェブ開発では、複数の外部サーバーと通信を行ってインタラクティブにデータの取得や送信、APIの呼び出しなどを行って機能を実装することがあります。このような場合、通信状況や外部サーバー、クライアント側の端末のパフォーマンスなどの影響により処理に掛かる時間が変わります。
外部サーバー等とのやりとりをJavaScriptで実装するには、処理の順番や例外処理を制御する必要があり、そのための手法として非同期処理を実装することが一般的です。非同期処理はJavaScript初学者の方にとって複雑な概念ですが、JavaScriptでの開発においてとても重要な役割を持ちます。
ES5以前で非同期処理を実装する際、関数内で関数を呼び出し、更にその関数内では別の関数を呼び出すというコールバック関数のネスト(コールバック地獄)によって実現していました。しかし、これは可読性の低いコードになる問題がありました。
以下はシンプルな例なのでコールバック地獄というほどでもありませんが、可読性の低さはなんとなく伝わるかと思います。
// コールバック関数1
function step1(callback) {
setTimeout(function() {
const success = Math.random() > 0.2;
if (success) {
console.log("ステップ1 完了");
callback(null);
} else {
const error = "ステップ1 失敗";
callback(error);
}
}, 1000);
}
// コールバック関数2
function step2(callback) {
setTimeout(function() {
const success = Math.random() > 0.2;
if (success) {
console.log("ステップ2 完了");
callback(null);
} else {
const error = "ステップ2 失敗";
callback(error);
}
}, 1000);
}
// コールバック関数3
function step3(callback) {
setTimeout(function() {
const success = Math.random() > 0.2;
if (success) {
console.log("ステップ3 完了");
callback(null);
} else {
const error = "ステップ3 失敗";
callback(error);
}
}, 1000);
}
// 非同期処理の実行
step1(function(error1) {
if (error1) {
console.error("処理中断:", error1);
return;
}
step2(function(error2) {
if (error2) {
console.error("処理中断:", error2);
return;
}
step3(function(error3) {
if (error3) {
console.error("処理中断:", error3);
return;
}
console.log("処理終了");
});
});
});
console.log("処理開始");
非同期処理の実行箇所を見てのとおり、制御したい処理が複数ある場合はコールバック関数のネストが発生し、可読性を損なう可能性があります。
Promiseの基本
Promiseは非同期処理の状態を管理するためのオブジェクトであり、Promiseが追加されたことにより非同期処理の実装を比較的簡潔に記述できるようになりました。Promiseは3つの状態を持ちます。
- Pending:インスタンス作成時の状態
- Fulfilled:処理が成功した時の状態
- Rejected:例外が発生した時の状態
先ほどのコールバック地獄の例をPromiseで表現すると以下のようになります。
function step1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.2;
if (success) {
resolve("ステップ1 完了");
} else {
const error = new Error("ステップ1 失敗");
reject(error);
}
}, 1000);
});
}
// step2とstep3はstep1とほぼ同じ処理なので省略します
// 非同期処理の実行
step1()
.then((resolve) => {
console.log(resolve);
return step2();
})
.then((resolve) => {
console.log(resolve);
return step3();
})
.then((resolve) => {
console.log(resolve);
console.log("処理終了");
})
.catch((error) => {
console.error(error.message);
});
console.log("処理開始");
Promiseの基本的な使い方について簡単に解説します。まず、非同期処理を行う関数の戻り値をPromiseインスタンスにします。以下の部分です。
function step1() {
return new Promise((resolve, reject) => {
// 関数の処理
});
}
処理の内容はPromiseインスタンスの引数にコールバック関数として記述します。また、コールバック関数の引数ではresolve, rejectを受け取っています。これで各関数の処理をPromiseで制御できるようになります。
Promiseの状態の変化についても触れておきます。resolveとrejectを処理の部分で実行するとPromiseの状態が変化し、一度変化した状態はもう一度変化することはありません。
if (success) {
// Promiseの状態が「 Fulfilled 」になります
resolve("ステップ1 完了");
} else {
const error = new Error("ステップ1 失敗");
// Promiseの状態が「 Rejected 」になります
reject(error);
}
resolve()とreject()に引数を渡しておくと、実行時にその値を取り出すことができます。サンプルコードは引数を記述していますが、引数は必須ではありません。
非同期処理の実行はthen()メソッドを使用します。
// 非同期処理の実行
step1()
.then((resolve) => {
console.log(resolve);
return step2();
})
.then((resolve) => {
console.log(resolve);
return step3();
})
.then((resolve) => {
console.log(resolve);
console.log("処理終了");
})
.catch((error) => {
console.error(error.message);
});
than()メソッドを繋げることで関数の実行順を制御することができます。Promiseが「 Fulfilled 」であれば次のthen()メソッド内の処理を実行し、「 Rejected 」であればcatch()メソッド内の処理を実行します。
Promiseでできることは他にもありますが、Promiseの解説は最低限にとどめておきます。というのも、非同期処理についてはECMAScript2017で「Async Funcrion」という構文が追加され、非同期処理の関数をより簡潔な記述で実装できるようになりました。
非同期処理を実装する方法としてAsync Functionに関する記事を書く予定です。Async Functionを使用するにはPromiseの理解が必要なので、Promiseについてはその記事でもう少し解説します。
最後に
この記事では、JavaScript初学者の方に向け、JavaScriptの歴史とECMAScriptとの関係について触れ、ECMAScript2015(ES6)で導入された主な機能について解説しました。
この記事でご紹介した機能は、ReactやNext.jsのようなJavaScriptライブラリやフレームワークなどを使用して開発を行う場合に頻繁に使われています。ウェブサイト制作にとどまらず、ウェブアプリケーション開発にも興味を持っている方であれば積極的に使ってみることをオススメします。
また、ECMAScriptはES6以降毎年アップデートをしています。興味がある方は、どのような機能追加や改善が行われているかについて調べてみると良いでしょう。
この記事はES6で追加された主要な機能について網羅性を意識して書きました。なので、各機能の詳細な解説はしていません。複雑な概念であるクラス構文やモジュール、Promiseなどは個別で解説記事をご用意しますので、より深く理解したい方はそちらを参考にしていただけると幸いです。
