どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「グローバルオブジェクトを使うのは、最小限にとどめる」をお届けしました。
今回は、「ローカル宣言は、必ず宣言しよう」 をお届けします。
テキスト第2章「変数のスコープ」の項目9に対応しています。
今日のアジェンダ
- ローカル宣言は、必ず宣言しよう
- レキシカルスコープとは
- 結合とは
ローカル宣言は、必ず宣言しよう
この項目では、前回、ついでに考察してしまった 「ローカル領域で宣言をつけずに変数を定義すると、グローバル変数になってしまう」 ということが書かれています。
globalThisつかって、裏を取ってみましょう。
function myFunction2_9_01(a, i, j) {
a = [10];
i = 0;
j = 0;
temp = a[i];
a[i] = a[j];
a[j] = temp;
console.log(globalThis.temp); //10
}
必ず、変数宣言をしましょう。そして、GASのばあいは、再代入する予定がなければconst宣言でいきましょう。
引数は、関数内で(再度の)変数宣言はできませんので、ご注意ください。
function myFunction2_9_02(a, i, j) {
a = [10];
i = 0;
j = 0;
const temp = a[i];
a[i] = a[j];
a[j] = temp;
console.log(globalThis.temp); //undefined
}
スクリプトエディタの変数宣言チェックツール
テキストでは「lintツール」なるものを紹介していますが、ブラウザで動くJSLintやJSHintが有名です。
ソースコードを貼りつけて、「JSLint」をクリックしてみましょう。
ソースコード内の、宣言されていない変数 を指摘してくれます。
これは、まずグローバルオブジェクトをリスト化して、リストにも存在しない、ソースコード内でも宣言されてない変数の参照、あるいは代入を指摘しています。
GASのスクリプトエディタには、変数を右クリック(もしくはCtrl + F12)して「定義へ移動」すると、変数宣言を確認する機能があります。
プロジェクト全体 で、変数宣言がされていないことを教えてくれます。(もし、別のスクリプトファイルのグローバル領域に変数宣言していると、何も表示されません。移動もしません。)
自動チェックツールではありませんが、活用してみましょう。
レキシカルスコープとは
「変数のスコープ」の基礎として、おさえておきたい用語のひとつに、レキシカルスコープがあります。
レキシカルとは
lexical(レキシカル)は、「語彙、辞書の、辞書的な」という意味です。似たような単語にtoken(トークン)があります。
どちらも「字句」という意味をもち、プログラミングにおいて、文字列の最小単位を表します。
とくにレキシカルが用いられるのは、字句解析(lexer)という表現をするときです。
字句解析(lexer)の対義語は、構文解析(parser)です。HTML Parserや、JSON Parserとして、みなさんも馴染みがあるかもしれません。
字句解析と構文解析は、解析する対象が、字句か文かです。 出典:プログラミング言語 8 字句解析器(lexer)と構文解析器(parser) 東京大学情報理工学系研究科電子情報学専攻田浦研究室
プログラミングにおける字句解析とは、ソースコードを字句単位で解析することです。 出典:プログラミング言語 8 字句解析器(lexer)と構文解析器(parser) 東京大学情報理工学系研究科電子情報学専攻田浦研究室
プロジェクトが実行されるまで
ソースコードを字句解析するのは、あらゆるプログラミング言語は、以下の手順を追って実行されるからです。
- ソースコードを書く
- ソースコードを読み込む
- ソースコードの文法をチェックする
- (ソースコードが実行できるように準備する)
- ソースコードを実行する
2.の「ソースコードを読み込む」という工程で、字句解析が行われています。解釈(かいしゃく)する、ともよばれます。
ちなみに、4.がみなさんが大好きな、コンパイルです。GASでは、ユーザーがコンパイルを意識する必要はありません。
少し脱線してしまいましたが、もっとも大切なのは、ソースコードを実行するまえとあとで、評価が2種類ある ということです。
よく、実行するまえに評価することを「静的○○」とよび、実行したあとに評価することを「動的○○」と呼びます。
実行しなくても、ソースコードを見たまんまの字面(じづら)で評価できる、という静的○○と、実行してみて、どんな動きになるか確認してからじゃないと評価できないね、という動的○○です。
- ソースコードを実行するまえ・・・静的○○
- ソースコードを実行したあと・・・動的○○
静的スコープと動的スコープ
たとえば、ソースコードを評価する項目のひとつに、「スコープ」があります。
ソースコードを実行するまえにスコープが決まる仕組み を、静的スコープと呼びます。
実行したあとに、もし変数や関数名の衝突が発生したら動的にスコープを生成しよう 、という仕組みを動的スコープと呼びます。
スコープについて考察したことがあるので、興味がある方はこちらを参照ください。
現在では、ほとんどのプログラミング言語が、静的スコープを採用 しています。
静的スコープは、関数内で作られた変数のスコープが、関数のソースコードの字句上の範囲と一致します。
静的スコープの仕組みを、実際のコードで確認してみましょう。
静的スコープの仕組み
このようなコードを実行してみましょう。
おさらいですが、const宣言は、ブロックスコープを生成 します。
ログ出力された「変数num2」や「変数num1」は、宣言されたブロックスコープの外で呼び出されている ため、リファレンスエラーとなります。
function myFunction2_9_03() {
const num = 0;
{
const num1 = 1;
{
const num2 = 2;
};
console.log(num2); //ReferenceError: num2 is not defined
};
console.log(num1); //ReferenceError: num1 is not defined
}
関数が実行されるまえに、ソースコードを読み込んだときに、字句解析が行われて、変数のスコープが生成されています。
補足すると、このときグローバルオブジェクト(緑枠)とCallオブジェクト(青枠・赤枠)が生成されています。(詳細は割愛します)
エディタでは、各スコープをデバッガで確認できます。
次に、このようなコードを実行してみましょう。
関数内の、いちばん内側のブロックスコープ(赤枠)では、変数num1と変数numが呼び出されています。
function myFunction2_9_04() {
const num = 0;
{
const num1 = 1;
{
const num2 = 2;
console.log(num1); //1
console.log(num); //0
};
};
}
このとき、プログラムは、いちばん内側のブロックスコープ(赤枠)内に、変数num1と変数numが宣言されているか探します。
もし、自身が呼び出されているスコープ内に、宣言がなければ、ひとつ上のスコープに宣言があるか見に行きます。
このように、上のスコープを見にいく仕組みを、スコープチェーン と呼びます。
最後に、このようなコードを実行してみましょう。
いちばん内側のブロックスコープ(赤枠)の内容をそのまま、青枠のブロックスコープの外に出します。
さきほどまで、ログ出力できていた「変数num1」が、リファレンスエラーになってしまいました。
function myFunction2_9_05() {
const num = 0;
{
const num1 = 1;
// {
// const num2 = 2;
// console.log(num1); //1
// console.log(num); //0
// };
};
{
const num2 = 2;
console.log(num); //0
console.log(num1); //ReferenceError: num1 is not defined
};
}
「そんなの当たり前じゃないか!」と思う方が多いと思います。
しかし、この 「ブロックスコープがどこに書かれているかによって参照できる変数が変わる」 というのは、静的スコープの特徴です。
補足ですが、このように、「自身のスコープから、外側のスコープに参照が可能になること」を、外部スコープと呼びます。
まとめると、静的スコープにはこのような特徴があります。
- ソースコードを読み込んだときに、字句解析が行われて、変数のスコープが生成される
- 自身が書かれているブロックスコープに変数定義がなければ、上のスコープを見にいく(スコープチェーン)
- どこに書かれているかによって参照できる変数が変わる
これらの仕組みをES5から、Lexical Environment(字句環境) と呼び、仕様をまとめました。
静的スコープの仕組みは、字句解析を行うものなので、レキシカルスコープ とも呼ばれます。
結合とは
今日の1セクション目で登場した「宣言されていない変数」のことを、テキストでは 「結合されていない変数」 と表現しています。
テキストP31では「不必要な結合(coupling)」、P37では「結合(バインディング)」、P44では「結合(binding)と代入(assignment)」のように、たびたび「結合」という言葉が登場します。
とくに今回知識として必要な「結合」は、プログラミングにおける 識別子と変数(関数やオブジェクトを含む)の対応づけ です。
これは、「名前束縛」と呼ばれています。
以下のようなコードを実行してみましょう。
JavaScriptはレキシカルスコープなので、ソースコードは読み込まれた時点で、静的スコープを生成します。
関数printX内で、ログ出力されているxは、「変数」とも言えますが、正確には「識別子」です。
識別子xは、スコープチェーン通りに「const x = 10」のことである、と評価します。
また、関数run内で、「const x = 20」が登場しますが、引数をやり取りしているわけでもありませんし、次の行で関数printX()を呼び出したとしても、xは「const x = 10」のことである、という評価は変わりません。
おさらいですが、プログラムを実行する前から、静的スコープは決まっています。
このとき、ソースコードを評価して、「どの識別子がどの変数に対応になるのか」を決めるのが 名前束縛 です。名前解決と呼ばれたりすることもあります。
ESの仕様書では、「Identifier Bindings(識別子の結合)」 と呼ばれています。
詳細は割愛しますが、「環境レコード(Environment Records)」という 変数名の対応表のようなオブジェクト に情報が記述されます。
function myFunction2_11_08() {
const x = 10;
function printX() {
console.log(x); //10 → A.「x」とは、「const x = 10」である
}
function run() {
const x = 20;
printX(); //10 → Q.「x」とは、「const x = 20」ではないの?
}
run();
}
名前束縛は、「変数の参照を行うための対応付け」とも言えるでしょう。
決して、「変数の代入」ではありません。
まとめ
以上で、「ローカル宣言は、必ず宣言しよう」をお届けしました。
項目9は本文も短かったので、次回の前振りとなる、静的スコープの仕組みについて考察しました。
次回は、「クロージャと仲良くしよう」 をお届けします。
参考資料
改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで 山田祥寛 技術評論社 Standard ECMA-262 5.1 Edition JS Primer 関数とスコープ
このシリーズの目次
- [EffectiveJavaScript輪読会2]グローバルオブジェクトを使うのは、最小限にとどめる
- [EffectiveJavaScript輪読会2]ローカル宣言は、必ず宣言しよう
- [EffectiveJavaScript輪読会2]クロージャと仲良くしよう
- [EffectiveJavaScript輪読会2]変数の巻き上げ(ホイスティング)を理解する
- [EffectiveJavaScript輪読会2]ローカルスコープを作るには即時関数式(IIFE)を使おう
- [EffectiveJavaScript輪読会2]名前付き関数式のスコープは可搬性がないので注意しよう
- [EffectiveJavaScript輪読会2]ブロックローカルな関数宣言のスコープも可搬性がないので注意しよう