You Don’t Know JS: Scope & Closures Chapter 1: What is Scope?

学習日: 180911
所要時間: 6時間

Compiler Theory

一般的に、JavaScriptはインタプリタ言語に分類されるが実際はコンパイル言語である。

伝統的なコンパイラ言語プロセスでは、プログラムは実行前に3つのステップを踏んでいる。これをcompilationと呼ぶ。

1.Tokenizing/Lexing 字句解析
文字列をトークンと呼ばれる塊に分割する。

例えば、var a = 2;var, a, =, 2;. の5つに分割される。
空白は意味があろうとなかろうと、トークンには残らない。

tokenizingとlexingの違いはステートレスかステートフルかの違い。

2. Parsing 構文解析
トークンのストリームに(array)を取り出し、AST (Abstract Syntax Tree)と呼ばれるツリーに変換する。

var a = 2; というツリーはVariableDeclarationと呼ばれるトップレベルのノードから始まり、Identifier (valueはa) を呼び、NumericLiteral (valueは2)と呼ばれる子をもつAssignmentExpression という別の子ノードがある。

3. Code-Generation コード生成
ASTの過程で、実行可能なコードに落とし込んでいく。
プラットフォームや言語で変わる。

JSエンジンは、他のコンパイル言語の様な三段階の構造よりも複雑。

buildステップの前にcompilationが行われないので、他の高級言語と同じくコンパイルにたくさんの時間をとられない。

コンパイル時間は、最適化されて(JITsのような 遅延コンパイル、ホットリコンパイルなど)、大抵の場合わずか数マイクロ秒、またはそれ以下になる。

JavaScriptのスニペットは実行前に(通常は直前に)コンパイルする必要がある。

JS compilerはvar a = 2;をすぐに取り、実行する準備をする。

Understanding Scope

The Cast

1.エンジン
開始から最後までコンパイルと実行までを担当する。

2.コンパイラ
エンジンの友人の1人。
構文解析、コード生成などの面倒な作業を行う。

3.スコープ: 
エンジンの別の友人。
宣言されたすべての識別子(変数)を収集し管理する。
現在実行中のコードにアクセスできるか厳しいルールを課す。

Back & Forth

var a = 2;をエンジンは2つの文と解釈する。
・エンジンが実行中に扱うもの
・コンパイラがコンパイル中に扱うもの

コンパイラは次のように処理する。

1.var aに遭遇すると、コンパイラはスコープに、特定のスコープコレクションに変数aがすでに存在しているかどうかを尋ねる。
もしそうであればコンパイラはこの宣言を無視して移動する。
それ以外の場合は、コンパイラはスコープにaという新しい変数を宣言するように指示する。

2.コンパイラはその時、エンジンが後で実行するためのコードを作り出す。エンジンは、まず、a = 2 を扱うために現在のスコープで変数aがアクセス可能かをスコープに尋ねる。もし問題なければその変数を使用する。違った場合はエンジンは他の所を見に行く。

最終的に変数を検出すると、値2が割り当てられる。
それ以外はエラーと伝える。

要約すると、
2つのアクションは、変数に代入されるために行われる。
まず、コンパイラが変数を宣言する。
そして、2つめに実行する時、
エンジンはスコープの中で変数をみつけて、代入する。

Compiler Speak

エンジンがstep(2)のために生成したコードを実行するとき、
すでに宣言されているかどうか変数をルックアップする必要がある。
ルックアップのタイプによって、出力にも影響を与える。

エンジンは変数aにLHSルックアップを実行していると言われる。
他のルックアップタイプをRHSという。

両者の違いは
Lはlefthand side
Rはright side

正確にはRHSは左側ではないという意味。

もう少し丁寧に言えば、RHSは代わりにソースを検索するという意味。RHSは値を取得するという意味。

console.log( a );

a を参照するのはRHSリファレンス。
a に代入されているものがないから。

代わりに、aの値を取得して調べる。
そうするとその値がconsole.logに渡される

a = 2

aを参照するのはLHSリファレンス。
= 2を振り分けるためのターゲットとしての変数を探したいから。

NOTE:LHSとRHSの意味は、「代入の左辺/右辺」は文字通り「代入演算子の左辺/右辺」を意味するとは限らない。

Engine/Scope Conversation

function foo(a) {
console.log( a ); // 2
}

foo( 2 );

これは

