Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 2 / 12·約 8 分·第25期 どの言語にもある仕組み

const が固定するのは名前 — 中身まで凍るかは言語が決める

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

同じ書き方でも、何が起きるかはその言語が約束した分しか決まっていない。今回は「変えられない」と宣言したとき、実際に何が禁じられるのかを言語ごとに並べます。

まず結論

JavaScript の const が固定するのは「名前がどの実体を指すか」(束縛)だけで、指した先の配列やオブジェクトの中身は変えられます。

「変えられない」と書く方法はどの言語にもありますが、鍵がかかる範囲は言語ごとに違います。束縛だけ、容器の差し先だけ、束縛と中身の両方、そもそも作れる値の種類を絞る——どれなのかは、その言語の仕様が約束した分しか決まっていません。

? 読む前に予測

JavaScript で `const xs = [1];` と宣言したあと `xs.push(2);` を実行すると、どうなるでしょうか(Node v24.21.0)。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

名前と実体のあいだに鍵をかける

変数には2つの層があります。名前がどの実体を指しているか(束縛)と、その実体の中身です。xs.push(2) は中身を変え、xs = [9] は束縛を付け替えます。

JavaScript の const がどちらに鍵をかけるかは、ECMA-262 に手順として書かれています。宣言が const なら(LetOrConst : const Return true.)、束縛は次の呼び出しで作られます。

If IsConstantDeclaration of decl is true, then Perform ! envRecord.CreateImmutableBinding(name, true).

作られるのは immutable binding、つまり不変の「束縛」です。配列やオブジェクトを不変にする手順は、ここにはありません。

const xs = [1] で鍵がかかる場所
名前 xsconst が固定するのはここから先の矢印
束縛(付け替えると TypeError)
配列の実体 [1]中身を変えられるかは実体の型が決める
中身の変更(const は関与しない)
push で [1, 2]同じ実体のまま伸びる
  • ・const は矢印(束縛)を固定する。実体の中身は別の約束の対象
  • ・不変の束縛は strict binding として作られ、モードに関係なく再代入は TypeError

2つ目の引数 true にも意味があります。CreateImmutableBinding は If strict is true, the new binding is marked as a strict binding. とし、代入の手順 SetMutableBinding は手順の中で If the binding for name in envRecord is a strict binding, set strict to true. と読み替えます。const への再代入は、書いた場所が strict モードでない(sloppy な)コードでも TypeError になるということです。

具体実物

Node で束縛と中身を別々に触る

Node v24.21.0(Docker・aarch64)で、const の配列に push し、sloppy なコードから再代入し、さらに Object.freeze を試しました。2回実行して同じ出力です。

$ node /w/a.js (v24.21.0, aarch64)コマンド出力
const push: [ 1, 2 ]
const reassign: TypeError Assignment to constant variable.
freeze (sloppy): {"inner":{"n":2},"n":1} true false
freeze (strict): TypeError
[exit 0]

1行目、push は通りました。2行目、束縛の付け替えだけが TypeError です。Assignment to constant variable. は V8 の文言で、ECMA-262 には出てきません。仕様が決めているのは TypeError という種類までです。

3行目は Object.freeze({ inner: { n: 1 }, n: 1 }) した f に、sloppy なコードで f.n = 2; f.inner.n = 2; を実行した結果です。f.n は例外も出ずに 1 のまま、入れ子の f.inner.n は 2 に変わりました。Object.isFrozen(f.inner) は false です。4行目、"use strict" で始まる関数の中で同じ代入をすると TypeError でした。freeze は1段だけで、失敗が見えるかどうかもモードで変わる——ここまでがこの版での観測です(freeze の挙動の仕様の逐語は今回は確かめていません)。

✓ ここまでの確認

strict モードでない(sloppy な)コードの中で、const で宣言した名前に再代入するとどうなりますか。

抽象原理

「変えられない」の範囲は言語ごとに違う

同じ「変えられない」でも、他の言語は別の場所に鍵をかけます。

Python のタプルが禁じるのは、要素がどの実体を指すかの変更です。要素が指すリストの中身ではありません。そして += は、FAQ が augmented assignment operators are assignment operators と書くとおり、最後に代入を行います。リストの += は中身をその場で伸ばしてから t[0] へ代入し直そうとし、その代入でタプルに拒否されます。

Rust の let は、mut を付けない限り束縛を不変にします(Although variables are immutable by default, you can make them mutable by adding mut)。JavaScript と違い、所有している Vec の中身を変える操作も拒否されます。例外は std::cell で、‘interior mutability’ (mutable via &T) と呼び、Shareable mutable containers exist to permit mutability in a controlled manner, even in the presence of aliasing. と説明します。RefCell は mut なしでも中身を変えられる代わりに、借用の重なりを実行時に検査します(try_borrow_mut は returning an error if the value is currently borrowed)。

Go の const は中身を凍らせる仕組みではありません。仕様が定数として並べるのは boolean constants, rune constants, integer constants, floating-point constants, complex constants, and string constants で、スライスはこの列挙にありません。

