[EffectiveJavaScript輪読会2]ローカルスコープを作るには即時関数式(IIFE)を使おう

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のおさらい

前回は、「変数の巻き上げ(ホイスティング)を理解する」をお届けしました。

[EffectiveJavaScript輪読会2]変数の巻き上げ(ホイスティング)を理解する
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のおさらい前回は、「クロージャと仲良くしよう...

今回は、「ローカルスコープを作るには即時関数式(IIFE)を使おう」 をお届けします。

テキスト第2章「変数のスコープ」の項目13に対応しています。

今日のアジェンダ

  • クロージャは外側の変数を値ではなく参照する
  • IIFEちゃん

クロージャは外側の変数を値ではなく参照する

テキストでは、下記ソースコードが、undefinedを返すことを指摘しています。

function myFunction2_13_01() {

  function wrapElements(a) {

    var result = [], i;
    for (i = 0; i < a.length; i++) {
      result[i] = function(){return a[i];}
    }

    return result;
  }

  const wrapped = wrapElements([10, 20, 30, 40, 50]);
  const f = wrapped[0]; //配列のインデックスをマジックナンバーで指定

  console.log(f()); //undefined

}

注目すべきは、for文の中に書かれている無名関数 で、無名関数の戻り値になっている 識別子i の存在です。

この識別子iは、無名関数の中で定義されていません。したがってスコープチェーンの仕組みにより、外側の変数を参照します。

for文がまわる度に、無名関数は作成されますが、i = 0やi = 1が戻り値に代入されるわけではありません。

あくまで iは、外側の変数への参照を行うもの です。

for文で回される変数iは、インクリメントされる度に、

  • i = 0
  • i = 1,
  • i = 2, と変化していきます。

var宣言された変数iが、for文を抜けて、関数wrapElementsがreturnされるときの、iの値は5 です。(for文を理解しましょう)

また、関数wrapElementの戻り値resultは、配列resultです。それぞれの要素に無名関数が格納されています。

[ [Function], [Function], [Function], [Function], [Function] ]

iはこのようにインクリメントされて、実行されます。

function myFunction2_13_05() {

  function wrapElements_(a) {
    
    var result = [], i, n; //nは宣言のみで未使用

    i = 0;
    result[i] = function () { return a[i]; };//0番目のi
    // return result;

    i++; //i=1
    result[i] = function () { return a[i]; };//1番目のi
    // return result;

    i++; //i=2
    result[i] = function () { return a[i]; };//2番目のi
    // return result;

    i++; //i=3
    result[i] = function () { return a[i]; };//3番目のi
    // return result;

    i++; //i=4
    result[i] = function () { return a[i]; };//4番目のi

    i++; //i=5。ここでループを抜ける
    return result;

  }
}

なので、a[i] = a[5]ということで undefined が返っています。

バグでもなんでもなく、意外でもないような、当然のような気もしないでもないですが、それは私たちがES6の恩恵を受けているからです。

ES6から導入されたlet、const宣言のブロックスコープは、この問題を解決します。(ついでにfro文の処理をアロー関数に書き換えています)

function myFunction2_13_02() {

  function wrapElements(a) {

    const result = [];
    for (let i = 0; i < a.length; i++) {
      result[i] = () => a[i];
    }

    return result;
  }

  const wrapped = wrapElements([10, 20, 30, 40, 50]);
  const f = wrapped[0]; //配列のインデックスをマジックナンバーで指定

  console.log(f()); //10

}

let宣言は、ブロックスコープを生成します。

なので、戻り値として、[10, 20, 30, 40, 50]を返す配列resultをもっているととらえていいと思います。

なので引数にインデックスの0を渡すと、10が返されています。

大切なのは、クロージャは 外側の変数を値ではなく、参照で保存する 、ということです。

varとletの違いは、参照した先が、ブロックスコープで閉じられているかどうか です。

let宣言をしたからと言って、値そのものが格納されているわけではありません。

IIFEちゃん

オランダのグラフィックデザイナー、ディック・ブルーナが生んだキャラクターは、MIFFYです。

かわいらしい顔をしていますが、お口がバッテン(x)です。なにか良くないことがあるのでしょうか。

ES6でブロックスコープが導入されるまえは、クロージャが外側の変数を参照する範囲 を閉じ込めることができませんでした。

当時の解決策として、強制的にブロックスコープを生成する 方法が、即時関数(immediately invoked function expression)です。

略して IIFE(イッフィー) と呼びます。

即時関数は、ローカルスコープを生成して、スコープを閉じ込めようというJavaScriptの書き方です。

function myFunction2_13_03() {

  var teacher = 'Kyle';

  (function anotherTeacher() {
    var teacher = 'Suzy';
    console.log(teacher); //Suzy
  })();

  console.log(teacher); //Kyle

}

クラスをグローバル領域に記述し、どこからでも呼び出せるようにグローバルオブジェクト化する方法としても重宝されていました。

即時関数の引数にthisをぶつけるのを理解したときに、衝撃を受けた記憶があります。

(function (global) {
  var msg = 'Hello!';
  var Person = function (name, age) {
    this.name = name;
    this.age = age;
  };

  Person.prototype.greet = function () {
    Logger.log("%s I'm %s!", msg, this.name);
  };

  global.Person = Person;
})(this);


function myFunction2_13_04() {
  var p1 = new Person('Bob', 25);
  p1.greet(); //Hello! I'm Bob!
}

とても重宝がられていたIIFEちゃんは、ES6のlet、const宣言によるブロックスコープの生成と、クラス構文により、GASでも役割を終えました。

私はMIFFYちゃんを見るたびに思い出すでしょう。

お口のバッテンは、スコープを閉じ込めたかったんだね、と。

まとめ

以上で、「ローカルスコープを作るには即時関数式(IIFE)を使おう」をお届けしました。

すこし寂しい気持ちになりましたが、前を向いていきましょう。

次回は、「名前付き関数式のスコープは可搬性がないので注意しよう」 をお届けします。

参考資料

the full Getting Started with JavaScript

ノンプロ研GAS中級講座1期 Day2 クラス

Special Thanks

SWD氏

このシリーズの目次

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