はじめに
前回の記事でJSのconst
、イミュータブル性について無知を晒した私です。
ありがたいことにコメントをいただきまして、それがどうも私が求めていることが書かれているっぽい。
大変丁寧に説明いただき、ありがとうございます。
問題
いただいたコメントがこちら
Primitive型
JavaScriptの世界でイミュータブル性を持つのはPrimitive型です。
追記
Primitive型とは、「言語によって設定されている、最初からあるデータ型」「定義にほかのデータ型を用いない独立したデータ型」のこと。ユーザーが自分で作り出すことはできない。
例:整数型(int)、浮動小数点型(float)、文字型(char)、真偽型(boolean)など
const number = 1; console.log(number.toString === Number.prototype.toString); // true number.toString = 2; console.log(number.toString === Number.prototype.toString); // true
Object型はイミュータブルではないので、イミュータブルを期待する場合はPrimitive型を使うことをお勧めします。
Object型
Object型でもプロパティの書き換えを禁止する事はできます。
const object = Object.freeze({}); object.foo = 1; console.log(object.hasOwnProperty('foo')); // false
ただし、
Object.freeze()
は直属のプロパティにのみ効果を発揮し、プロトタイプチェーン上のオブジェクトのプロパティ書き換えを禁止しません。
(※この性質はPrimitive型も同様です)const object2 = Object.freeze({}); Object.prototype.toString = 1; console.log(object2.toString); // 1
Object.freeze()
はプロパティアクセサで代入可能なプロパティ書き換えを禁止しますが、それ以外の方法でデータを保存している場合もデータの書き換えを禁止しません。const map = Object.freeze(new Map); map.set('foo', 1); console.log(map.get('foo')); // 1
罠にはまらない為には言語仕様を理解し、一つ一つの自分ルールに理由をつけていく必要があります。
……よし。一見してわかる内容ではないな。
ChatGPT先生に解説してもらおう。
解決方法
①Primitiveはイミュータブル
まずはこのコードから。
const number = 1; console.log(number.toString === Number.prototype.toString); // true number.toString = 2; console.log(number.toString === Number.prototype.toString); // true
Q. このコードは何を試みていますか?
A. このコードはJavaScriptで、数値オブジェクトの
toString
メソッドを変更しようとしています。しかし、この試みはうまくいっていません。コードを詳しく見てみましょう
const number = 1;
:これは数値リテラル1
を変数number
に割り当てています。console.log(number.toString === Number.prototype.toString);
:ここで、数値オブジェクトのtoString
メソッドがNumber
プロトタイプのtoString
メソッドと同じかどうかをチェックしています。これはtrue
を返します。なぜなら、number
はプリミティブな数値で、JavaScriptではプリミティブな数値にメソッドを呼び出すとき、一時的なNumber
オブジェクトに変換され、そのオブジェクトはNumber.prototype
を継承します。number.toString = 2;
:この行は、number
というプリミティブ値にtoString
プロパティを設定しようとしています。しかし、プリミティブ値にプロパティを設定することはできません。JavaScriptはこの操作を無視し、エラーは発生しませんが、何も変更されません。console.log(number.toString === Number.prototype.toString);
:再度、number.toString
がNumber.prototype.toString
と同じかどうかをチェックしています。前述の通り、number
へのtoString
プロパティの設定は無視されるため、これは依然としてtrue
を返します。要するに、このコードはプリミティブ値にメソッドを追加しようとしていますが、JavaScriptの動作によりこれは無視され、元の状態のままになります。
なるほど。メソッド同士の比較をしてるのね
4では、「変更可能なら」falseを返すはずなんだけど、「変更不可なので」trueを返すと。
しかも、「JavaScriptはこの操作を無視し、エラーは発生しませんが、何も変更されません。」 ってまじか。
const n = 0;
n.toString = 2;
console.log(n.toString === Number.prototype.toString);
ってオイ!エラー吐くじゃねえか!!
Uncaught TypeError TypeError: Cannot create property 'toString' on number '0'
at <anonymous> (c:main.mjs:72:12)
at run (internal/modules/esm/module_job:194:25)
--- await ---
at processTicksAndRejections (internal/process/task_queues:95:5)
--- await ---
at runMainESM (internal/modules/run_main:55:21)
at executeUserEntryPoint (internal/modules/run_main:78:5)
at <anonymous> (internal/main/run_main_module:23:47)
……まあいいや。むしろ、ちゃんとエラー吐くことを喜ぼう。
とにかく、これで
イミュータブルを期待する場合はPrimitive型を使うことをお勧めします。
という部分は理解できた。
追記
「chrome consoleならエラー吐かないよ~」とのコメントいただきました。なぬ。
くそざこナメクジの僕はChatGPTに聞くしかなく……
Chromeコンソール(ブラウザ環境)では、プリミティブ型の値にプロパティを追加しようとすると、その操作は無視されますがエラーは発生しません。このため、コード number.toString = 2; は無視され、number.toString は依然として Number.prototype.toString に等しいままです。
一方、Node.js環境(VSCodeでの実行)では、プリミティブ型の値にプロパティを追加しようとすると TypeError が発生します。このエラーは、Node.jsがより厳格なエラーチェックを行っているためです。そのため、number.toString = 2; の行でエラーが発生します。
僕の場合はNode.js環境だったためエラーチェックに引っかかったっぽいです。
あと、use strict
(厳格モード)を使用すると、chrome consoleでもエラー吐いてくれるっぽい。
②Object.freeze()
const object = Object.freeze({}); object.foo = 1; console.log(object.hasOwnProperty('foo')); // false
これはわかる。なるほど、求めていたものだ。
だけど、注意点が2つあるんだな。ふむふむ。
②-注意点①
const object2 = Object.freeze({}); Object.prototype.toString = 1; console.log(object2.toString); // 1
ただし、
Object.freeze()
は直属のプロパティにのみ効果を発揮し、プロトタイプチェーン上のオブジェクトのプロパティ書き換えを禁止しません。
(※この性質はPrimitive型も同様です)
直属のプロパティはわかるけど……プロトタイプチェーン……?
hoge.prototype.hoge
って、たまによく見るけど、そういえばよくわかってないなあ
Q. ”プロトタイプチェーン”と”直属のプロパティ”の違いを教えてください。
A. "プロトタイプチェーン"と"直属のプロパティ"は、JavaScriptにおけるオブジェクトの特性と継承の概念を理解する上で重要な要素です。それぞれの違いを説明します。(一部抜粋)
プロトタイプチェーン
- 定義: プロトタイプチェーンは、JavaScriptにおけるオブジェクト指向プログラミングの基本概念で、オブジェクトが他のオブジェクトからプロパティやメソッドを継承する方法を指します。
- 動作: オブジェクトに対してプロパティやメソッドの呼び出しが行われたとき、JavaScriptはまずそのオブジェクト自身のプロパティを検索します。もしプロパティが見つからなかった場合、JavaScriptはそのオブジェクトのプロトタイプ(親オブジェクト)に定義されているプロパティを検索し、それを繰り返します。この一連のプロトタイプを辿る過程がプロトタイプチェーンです。
- 例:
let animal = { eats: true }; let rabbit = Object.create(animal); console.log(rabbit.eats); // true(rabbitにはないが、プロトタイプチェーン上のanimalにある)
ほへー。なるほど。
「自分のプロパティを探す→親のプロパティを探す」
っていう挙動をすると。
で、object.freeze()
をしても、親のプロパティの変更までは阻止できないよってことか。
②-注意点②
const map = Object.freeze(new Map); map.set('foo', 1); console.log(map.get('foo')); // 1
Object.freeze()
はプロパティアクセサで代入可能なプロパティ書き換えを禁止しますが、それ以外の方法でデータを保存している場合もデータの書き換えを禁止しません。
つまり……
const setDate = Object.freeze(new Date());
setDate.setHours(20, 0, 0, 0);
const nowDate = new Date();
console.log(setDate === nowDate, setDate, nowDate);
// false Sat Jan 06 2024 20:00:00 GMT+0900 (日本標準時) Sat Jan 06 2024 07:08:47 GMT+0900 (日本標準時)
……そうなりますよね~
なるほど。
終わりに
まとめると、
- JSでイミュータブルを期待するなら、まずPrimitive型を推奨する。
- Object型は
object.freeze()
を用いればある程度のイミュータブルは可能。 - なんにしろ完全なイミュータブルは不可。
ですかね。
いやあ、コメントをいただかなければ半永久的に知らなかった概念を知ることができました。
ありがたいことに、サンプルコードまでいただきまして、おかげで手軽に試して、理解を深められました。
@think49 さん、本当にありがとうございました。
追記
-
Object.create(null)
でprototype
を葬ることができるよ! - そもそも、細かく制御したいならユーザー定義クラス使えば?
という内容です!イミュータブラーな皆様、ぜひご覧ください。