26. 評価と環境 — 変数の表を外側へ辿り、関数呼び出しごとに新しい表を作る
()は を根から辿って値にします。数値リテラルは Number、文字列リテラルは Str、はから値を引く、BinaryExpression は左右を評価して + か - を適用する、という素直な再帰です。値は RuntimeValue(Undefined / Null / Number / Str / Function / Builtin / Object)で表します。
変数の置き場所が環境(Environment)です。名前から値への と、外側の環境への参照(outer)を持ちます。get は自分の表に無ければ外側へ辿る()、define は自分の表に作る(var / function / 仮引数)、assign は見つかった環境の値を書き換える。 §9 の Environment Record の、必要な部分だけです。グローバルの環境には最初から console(と、 があれば )が入れてあります。
関数呼び出し(call)は、Function が持つ (定義されたときの環境)を外側にして新しい環境を作り、仮引数に実引数を define し、本体を実行します。本体の中の var は新しい環境に入るので外に漏れず、外側の変数は outer を辿って読めます。return は Completion::Return として呼び出し元まで持ち上げ、無ければ Undefined です。関数はの先頭で先に define するので、宣言より前の行から呼べます()。
+ は ECMAScript の規則の縮小版で、どちらかが文字列なら連結、そうでなければ数の足し算です('a' + 1 + 2 は "a12")。宣言していない名前を読むと ReferenceError、関数でないものを呼ぶと TypeError を返します。前のステップ(step13)を実行すると、11 のプログラムの結果と console.log の出力、2 つのエラーが出ます。
本物のブラウザでは: はまず (インタプリタ)でを実行し、実行中に集めた型や形の情報()をもとに、よく呼ばれる関数を → → と段階的に機械語へコンパイルします(v8.dev/blog/maglev)。 は Interpreter → (インラインキャッシュ付き)→ WarpMonkey の順です。この回の Environment の連鎖は V8 では Context と ScopeInfo(src/objects/contexts.h、scope-info.h)、SpiderMonkey では EnvironmentObject と Scope にあたります。メモリは V8 の が(nursery を Scavenger で、全体を Mark-Compact で)に回収し、並列・増分・並行で停止時間を削っています(v8.dev/blog/trash-talk)。 で済ませたこの実装には GC がありません。
開発のどこに活かすか: 「スコープチェーン」「クロージャは定義時の環境を持つ」「関数宣言の巻き上げ」は JavaScript の面接の定番ですが、環境を自分で HashMap と outer で作ると、暗記ではなく仕組みとして残ります。テンプレートエンジンの変数展開、設定ファイルの変数参照、ワークフローの式評価のような「つきの評価器」は、この Environment と eval の形をそのまま流用できます。
点線の付いた語はタップすると説明が出ます。用語集を開く →
① var base = 10; function f(a) { return base + a; } f(5) の環境
- ・f の中で var inner = 1 としても、入るのは呼び出しの環境。外から inner を読むと ReferenceError
- ・呼び出しが終わると環境は捨てられる(Rc の参照が 0 になる)
「名前をどこで探すか」が outer の鎖で決まるので、同じ名前でも中と外で別の変数になります。
サンプルコード(フレームワーク環境が必要なため表示のみ)
// src/js/runtime.rs の環境・ブロックの実行・関数呼び出し
impl Environment {
pub fn new(outer: Option<EnvRef>) -> EnvRef {
Rc::new(RefCell::new(Environment {
values: HashMap::new(),
outer,
}))
}
/// 名前を探す。無ければ外側へ(スコープチェーン)
pub fn get(&self, name: &str) -> Option<RuntimeValue> {
match self.values.get(name) {
Some(v) => Some(v.clone()),
None => self.outer.as_ref().and_then(|o| o.borrow().get(name)),
}
}
/// この環境に新しく定義する(var / function / 仮引数)
pub fn define(&mut self, name: &str, value: RuntimeValue) {
self.values.insert(name.to_string(), value);
}
/// 既存の変数に代入する。見つかった環境で書き換える。どこにも無ければ false
pub fn assign(&mut self, name: &str, value: RuntimeValue) -> bool {
if self.values.contains_key(name) {
self.values.insert(name.to_string(), value);
return true;
}
match &self.outer {
Some(o) => o.borrow_mut().assign(name, value),
None => false,
}
}
}
fn exec_block(&mut self, statements: &[Node], env: &EnvRef) -> Result<Completion, JsError> {
// 関数宣言は巻き上げ(hoisting)される: ブロックの先頭で先に定義する
for s in statements {
if let Node::FunctionDeclaration { name, params, body } = s {
env.borrow_mut().define(
name,
RuntimeValue::Function {
params: params.clone(),
body: body.clone(),
closure: env.clone(),
},
);
}
}
let mut last = RuntimeValue::Undefined;
for s in statements {
match self.exec_statement(s, env)? {
// 関数宣言と変数宣言は「値を持たない文」。最後の値を上書きしない(ECMAScript の empty completion)
Completion::Normal(_) if matches!(s, Node::FunctionDeclaration { .. } | Node::VariableDeclaration { .. }) => {}
Completion::Normal(v) => last = v,
ret @ Completion::Return(_) => return Ok(ret),
}
}
Ok(Completion::Normal(last))
}
fn call(&mut self, f: RuntimeValue, args: Vec<RuntimeValue>) -> Result<RuntimeValue, JsError> {
match f {
RuntimeValue::Function { params, body, closure } => {
// 呼び出しごとに新しい環境。外側は「定義されたときの環境」(字句スコープ)
let local = Environment::new(Some(closure));
for (i, p) in params.iter().enumerate() {
let v = args.get(i).cloned().unwrap_or(RuntimeValue::Undefined);
local.borrow_mut().define(p, v);
}
match self.exec_block(&body, &local)? {
Completion::Return(v) => Ok(v),
Completion::Normal(_) => Ok(RuntimeValue::Undefined), // return が無ければ undefined
}
}
RuntimeValue::Builtin(Builtin::ConsoleLog) => {
let line = args.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(" ");
self.console_output.push(line);
Ok(RuntimeValue::Undefined)
}
RuntimeValue::Builtin(Builtin::GetElementById) => {
let id = args.first().map(|a| a.to_string()).unwrap_or_default();
let doc = self
.document
.clone()
.ok_or_else(|| JsError::ReferenceError("document is not defined".into()))?;
Ok(match DomNode::get_element_by_id(&doc, &id) {
Some(n) => RuntimeValue::Object(JsObject::Element(n)),
None => RuntimeValue::Null, // 見つからなければ null(仕様どおり)
})
}
other => Err(JsError::TypeError(format!("{other} is not a function"))),
}
}
手元で動かす(配布物)
- ステップ 13 の 1 ファイル版(step13_js_runtime.rs)実行すると 11 のプログラムの結果が出る。テスト 36 本
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