[EffectiveJavaScript輪読会2]ローカル宣言は、必ず宣言しよう

GAS

どうも。つじけ(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: The JavaScript Code Quality and Coverage Tool
JSLint, The JavaScript Code Quality and Coverage Tool. This file allows JSLint to be run from a web browser. It can accept a source program and ...

ソースコードを貼りつけて、「JSLint」をクリックしてみましょう。 

ソースコード内の、宣言されていない変数 を指摘してくれます。

これは、まずグローバルオブジェクトをリスト化して、リストにも存在しない、ソースコード内でも宣言されてない変数の参照、あるいは代入を指摘しています。 

GASのスクリプトエディタには、変数を右クリック(もしくはCtrl + F12)して「定義へ移動」すると、変数宣言を確認する機能があります。 

プロジェクト全体 で、変数宣言がされていないことを教えてくれます。(もし、別のスクリプトファイルのグローバル領域に変数宣言していると、何も表示されません。移動もしません。) 

自動チェックツールではありませんが、活用してみましょう。

レキシカルスコープとは

「変数のスコープ」の基礎として、おさえておきたい用語のひとつに、レキシカルスコープがあります。

レキシカルとは

lexical(レキシカル)は、「語彙、辞書の、辞書的な」という意味です。似たような単語にtoken(トークン)があります。

どちらも「字句」という意味をもち、プログラミングにおいて、文字列の最小単位を表します。

とくにレキシカルが用いられるのは、字句解析(lexer)という表現をするときです。

字句解析(lexer)の対義語は、構文解析(parser)です。HTML Parserや、JSON Parserとして、みなさんも馴染みがあるかもしれません。

字句解析と構文解析は、解析する対象が、字句か文かです。  出典:プログラミング言語 8 字句解析器(lexer)と構文解析器(parser) 東京大学情報理工学系研究科電子情報学専攻田浦研究室

プログラミングにおける字句解析とは、ソースコードを字句単位で解析することです。  出典:プログラミング言語 8 字句解析器(lexer)と構文解析器(parser) 東京大学情報理工学系研究科電子情報学専攻田浦研究室

プロジェクトが実行されるまで

ソースコードを字句解析するのは、あらゆるプログラミング言語は、以下の手順を追って実行されるからです。

  1. ソースコードを書く
  2. ソースコードを読み込む
  3. ソースコードの文法をチェックする
  4. (ソースコードが実行できるように準備する)
  5. ソースコードを実行する

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 関数とスコープ

このシリーズの目次

タイトルとURLをコピーしました