30. You Don’t Know JS: Scope & Closures Chapter 2: Lexical Scope
【所要時間】2時間48分 (2018年7月29日)
【概要】
レキシカルスコープについて
【要約・学んだこと】
scopeがどのように動作するかは主に2種類ある。ほとんどのメジャー言語ではLexical Scop(語彙の)、もう一方はDynamic Scope(動的な)と呼ばれる。
Lex-time
標準言語compilerの最初ので伝統的なフェーズは、lexing(tokenizing)とよばれる。lexingプロセスはソースコードの文字を調査し、状態を持つ解析の結果として、tokenに意味を割り当てる。
lexical scopeはlexing timeに定義されるscopeだ。つまり、lexical scopeは書き込み時に、variableやscopeのblockがどこで書かれるかに基づく。よって、lexerがコードを処理するまで、石にされる。
Note:
lexical scopeを欺くいくつかの方法があり、それによって、lexerが通り過ぎた後に修正する方法があるが、これらは眉をひそめる。lexical scopeは実際にはlexicalのみ、つまり完全な制作時間として扱うのがベストプラクティスと考えられる。
- global scopeを包括し、中には foo というidentifierだけがある。
- foo scopeを包括し、中には a , bar, b 3つのidentifierがある。
- bar scopeを包括し、中には c というidentifierだけがある。
scope bubbleはscopeのブロックがどこで書かれたか、どれが他のblockの内部に囲まれているかで定義される。ここではそれぞれのfunctionが1つの新しいscope bubbleを作る想定で説明する。
bar のbubbleは foo のbubbleに完全に含まれている。なぜならfunction bar を定義するために選択したからだ。
Look-ups
これらのscope bubbleの構造と相対配置は、identifierを見つけるために必要な全ての場所を、エンジンに完全に説明する。
Engineはconsole.log(..) statementを実行し、参照された3つのa, b, c variableを探しに行く。
第一に最も内側のscope babbleである、 bar(..) functionのスコープから始める。そこでは a が見つからないので、1つ上のlevelに上がり、次にちかいscope bubbleに出て行き、 foo(..)scopeに行く。そこで a を見つけ、 a を使う。 b も同様。だが、c は bar (..)の中では見つからない。
c はbar(..) と foo(..) の中両方で、 console.log(..) statementは bar(..) でそれを見つけ、利用する。foo(..)の中ではない。
scope look-upは一度マッチすると止まる。同じ名前のidentifierはいくつかのnested scopeで指定することができる。これは shadowing (中のidentifierは外のidentifierをshadowする。)と呼ばれる。shadowing は関係なく、scope look-upはいつも最も内側のscopeから実行が始められ、最初のマッチが見つかるまで、外側/上側に進む働きをし、止まる。
Note: Global variablesはglobal object(ブラウザの window など)の自動propertyでもあるので、global variableをlexical nameによって直接ではなく参照することができるが、そのかわりglobal objectのproperty参照として間接的に使用される。
このテクニックはglobal variableにアクセスすることができる。global variableにアクセスするには、shadowされているのでできない。しかし、non-global shadowed variablesにはアクセスできない。
Cheating Lexical
lexical scopeは記述時にどこでfunction が宣言されたかで決まってしまうが、cheating lexical scopeで修正することができる。しかし、cheating lexical scopeはパフォーマンス低下を引き起こす。
eval
JSの eval(..) functionは、 argumentとしてstringをとり、まるでプログラムの該当箇所が実際に書かれたかのようなstringのコンテンツとして扱う。つまり、書かれたコードの中にコードを作り、まるで記述時には存在したかのように作られたコードを実行する。
eval(..)の後のコードのラインが実行され、engineは問題の前のコードがdynamicに解釈され、lexical scope環境が変更されたことを気にしない。
function foo(str, a) {
eval( str ); // cheating!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
string “var b = 3;” は、eval(..)が呼んだポイントで、そこにあったコードとして扱われる。なぜならそのコードは新しいvariable b を宣言させ、すでに存在しているlexical scope foo(..)の中に実際に b を作るからだ。前述の通り、実際にはこのコードは 外側のscope(global scope)で宣言されたb のshadowであるfoo(..)の中に variable b を作る。
console.log(..)で呼び出しが発生する時、foo(..)scopeに a と b 両方を発見し、外側のbは決して見つけない。そのため、”1 2”の代わりに”1 3”をプリントする。
Note:
このサンプルでは簡単にするために、渡したcodeのstringは固定文字だった。しかし、プログラムロジックに基づいて、文字を一緒に追加することで、プログラム的に簡単に作成できた。 eval(..)は通常動的に作られたコードで実行され、string literalから本質的にstatic コードをdynamicに評価すると、コードを直接書くだけで、実際の利益は何もない。
デフォルトでは、コードのstringはeval(..) は1つ以上の宣言(variableかfunction)を実行し、このアクションはeval(..)に存在するlexical scopeを修正する。技術的には、eval(..)は、さまざまなトリックを通じて、gloobal scopeのコンテキストで実行される代わりに間接的に呼び出される。しかし、どちらの場合でも、eval(..)は実行時に記述時のlexical scopeを修正することができる。
Note:
strict-modeプログラムでeval(..)を使う時、自身のlexical scopeを扱う。それは宣言がeval()の中で囲われたscopeに実際には修正されないという意味だ。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
JSにはeval(..)と似た効果を与えるfacilityに、 setTimeout(..) と setInterval(..) がある。それらはそれぞれの最初のargumentにたいしてstringを取ることができ、内容は動的に作られるfunctionコードとして評価される。これは古い挙動なので、使わないべき。
new Function(..) function constructorは、同様に最後のargumentでコードのstringを取り、動的に作られたfunctionに変換する。(最初のargumentがあれば、新しいfunctionのparameterの名前がつけられる。)このfunction-constructor syntaxは、eval(..)より少し安全だが、それでもコードでは避けるべきだ。
with
withにはいくつかの説明の仕方があるが、ここではどうやってlexical scopeに相互作用し、影響を与えるかという観点から説明する。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- Oops, leaked global!
この例には 01 と 02 というobjectが作られた。1つは a propertyを持ち、もう1つは持たない。 foo(..) functionはargumentとしてobject reference obj をわたし、 with (obj) {..} を参照し呼ぶ。 with block の中は、 variable a へ通常のlexical referenceに見えるものを作成し、 LHS referenceは、 value 2 をそれ代入する。
01 を渡すとき、 a = 2 assignment は property 01.a を見つけ、 value 2 に代入し、その後の console.log(01.a) statementに反映される。
しかし、 02 を渡す時、a propertyを持たないので、このようなproperty は作られず、 02.a はundefined のままである。
しかし、a = 2 assignmentで、global variableが作られたという特殊な副作用に気が付いている。
with statementは 0以上のpropertyを持つobjectをとり、そのobjectを完全に別のlexical scopeとして取り扱う。そのため、objectのpropertyはそのscope内のlexicalに定義されたidentifierとして扱われる。
Note:
with blockはobjectをlexical scopeのように扱うが、通常with block内のvar 宣言はwith blockにscopeされない。しかし、代わりに function scopeを含む。
eval(..) functionがその中に1つ以上の宣言があるコードのstringをとるなら、存在するlexical scopeを修正することができるが、with statementは実際に渡したobjectから、全く新しいlexical scopeを作り出す。
このように 01 を渡した時に with statementに宣言されたscopeは01であり、 scopeは01.a propertyに対応するidentifierが含まれていた。しかし、02 をscopeとして使ったとき、その中にそのようなidentifierを持たなかった。そして、LHS identifier look-up の通常ルールが起こった。
2のscope、 foo(..) scope、global scopeいずれも a identifierが見つけられないので、a = 2 が実行されたとき、自動global が作られる。(non-strict modeのため)
w実行時に、withがobjectとそのpropertyをidentifiersでscopeに変えるのを見るのは、奇妙だ。しかし、これが最もはっきりとした説明である。
Note:
eval(..)とwithという悪いアイデアを使うのは、strict modeに影響される。 with は様々な間接的だったり、安全でないeval形式を許可しない。
Performance
eval(..)もwithも、実行中に修正か新しいlexical scopeをつくるかで、記述の時間をlexical scopeに定義する。
JS engineはcompilation phaseの間、たくさんのパフォーマンスの最適化が行われる。これらの中には、コードを本質的に静的に解析することができき、全てのvariableとfunction宣言の場所を事前に決め、実行中のidentifierを解決する労力を少なくする。
しかし、Engineがコード内でeval(..) か withを見つけると、基本的にはidentifierの位置に関する全ての認識が無効であると想定しなければならない。なぜなら、lexical scopeを変更するためにeval(..)に渡す可能性のあるコードを、lexingの時に正確に知ることができないからだ。
つまり、悲観的に言えば、eval(..)かwithが存在するなら、それらほとんどの最適化は無意味なので、最適化を実行しません。
コードはeval(..)かwithをコードのどこかに含むことによって、遅くなる傾向にある。これら悲観的な予想の副作用を制限しようとしているengineがどれだけ賢くても、最適化がなければ、コードは遅く実行される。
Review (TL;DR)
Lexical scopeはscopeがfunctionが宣言されている場所の記述時間の決定によって定義されるという意味。compileのlexing phaseでは、どこでどうやって全てのidentifiersが宣言されてるかを知ることが基本的にでき、それゆえ実行中にどうやってlook-upされるのか予想できる。
JSの2つのメカニズムeval(..)とwithはlexical scopeをチートすることができる。eval(..)は存在しているlexical scopeを,その中のコードが持つ1つ以上の宣言のstringを評価することによって、修正することができる。
withは、object参照をscopeとして扱い、scopeされたidentifierとしてobjectのpropertyを扱うことで、新しいlexical scopeを完全に作り出す。
欠点は、scope look-upに関するcompile時の最適化を実行するengineの能力を負かすことだ。なぜならeigineはそのような最適化が無効になると悲観的に推測しなければならない。コードはそれを使った特徴の結果として、ゆっくり実行される。これらを使わないべきだ。
【わからなかったこと】
特になし
【感想】
使わない方がいいシリーズが結構あるので、その辺は知っておかないと、よくわからずに使ってしまい、後々困りそう。