JavaScriptの定数
概要
はじめに
こんにちは、株式会社 Life Arc System の 開発担当の mzn です。
本記事では JavaScript の定数について説明させていただきます。
対象読者
本投稿は、以下のような方を対象として執筆しています。
- JavaScript の定数の使い方を知りたい方
- JavaScript の定数を使うメリットを知りたい方
環境
本投稿の内容は、以下の環境にて確認しています。
- 言語 … JavaScript (ES2015 以降)
- ブラウザ … Google Chrome, Firefox
定数の宣言
JavaScript では、const
キーワードで識別子を宣言することで、定数を利用することができます。
また、分割代入や、for..of
とfor...in
のような繰り返し構文で要素を取得する際にも、定数を宣言することができます。
// 基本的な定数宣言
const name = value;
// 分割代入で定数を宣言
const arr = [1, 2];
const [item1, item2] = arr;
// ループ構文での要素を定数として取得
for (const item of arr) {
// ...
}
定数は変数と同様に、任意の名前1の識別子を宣言し、データを格納しますが、以下の点が変数と異なります。
- 宣言と同時に初期化を行わなければいけない
- 初期化以降、代入やインクリメントなどのデータを直接変更するような操作を行うことができない
そのため、下記のようなコードはエラーとなります。
const num = 1;
// 定数に対してデータを直接変更するような操作を行っているためエラー
num = 5;
num += 2;
num++;
// 初期化が行われていないため、エラー
const notInitialied;
なお、後述しますが、プリミティブ2に分類されるデータの定数は宣言以降一切の変更ができません。 一方で、オブジェクト3に分類されるデータの定数については再代入を行うことができないだけで、格納されているデータに対して変更を加えることは可能です。
プリミティブの定数
プリミティブのデータは、プロパティやメソッドを持たず、データの内容を変更することはできません。
そのため、プリミティブのデータが格納されている変数の変更を行う手段が、インクリメントや代入のような、直接データを置き換える操作のみとなります。
なお、プリミティブのデータのように変更できないデータは、イミュータブルなデータと呼ばれます。
let num = 1;
num = 3;
num += 2;
num++;
定数に対して上記のような記載はできないため、プリミティブのデータが格納された定数は、宣言時の初期化以降一切変更することができません。
プリミティブのデータが格納された定数もイミュータブルです。
プリミティブの定数の活用
プリミティブのデータの定数は、変数と比較して制約が強く使える場面は少なくなります。 しかし、下記のような、出現以降変更を行う必要がないデータは、可読性の向上を期待できるため、定数を利用すると良いと考えます。
- 計算上の係数やループの繰り返し回数等、直に記述することによるマジックナンバーを削減し、定数の名前で用途を説明する
if
やswitch
文等による処理の切り替えのためのフラグや、ステータス管理用の列挙値- 処理の途中経過のデータ保持
また、同じ目的の出現以降変更を行う必要がないデータが複数個所で出現するような場合、定数を利用することで、出現箇所が定数宣言時の 1 箇所になります。 そのため、定数の内容を変えたくなった時の、変更箇所が限定され、修正漏れを削減可能です。
上記のような用途は変数でも対応できますが、宣言以降も処理のどこかで変更されている可能性があります。
変数が出現するスコープでどのような処理が行われているか把握していないと、変数に意図しないデータが格納されている場合があり、宣言場所以外も考慮しなければいけません。
一方で、プリミティブの定数は変更できないため、宣言場所を見ればどのようなデータが格納されているか確認可能です。
上手く定数を利用し、制約を与えることで、プログラミングする際の負担を軽減することができます。
プリミティブの定数を利用したソースコード例
下記ソースコード例では、プリミティブの定数を利用しないpagenate1
と、利用するpagenate2
で同等の処理を記述しています。pagenate2
では、定数を利用することで、マジックナンバーや計算経過の意味を識別子名で説明しています。
さらに、複数出てきたマジックナンバー20
を定数maxDisplayRowNumber
に統一し、後からの変更が必要な個所を削減しています。
/**
* プリミティブのデータの定数を利用しないでテーブルの表示を 20 行毎に分割し、
* 指定されたページに該当する20行の表示制御を行う。
* @param {number} displayPageNumber 表示したいページ
* @param {HTMLTableElement} tableElement 表示を制御したいテーブル
* @param {string} trDefaultDisplay 該当テーブルのtrの display プロパティ
*/
function pagenate1(displayPageNumber, tableElement, trDefaultDisplay = 'block') {
let tableRows = [...tableElement.querySelectorAll('tr:has(> td)')];
// 20 がどのような数値なのか、コメントが無ければ周りの処理から推測するしかない
if (tableRows.length / 20 < displayPageNumber) {
console.log('displayPageNumber に存在しないページが指定されました。');
return;
}
// テーブルの行を一旦全て不可視にする
for (let row of tableRows) {
row.style.display = 'none';
}
// 該当するページの行のみ可視状態とする
for (let row of tableRows.slice(displayPageNumber * 20, (displayPageNumber + 1) * 20)) {
row.style.display = trDefaultDisplay;
}
}
/**
* プリミティブのデータの定数を利用してテーブルの表示を 20 行毎に分割し、
* 指定されたページに該当する20行の表示制御を行う
* @param {number} displayPageNumber 表示したいページ
* @param {HTMLTableElement} tableElement 表示を制御したいテーブル
* @param {string} trDefaultDisplay 該当テーブルのtrの display プロパティ
*/
function pagenate2(displayPageNumber, tableElement, trDefaultDisplay = 'block') {
let tableRows = [...tableElement.querySelectorAll('tr:has(> td)')];
const maxDisplayRowNumber = 20;
const lastPageNumber = tableRows.length / maxDisplayRowNumber;
if (lastPageNumber < displayPageNumber) {
console.log('displayPageNumber に最大ページ数より大きな値が指定されました。');
return;
}
// テーブルの行を一旦全て不可視にする
for (let row of tableRows) {
row.style.display = 'none';
}
// 計算の経過を定数に保持
const start = displayPageNumber * maxDisplayRowNumber;
const end = (displayPageNumber + 1) * maxDisplayRowNumber;
// 該当するページの行のみ可視状態とする
for (let row of tableRows.slice(start, end)) {
row.style.display = trDefaultDisplay;
}
}
オブジェクトの定数
オブジェクトのデータはプロパティやメソッドを持つことができます。
定数は、インクリメントや代入のような、直接データを変更する操作以外は特に禁止していないため、以下の操作によるデータの変更が可能です。プロパティやメソッドを通した変更は可能です。
- プロパティの追加・削除・変更
- メソッドを通じたプロパティの変更
上記のように変更が可能なデータはミュータブルなデータと呼ばれます。
また、JavaScript の配列もオブジェクトであり、配列の添え字アクセスによる要素の変更も可能です。
const obj = {
prop1: 1,
prop2: 'foo',
setProp2: function(value) { this.prop2 = value; },
};
// プロパティの値変更
obj.prop1 = 3;
// メソッドを介したプロパティの変更
obj.setProp2('var');
// 動的なプロパティの追加
obj.prop3 = 3;
obj['prop4'] = { prop1: 1 };
const arr = [1];
// 配列の定数もメソッドによる要素の変更が可能
arr.push(2, 3, 4);
// 添え字アクセスによる要素の変更
arr[2] = 5;
// 配列も動的にプロパティを追加可能
arr.lastChangeIndex = 2;
オブジェクトの定数の活用
変数に対してはどのようなデータ型でも代入することが可能で、元々格納されていたデータを上書きできてしまいます。
プログラマのミス等によって、意図しない代入が行われ、別の型のデータで上書きされていた場合、以下のような問題が発生する危険があります。
- 未定義のプロパティやメソッドへアクセスし、エラーが発生してしまう
- 代入前のデータと類似したデータで上書きされた場合、見つけにくバグとなってしまう可能性がある
上記の問題は、いずれも変数に対して代入可能なために発生してしまうため、代入を禁止しているオブジェクトの定数を利用することで回避可能です。
オブジェクトの定数を利用したソースコード例
下記のソースコードの関数pagenate3
はプリミティブの定数を利用したソースコード例で記載した、関数pagenate2
内での宣言を定数で行うように変更したものです。
元々pagenate2
ではオブジェクトのデータが格納された変数に対して、再代入を行っていなかったため、宣言を全てconst
に変更しても全く同じ処理になります。
今後、ソースコードの修正が必要になっても、宣言部分を変更しない限り、意図しない代入で上書きされる危険がなくなっています。
/**
* 定数を利用してテーブルの表示を 20 行毎に分割し、
* 指定されたページに該当する20行の表示制御を行う
* @param {number} displayPageNumber 表示したいページ
* @param {HTMLTableElement} tableElement 表示を制御したいテーブル
* @param {string} trDefaultDisplay 該当テーブルのtrの display プロパティ
*/
function pagenate3(displayPageNumber, tableElement, trDefaultDisplay = 'block') {
const tableRows = [...tableElement.querySelectorAll('tr:has(> td)')];
const maxDisplayRowNumber = 20;
const lastPageNumber = tableRows.length / maxDisplayRowNumber;
if (lastPageNumber < displayPageNumber) {
console.log('displayPageNumber に最大ページ数より大きな値が指定されました。');
return;
}
// テーブルの行を一旦全て不可視にする
for (const row of tableRows) {
row.style.display = 'none';
}
// 計算の経過を定数に保持
const start = displayPageNumber * maxDisplayRowNumber;
const end = (displayPageNumber + 1) * maxDisplayRowNumber;
// 該当するページの行のみ可視状態とする
for (const row of tableRows.slice(start, end)) {
row.style.display = trDefaultDisplay;
}
}
イミュータブルなオブジェクト
JavaScript のオブジェクトは基本的にミュータブルなデータです。
しかし、読み取りするだけの用途のオブジェクトが出現した場合、そのままでは意図しない変更が行われてしまう可能性があり、ミュータブルなデータのままでは不都合です。
JavaScript には上記の問題に対応可能なObject.freeze()
4メソッドが存在しています。Object.freeze()
メソッドを適用したオブジェクトは、メソッド内の処理を含め、全てのプロパティの追加、削除、変更を行うことが出来なくなります。Object.freeze()
を適用することでオブジェクトはイミュータブルとなり、該当のオブジェクトが格納された定数は、プリミティブなデータを格納したものと同様に一切変更出来なくなります。
ただし、Object.freeze()
を適用したオブジェクトが、オブジェクトをプロパティとして持っていた場合は変更可能なままとなってしまいます。
ネストしたオブジェクト全てを不変とするには、全てのオブジェクトのプロパティも含めて、再帰的にObject.freeze()
を適用しなければいけません。
なお、セッターやArray.push()
のようにプロパティ等オブジェクトの変更を伴うメソッドは破壊的メソッド、ゲッターやObject.toString()
のようにオブジェクトの変更を伴わないメソッドは非破壊的メソッドと呼ばれます。
Object.feeze()
を適用後のオブジェクトから呼び出し可能なメソッドは、非破壊的メソッドのみです。
const obj1 = { prop: 5 };
// 15に変更される
obj1.prop = 15;
// これ以降obj1に対する変更を行うことはできない
Object.freeze(obj1);
// 反映されない or エラー
obj1.prop = 25;
console.log(obj1.prop); // 15
// freezeメソッドは引数に渡したオブジェクトをそのまま返すため、
// 初めからイミュータブルなオブジェクトの定数を宣言することも可能
const obj2 = Object.freeze([1, 2, 3, 4]);
// 反映されない or エラー
obj2.push(5);
console.log(obj2); // Array [1, 2, 3, 4]
const obj3 = Object.freeze({ innerObj: {} });
// プロパティに格納されているデータ自体にはfreezeの効力が及ばないため、
// 下記のような変更は出来てしまう
obj3.innerObj.prop = 1;
console.log(obj3.innerObj.prop); // 1
おわりに
定数を利用することで、バグを見つけやすくしたり、ソースコードの可読性を向上させることが可能です。
そのため、mzn は可能な限り変数ではなく、定数を利用するとよいのではないかと考え、JavaScript のソースコードを作成する際も意識しています。
実際に、ループカウンタや、数値の集計を行うような場合を除いて、ほとんどの変数は定数に置き換えられると感じています。
なお、記事を作成していたところ、オブジェクトのプロパティの変更の制限を行う制御ができないか気になりましたので、調べてみたいと思いました。
以上、JavaScript の定数について説明させていただきましたが、参考になりましたら幸いです。
識別子として利用できる文字には制限があります。
詳しくは mdn web docs の Identifier (識別子)を参照してください。 ↩︎プリミティブに属するデータの型は JavaScript の持つデータ型でも最も基本的なもので、数値型、文字列型、論理型などが含まれます。
また、プリミティブのデータはプロパティやメソッドを持ちません。なお、
'string'.includes('s')
のように、一部のプリミティブのデータはあたかもメソッドを持っているかのような記述が可能ですが、暗黙的にラッパーオブジェクトのデータを新たに生成したうえで、該当のメソッドを呼び出しているようです。プリミティブについて、詳しくはmdn web docs の Primitive (プリミティブ)及び、mdn web docs の JavaScript のデータ型とデータ構造を参照してください。 ↩︎
オブジェクトに属するデータは全て Object 型であり、プリミティブ以外のデータは全てオブジェクトに含まれます。
オブジェクトのデータはプロパティやメソッドを持つことができ、それらを介してデータの内容を変更することが可能です。オブジェクトについて詳しくはmdn web docs の JavaScript のデータ型とデータ構造を参照してください。 ↩︎
Object.freeze()
メソッドの詳細については、mdn web docs のObject.freeze()
を参照してください。 ↩︎