どうも。つじけ(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
このシリーズの目次
- [EffectiveJavaScript輪読会2]グローバルオブジェクトを使うのは、最小限にとどめる
- [EffectiveJavaScript輪読会2]ローカル宣言は、必ず宣言しよう
- [EffectiveJavaScript輪読会2]クロージャと仲良くしよう
- [EffectiveJavaScript輪読会2]変数の巻き上げ(ホイスティング)を理解する
- [EffectiveJavaScript輪読会2]ローカルスコープを作るには即時関数式(IIFE)を使おう
- [EffectiveJavaScript輪読会2]名前付き関数式のスコープは可搬性がないので注意しよう
- [EffectiveJavaScript輪読会2]ブロックローカルな関数宣言のスコープも可搬性がないので注意しよう