- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- 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 は矢印(束縛)を固定する。実体の中身は別の約束の対象
- ・不変の束縛は 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回実行して同じ出力です。
const push: [ 1, 2 ]const reassign: TypeError Assignment to constant variable.freeze (sloppy): {"inner":{"n":2},"n":1} true falsefreeze (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)
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行の抜粋)。
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 は実行時にコピーしたファイル名)。
# 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 はどうなっていますか。
JavaScript の const xs = [1]
束縛だけが不変です。xs.push(2) は通って [ 1, 2 ] になり、xs = [9] だけが TypeError になりました。中身を守りたいなら別の仕組みが要り、Object.freeze も1段しか凍らせません。
const push: [ 1, 2 ]const reassign: TypeError Assignment to constant variable.
Rust の let v = vec![1]
mut が無いと、所有する Vec の中身を変える push もコンパイル時に拒否されました。中身を変えたければ mut を付けるか、RefCell で検査を実行時へ移します。
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 は 定数にできる種類を絞る(スライスは不可)。
- 不変な値は複製せず共有できる。「不変ならコピーが増える」は言い過ぎ。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
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] += [...]` の例外の原因に挙げている。
出典(一次ソース)
- ECMA-262 Draft / September 20, 2026 — §8.2.3 IsConstantDeclaration・§9.1.1.1.3 CreateImmutableBinding・§9.1.1.1.5 SetMutableBinding ↗
- Python 3.14.7 Programming FAQ(a_tuple[i] += の節、y = x の節、tuple() の節) ↗
- Python 3.14.7 Tutorial 4.9.1 Default Argument Values ↗
- The Rust Programming Language — 3.1 Variables and Mutability ↗
- Rust std 1.98.1 — std::cell(interior mutability) ↗
- Rust std 1.98.1 — std::cell::RefCell(borrow_mut・try_borrow_mut) ↗
- The Go Programming Language Specification(Language version go1.27 (May 26, 2026))— Constants ↗