どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「ローカル宣言は、必ず宣言しよう」をお届けしました。
今回は、「クロージャと仲良くしよう」 をお届けします。
テキスト第2章「変数のスコープ」の項目11に対応しています。※担当回です。気合いが入ります。
今日のアジェンダ
- メモリの破棄と保持
- 関数に戻り値を持たせた時のメモリの保持
- 関数の戻り値に関数をもたせる
- 関数式によるクロージャ
- クロージャは、外側の変数を更新できる
- クロージャとクラス
メモリの破棄と保持
スコープのおさらいですが、const宣言は 関数スコープを生成 しますので、関数を飛び出しません(関数の外から参照できません)。
これは、内部的には、関数を抜けるとメモリを破棄 しています。もう、2度と使わないからです。
メモリを破棄するのは、変数に限った話ではありません。
関数自体も、その後使われることがなければ(記述していてもコメントアウトしていれば使わないこととイコールです。)、メモリは破棄されます。
このようなコードには、メモリを保持するタイミングがありませんので、ブレークポイントも置けません。
function myFunction2_11_01() {
function makeSandwich() {
const vegetable = 'レタス';
}
// console.log(vegetable); //ReferenceError: vegetable is not defined
// const sw = makeSandwich(); //コメントアウトは使わないこととイコール
// console.log(sw);
}
関数を抜けたあと、変数に代入するなど、関数を使うコードを記述すると、メモリは保持されます。
function myFunction2_11_02() {
function makeSandwich() {
const vegetable = 'レタス';
}
// console.log(vegetable); //ReferenceError: vegetable is not defined
const sw = makeSandwich();
}
ブレークポイントを置いて、デバッガで確認してみましょう。
関数に戻り値を持たせた時のメモリの保持
このことを応用して、関数に戻り値 を持たせてみましょう。
実行すると、関数makeSaladsのメモリは破棄されますが、関数makeSandwichは、後で変数swに代入されているので、メモリは破棄されません。
さらに、関数makeSandwichを抜けた時点で変数vegetableのメモリは破棄されますが、戻り値として、変数vegetableをもっていますので、値’レタス’を取得できます。
function myFunction2_11_03() {
function makeSalads() {
const vegetable = 'レタス';
}
function makeSandwich() {
const vegetable = 'レタス';
return vegetable;
}
const sw = makeSandwich(); //戻り値vegetableを格納している
console.log(sw); //レタス
}
関数の戻り値に関数をもたせる
それでは最後に、関数makeSandwichの戻り値に関数make をもたせてみましょう。
おさらいですが、関数makeは「スコープチェーンとして、変数vegetableを参照します」。
関数makeSandwichの戻り値は、関数makeです。ということは、「スコープチェーンとして、変数vegetableを参照します」も、戻り値と一緒に連れてきます 。
function myFunction2_11_04() {
function makeSandwich() {
let vegetable = 'レタス';
function make(topping = 'なし') {
return vegetable + 'and' + topping;
}
return make;
}
const sw = makeSandwich(); //戻り値に関数makeを格納している
console.log(sw); //[Function: make]
console.log(sw()); //レタスandなし
console.log(sw('トマト')); //レタスandトマト
}
この、参照先(データを保存してる場所)をもつ関数のことを、クロージャと呼びます。
クロージャという名前の由来
関数makeは変数vegetableを参照します。変数vegetableは関数make内では定義されていません。
このような変数を 自由変数 と呼びます。
関数makeは自由変数を含んでいるので、開いた関数 です。
そして、関数makeSandwichの内部にある変数名対応表(のようなもの)では、’レタス’とvegetableという 名前が結合 しています。
開いた関数makeが、関数makeSandwichの変数名対応表(のようなもの)とセットになることで、それ以上、外側のスコープに変数を探しにいかなくてよくなります 。
ある種、完結したものになるのです。これを「閉じた」と表現しています。
クロージャによる関数定義
クロージャによる関数定義は、関数式 でも記述できます。
関数makeSandwich内部の関数(先ほどまでの関数makeのような)を個別に呼び出すことがないので、関数に名前をつける必要がありません。
また、さきほどまでローカル変数だった変数vegetableを関数makeSandwichの仮引数に設定し、変数に代入するときに引数として渡すことで、オブジェクトのようなものを生成できる ことがわかります。
function myFunction2_11_05() {
function makeSandwich(vegetable) {
return function (topping) {
return vegetable + 'and' + topping;
};
}
const lettuceAnd = makeSandwich('レタス');
console.log(lettuceAnd('トマト')); //レタスandトマト
console.log(lettuceAnd('バジル')); //レタスandバジル
const eggAnd = makeSandwich('エッグ');
console.log(eggAnd('トマト')); //エッグandトマト
console.log(eggAnd('バジル')); //エッグandバジル
}
クロージャは、外側の変数を更新できる
クロージャは、外側の変数vegetableへの参照を保存します。決して値をコピーするわけではありません。
したがって、変数vegetableの更新は、スコープチェーンによりアクセスされる すべてのクロージャに反映 されます。
下記のような、3つのクロージャ(set、get、type)を用意しました。
変数vegetableへのアクセスは、クロージャ経由でなければなりません。
function myFunction2_11_06() {
function makeSandwich() {
let vegetable = undefined;
const obj = {
set(newVegetable) {
vegetable = newVegetable;
},
get() {
return vegetable;
},
type() {
return typeof vegetable
}
};
return obj;
}
const sw = makeSandwich();
console.log(sw.type()); //undefined
sw.set('レタス');
console.log(sw.get()); //レタス
console.log(sw.type()); //string
}
みなさんも、お気付きだと思いますが、これはgetter/setterです。
JavaScriptでgetter/setterが書けるのは、クロージャのおかげです。
そして、なんとなく、クロージャがクラスなんじゃないか と思われたかもしれません。さすがです。
JavaScriptのクラスは、クロージャ と、prototype によクラスと同様の書き方ができます。
function myFunction2_11_07() {
//クロージャによるクラス
function VegetableA(vegetable) {
this.getVegetable = function () {
return vegetable;
}
this.makeSandwich = function (topping) {
return vegetable + 'and' + topping;
}
}
//prototypeによるクラス
function VegetableB(vegetable) {
this.vegetable = vegetable
}
VegetableB.prototype.getVegetable = function () {
return this.vegetable;
}
VegetableB.prototype.makeSandwich = function (topping) {
return this.vegetable + 'and' + topping;
}
const a = new VegetableA('レタス');
console.log(a.getVegetable()); //レタス
console.log(a.makeSandwich('トマト')); //レタスandトマト
const b = new VegetableB('レタス');
console.log(b.getVegetable()); //レタス
console.log(b.makeSandwich('チーズ')); //レタスandチーズ
}
しかし、クロージャによるクラスの書き方は、インスタンスを生成するたびにメソッド(もいっしょにコピーしてしまうので)の分までメモリを食ってしまいます。
prototypeの方が合理的でしょう。
そして、ES6からは クラス構文 が登場しました。
クラス構文では、constructorキーワードでコンストラクタ関数 を定義し、メソッド構文で関数(メソッド) を定義します。
なので、クロージャ(関数の中に関数がある)という書き方をあえて選択する場面はないと思います。JavaScriptの関数の成り立ちを理解しましょう。
function myFunction2_11_08() {
class Vegetable {
constructor(vegetable) {
this.vegetable = vegetable;
}
getVegetable() {
return this.vegetable;
}
makeSandwich(topping) {
return this.vegetable + 'and' + topping;
}
}
const v = new Vegetable('レタス');
console.log(v.getVegetable()); //レタス
console.log(v.makeSandwich('トマト')); //レタスandトマト
}
最後に、クラス構文によるgetter/setterもご紹介します。
function myFunction2_11_09() {
class Vegetable {
constructor(vegetable) {
this._vegetable = vegetable;
}
get vegetable() {
return this._vegetable;
}
set vegetable(vegetable) {
this._vegetable = vegetable;
}
}
const v = new Vegetable('レタス');
console.log(v.vegetable); //レタス →getterの呼び出し
v.vegetable = 'にんじん'; //setterの呼び出し
console.log(v.vegetable); //にんじん
}
まとめ
以上で、「クロージャと仲良くしよう」をお届けしました。
テキストで書かれていたことは、クロージャは3つの事実があるということでした。
- JavaScriptは、現在の関数の外側で定義された変数を参照できる
- 関数は、その外側の関数の処理が抜けた後でも、まだ外側の関数内で定義された変数を参照できる
- クロージャは、外側の変数を更新できる
わたしは、とくに2番目が理解できなかったので、「メモリの解放」という視点から考察してみました。
気付いてみると、「JavaScriptの関数はクロージャである」 という言葉の意味が、理解できた気がします。
これから、積極的にクラス構文を書いていこうと思います。
次回は、「変数の巻き上げ(ホイスティング)を理解する」 をお届けします。
参考資料
- 改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで 山田祥寛 技術評論社
- コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus) 西尾 泰和
- JS Primer 関数とスコープ
このシリーズの目次
- [EffectiveJavaScript輪読会2]グローバルオブジェクトを使うのは、最小限にとどめる
- [EffectiveJavaScript輪読会2]ローカル宣言は、必ず宣言しよう
- [EffectiveJavaScript輪読会2]クロージャと仲良くしよう
- [EffectiveJavaScript輪読会2]変数の巻き上げ(ホイスティング)を理解する
- [EffectiveJavaScript輪読会2]ローカルスコープを作るには即時関数式(IIFE)を使おう
- [EffectiveJavaScript輪読会2]名前付き関数式のスコープは可搬性がないので注意しよう
- [EffectiveJavaScript輪読会2]ブロックローカルな関数宣言のスコープも可搬性がないので注意しよう