具体実物

Python・Rust・Go で同じことを試す

Python 3.14.7(aarch64)で実行したコードと出力です。

t = ([1],)
t[0].append(2); print("tuple holds mutable:", t)
try:
    t[0] += [3]
except TypeError as e:
    print("TypeError:", e, "-> but t is now", t)
try: hash(t)
except TypeError as e: print("hash:", e)
def add(item, bucket=[]):
    bucket.append(item); return bucket
print(add(1), add(2))
s = "abc"; s2 = s; s += "d"; print("str:", s2, s)
$ python /w/a.py (Python 3.14.7, aarch64)コマンド出力
tuple holds mutable: ([1, 2],)
TypeError: 'tuple' object does not support item assignment -> but t is now ([1, 2, 3],)
hash: unhashable type: 'list'
[1, 2] [1, 2]
str: abc abcd
[exit 0]

2行目、例外が出たのに t は ([1, 2, 3],) です。3行目、リストを含むタプルは hash が取れません(エラーは中のリストについて出ています)。4行目は既定引数です。チュートリアルは The default value is evaluated only once. と警告します。bucket=[] の空リストは定義時に1回だけ作られ、2回の呼び出しが同じリストを返したので、両方とも [1, 2] と表示されました。

Rust 1.98.1 で、mut なしの Vec に push するとコンパイルが止まります(先頭3行の抜粋)。

$ rustc /w/main.rs -o /tmp/m (rustc 1.98.1, aarch64・抜粋)コマンド出力
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
--> /w/main.rs:4:5
|

同じ Vec を mut なしの RefCell::new(vec![1]) に入れると、borrow_mut() 経由の push は通って [1, 2] と表示され、borrow() を持ったまま try_borrow_mut() を呼ぶと borrow_mut while borrowed: true(エラーが返った)と表示されました。検査がコンパイル時から実行時へ移っています。

Go 1.27.1 では const xs = []int{1} がビルドで止まります(ログの m.go は実行時にコピーしたファイル名)。

$ go run main.go (go1.27.1, linux/arm64)コマンド出力
# command-line-arguments
./m.go:2:12: []int{…} (value of type []int) is not constant
[exit 1]
✓ ここまでの確認

Python 3.14.7 で `t = ([1, 2],)` に対して `t[0] += [3]` を実行すると TypeError が出ます。例外を捕まえたあとの t はどうなっていますか。

⇄ 2つを見比べる
具体例 A

JavaScript の const xs = [1]

束縛だけが不変です。xs.push(2) は通って [ 1, 2 ] になり、xs = [9] だけが TypeError になりました。中身を守りたいなら別の仕組みが要り、Object.freeze も1段しか凍らせません。

Node v24.21.0コマンド出力
const push: [ 1, 2 ]
const reassign: TypeError Assignment to constant variable.
具体例 B

Rust の let v = vec![1]

mut が無いと、所有する Vec の中身を変える push もコンパイル時に拒否されました。中身を変えたければ mut を付けるか、RefCell で検査を実行時へ移します。

rustc 1.98.1コマンド出力
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable

どちらも「変えない」と宣言しているのに、push の結果が逆になったのはなぜでしょうか。

それぞれのキーワードが、名前・実体・中身のどこまでを対象にしているかを比べてください。

この章のまとめ

  • JavaScript の const は 束縛を不変にする(CreateImmutableBinding)。push は通り、再代入は TypeError。
  • const の束縛は strict binding なので、sloppy なコードからの再代入も TypeError。文言 Assignment to constant variable. は V8 のもの。
  • Object.freeze は 1段だけ。sloppy では失敗が黙って無視され、strict では TypeError(Node v24.21.0 で観測)。
  • Python のタプルは要素の差し先を固定するだけ。t[0] += [3] は 例外を出しつつリストを伸ばす。既定引数は1回だけ評価される。
  • Rust の let は mut が無ければ 中身の変更まで拒否。RefCell は検査を実行時へ移す。Go の const は 定数にできる種類を絞る(スライスは不可)。
  • 不変な値は複製せず共有できる。「不変ならコピーが増える」は言い過ぎ。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Go 1.27.1 で `const xs = []int{1}` と書いてビルドすると、どうなりますか。

用語集(4語)
束縛(binding)
名前と、その名前が指す実体の結びつき。JavaScript の const は束縛を不変にするが、指した先の中身には触れない。
厳格束縛(strict binding)
ECMA-262 の用語。const の束縛はこれとして作られ、代入はコードのモードに関係なく strict として扱われて TypeError になる。
内部可変性(interior mutability)
Rust の std::cell の用語。共有参照(&T)越しに中身を変えられる性質。RefCell は重なった借用を実行時に検査する。
拡張代入
`+=` などの演算子。Python の FAQ は、これが代入演算子であることを `t[0] += [...]` の例外の原因に挙げている。