Engine: スコープ、fooのためのRHSリファレンスを持っているけど、聞いたことある?
Scope: もちろん持ってるよ。コンパイラがついさっき宣言していたやつだね。どうぞ。
Engine: ありがとう。foo を実行中だ。
Engine: スコープ、a のためのLHSリファレンス持ってるけど、聞いたことある?
Scope: もちろん持ってるよ。コンパイラーが、ついさっきフォーマルなfoo へのパラメータとして宣言していたやつだね。どうぞ。
Engine: いつも助かるよ。ありがとう。 2a にアサインする時間だ。
Engine: スコープ、たびたびごめん。cosnoleを探す為にRHSルックアップが必要なんだけど、知ってる?
Scope: 問題ない。エンジン。いつもやってることだ。コンソールを持ってるよ。彼は組み込まれてるんだ。
Engine: 完璧だ。logをルックアップする。関数だね。
Engine: スコープ、スコープ。aへのRHSレファレンスで僕のことを助けてくれないか?覚えていると思うんだけど、ダブルチェックをしたいんだ。
Scope: わかった。エンジン。同じだね。変更なしだ。
Engine: いいね。 a の値をlog(..)送ろう。

Nested Scope

スコープ内で変数が見つからなかった場合、
見つかるまで最も外側のスコープに到達するまで探し続ける。

function foo(a) {
console.log( a + b );
}

var b = 2;

foo( 2 ); // 4

bのRHS参照は関数fooでは解決できないが、
それを囲むスコープで解決できる。
(この場合はグローバルスコープ)

Errors

LHSまたはRHSを呼ぶのはなぜ重要か。
変数が宣言されてない場合に異なる振る舞いをするため。

RHSルックアップが変数を見つけられなかった場合、
エンジンによってReferenceErrorになる。

逆に、そのエンジンが、LHSルックアップを実行していてグローバルスコープに行き着いた場合、新たな変数を作ってエンジンに返す。

strict-modeを使用すると同じ振る舞いになる。

ReferenceError 
スコープに関連している失敗のエラー

TypeError 
スコープは成功しているが不可能な動作を試みてる場合のエラー。

質問事項

質問したため特になし。

所感

コードについての説明だとconsoleで書いて値を変えてみたりして、理解できることもあるけど、コンピューターサイエンスの分野を文章のみで淡々と説明されると自分の英語力だと意味が汲み取れないことが多くて難しかった。

焦らずに1つずつ覚えていこう。

メモ

Q.
LHS/RHSリファレンスの役割がわからない。

A.

まず LHS/RHS を軽く整理しておくと、
https://en.wikipedia.org/wiki/Sides_of_an_equation
いわゆる数学でいう等式の = を挟んで左が left-hand side、右が right-hand side ですね。
つまり、
```var a = 1;```
みたいなものがあると、 a が left-hand side で 1 が right-hand side になります。
この記事では、上の代入は LHS reference と言っていますが、多くのプログラミング言語では、= は数学の等式と異なり、代入を意味しています。そのため、 = の左には、計算式ではなく必ず代入を行う定数や変数が存在しなければなりません。そして、右には数値、文字列、変数、定数など何が来ても大丈夫ですが変数と定数の場合は、必ず既に定義されている変数と定数でなければなりません(存在しなければ必ずエラーになる)。
つまり、LHS が出現した時点で、必ず代入操作を行うことになりますが、この流れが
1. a (left-hand side) が存在するかどうかを調べる(LHS look-up)
2. そして変数/定数が存在しなければ作成して、変数/定数が存在していれば上書き、あるいはエラーを出す(LHS reference)
という形で分けられています。
RHS reference については、
```console.log(a);```
というものがあったとき、
1. a が存在するかどうかを調べる(RHS look-up)
2. a が存在していれば値を結び付ける(RHS reference)
3. a が存在していなければエラーを出す(reference エラー)
という形で分けられています。
でもまぁ、実際のところ重要なのは、代入のときは存在していない変数/定数でも良くて、それを利用するときは必ず存在していなければならない(でなければ必ずエラーが出てプログラムが止まる)というルールなので、それを覚えておけば大丈夫です。

各言語の型の特徴をまとめてみたhttps://qiita.com/kaitaku_san/items/148d1491596a58b9c97d

コンパイラの構造を解説 | Shinta’s Site http://www.gadgety.net/shin/tips/unix/compiler.html

JavaScriptを最適化コンパイルするために避けるべきこと https://qiita.com/rana_kualu/items/9211a776da8a60e3e4c2

Like what you read? Give Shuma Mizuno a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.