こんにちは。。 新卒エンジニアの”クリス”と申します!
今日は、変数をしっかり保護するために実際に取れるいくつかのステップについて話したいと思います。
const 宣言は、現代のJavaScriptにおいて多くの用途があります。
一部の開発者は、変数に再代入する必要がない限り、デフォルトで全ての変数を const を使って宣言することを好みます。再代入が必要な場合にのみ、let を使用します。
しかし、オブジェクト(配列や関数も含む)を const で宣言した変数に代入した場合でも、そのオブジェクト自体はミュータブル(変更可能)であることを理解しておくことが重要です。const 宣言は、変数識別子(変数名)の再代入を防ぐだけです。
私たちは、const キーワードが変数をイミュータブル(不変)にすると理解しがちですが、場合によってはそうではありません。
例えば、以下のコードを見てください。
const s = [5, 6, 7]; s = [1, 2, 3]; console.log(s);
これはエラーになることに、誰もが同意するでしょう。なぜなら、s 変数に再代入しようとしているからです
しかし、const の s を変更せずに(イミュータブルに保つことを目的として)このコードを書き直すと、次のようにできます。
const s = [5, 6, 7]; //s = [1, 2, 3]; s[2] = 45; console.log(s);
この場合、コードはエラーもなく安全に実行されます。
ご覧のとおり、[5, 6, 7] というオブジェクト自体を変更することは可能であり、変数 s は変更後の配列 [5, 6, 45] を指し続けます。すべての配列と同様に、s の配列要素はミュータブル(変更可能)です。しかし、const を使って宣言されているため、代入演算子を使って s に別の配列を再代入することはできません。
上記の例から分かるように、const 宣言だけではデータの変更(ミューテーション)を完全に防ぐことはできません。データを変更されないように保護するために、JavaScript では Object.freeze という関数が用意されており、これを使うことでデータのミューテーションを防ぐことができます。
オブジェクトを変更しようとすると、その操作は拒否され、スクリプトが strict モードで実行されている場合はエラーが発生します。
この Object.freeze 関数を使えば、たとえ let キーワードで宣言されたオブジェクトであっても、ミューテーションから保護することができます。
では、以下の例を見てみましょう。
そもそもstrict modeとは?
JavaScriptの strict mode(厳格モード) は、バグを見つけやすくし、安全で予測可能なコードを書くための特別な実行モードです。
• 利点:バグを早期に発見できる、セキュリティリスクが減る、パフォーマンスが向上する可能性がある
"use strict"; x = 10; // ❌ エラー: 変数が宣言されていない
let obj = { name:"Chris@iimon", review:"Awesome" }; Object.freeze(obj); obj.review = "bad"; obj.newProp = "Test"; console.log(obj);
obj.review や obj.newProp への代入は、エディタが strict モードで動作している場合、エラーとなります。そしてコンソールには、{ name: "Chris@iimon", review: "Awesome" } という値が表示されます。
function freezeObj() { const MATH_CONSTANTS = { PI: 3.14 }; Object.freeze(MATH_CONSTANTS) try { MATH_CONSTANTS.PI = 99; } catch(ex) { console.log(ex); } return MATH_CONSTANTS.PI; } const PI = freezeObj();
Object.freeze() 欠点
Object.freeze() は浅い凍結(shallow freeze)であるため、ネストされたオブジェクトの中身は凍結されません。
たとえば:
const user = { name: "Taro", settings: { theme: "dark" } }; Object.freeze(user); user.settings.theme = "light"; // これは変更できてしまう!
これを防ぐには、再帰的にすべてのネストを凍結する deepFreeze 関数が必要です。
function deepFreeze(obj) { Object.getOwnPropertyNames(obj).forEach(function(name) { const prop = obj[name]; if (typeof prop === 'object' && prop !== null) { deepFreeze(prop); } }); return Object.freeze(obj); } const profile = { name: "Chris", prefs: { lang: "ja" } }; deepFreeze(profile); profile.prefs.lang = "en"; // 無視される/strict modeならエラー
2. TypeScriptでの保護 (readonly など)
もしTypeScriptを使っていれば、型レベルでも変数のミューテーションを防げます。
type User = { readonly id: number; name: string; }; const user: User = { id: 1, name: "Chris" }; user.id = 2; // エラー!readonlyプロパティには再代入できません
まとめ
たとえば let キーワードで変数を宣言すると、いつでも再代入できることは分かっていても、実際にはその「弱い」let をイミュータブル(不変)にすることが可能です。
私たちは常に const キーワードを使えば、変数を安全に不変にできると信じてきましたが、実際にはロジックを加えることで、その変数の中身(形)を変更できてしまうことも見てきました。
しかし、Object.freeze とdeepFreeze 関数とエディタが strict モードで動作していることを組み合わせれば、const キーワードで宣言した変数をミューテーションから確実に保護することができます。
変数の保護にはさまざまなレベルがあります:
const:再代入は防げるが、内容の変更は可能Object.freeze():オブジェクトの内容の変更を防げる(ただし浅い)deepFreeze関数:再帰的に完全凍結 (recursive freeze)- TypeScriptの
readonly:型レベルでミューテーションを防ぐ strict mode:不正な代入を検出できる
ここまで読んでいただきありがとうございました!✨
弊社ではエンジニアを募集しております。
この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!
下記リンクよりご応募お待ちしております!
iimon採用サイト / Wantedly / Green