順番に読み進めながら学べます

クロージャ — 外側の変数を覚える関数

関数が外側のスコープの変数を覚えるクロージャを扱います。呼び出しをまたいで残るカウンター、設定値を覚えた関数を返す関数ファクトリ、ループの中で作る関数まで確かめます。

ページの表示回数のように、呼ぶたびに増えていく値を関数に持たせたいことがあります。ふつうの変数で数えると、値が残らないか、どの行からでも書き換えられるかのどちらかになります。

この記事では、外側のスコープの変数を覚えている関数であるクロージャと、設定値を覚えた関数を返す関数ファクトリを扱います。

呼び出しをまたいで値を残す — クロージャの基本

資料のダウンロード数を数えたいとします。数える関数の中で let を宣言すると、呼び出しのたびに 0 に戻ります。一番外側で宣言すれば値は残りますが、どの行からでも書き換えられる場所に置くことになります。

クロージャ(外側のスコープの変数を覚えている関数)を使うと、値を呼び出しをまたいで残しながら、外の行からは書き換えられないようにできます。関数の中で変数を宣言し、その変数を読み書きする関数を return で返すと、返された関数はその変数を覚えたまま外へ渡ります。

function makeDownloadCounter() {
  let total = 0;                  // makeDownloadCounter の中だけの変数

  const countUp = () => {
    total = total + 1;            // 外側の total を書き換える
    return total;
  };

  return countUp;                 // 関数そのものを返す
}

const countDownload = makeDownloadCounter();   // カウンターを 1 つ作る
console.log(countDownload());     // 1
console.log(countDownload());     // 2
console.log(countDownload());     // 3(前の値から続く)
// console.log(total);            // ReferenceError: total is not defined
返された関数が覚えている変数
一番外側(どの波括弧にも囲まれていない)
  • const countDownload = makeDownloadCounter() — 返ってきた関数が入る
  • console.log(total) — 名前が見えず ReferenceError
makeDownloadCounter の波括弧
  • let total = 0countDownload() を呼んでも 0 には戻らない
  • return countUp — 値ではなく関数そのものを返す
countUp の波括弧
  • total = total + 1 — 外側の total を書き換える
  • 外側の実行が終わったあとも、この total を読み書きできる
countUp は外側の total を覚えたまま一番外側へ渡ります。関数を抜けても、覚えられた変数だけは残ります

中で countUp と名付けた関数を、外では countDownload という名前で呼んでいます。return で渡るのは関数そのものなので、受け取る変数には使う場面に合った名前を付けられます。下の表は、数える値を置く場所ごとに、値が残るかと書き換えられるかを並べたものです。

数える値を置く場所呼び出しをまたいだときの値外の行から書き換えられるか
数える関数の中で let を宣言する呼び出しが終わると消え、次は 0 から名前が見えないので書き換えられない
一番外側で let を宣言するそのまま残るどの行からでも書き換えられる
外側の関数で let を宣言し、内側の関数を返す返された関数を呼ぶたびに前の値から続く返された関数を通したときだけ変わる

動画の無料体験は 3 回まで再生できます。変数は自分で宣言します。

① 残り回数を 3 から始め、呼ぶたびに 1 減らした残り回数を返す関数を返す makeTrialCounter を書いてください。残りが 0 のときは減らしません。

② makeTrialCounter から体験用の関数を作ってください。

③ ② を 4 回呼び出して、それぞれ「残り ◯ 回」の形で表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

設定値を覚えた関数を作る — 関数ファクトリ

会員ランクごとにポイントの付与率が違うとします。計算のたびに率を引数で渡す書き方だと、呼び出し側がランクを取り違える余地が残り、同じ率を何度も書くことになります。率だけを先に決めておきたい場面です。

関数ファクトリ(設定値を受け取って、その値を覚えた関数を返す関数)を作ると、率を渡すのは 1 度だけで済みます。引数もクロージャが覚える対象なので、返された関数は自分が作られたときの rate を読み続けます。

function makePointCalculator(rate) {
  // rate は引数。返した関数がこの rate を覚え続ける
  return (amount) => Math.floor(amount * rate);
}

const regularPoint = makePointCalculator(0.01);    // 通常会員は 1%
const goldPoint = makePointCalculator(0.05);       // ゴールド会員は 5%
const platinumPoint = makePointCalculator(0.1);    // プラチナ会員は 10%

console.log(regularPoint(3000));    // 30
console.log(goldPoint(3000));       // 150
console.log(platinumPoint(3000));   // 300

// 変数に入れず、括弧を 2 組続けてその場で計算する
console.log(makePointCalculator(0.05)(5000));   // 250
同じ定義から 3 つの関数ができる
makePointCalculator(rate) の定義は 1 つ(0.01) を渡してregularPoint(0.05) を渡してgoldPoint(0.1) を渡してplatinumPoint覚えた rateは 0.01覚えた rateは 0.05覚えた rateは 0.13000 円で30 が返る3000 円で150 が返る3000 円で300 が返る
関数の本体はどれも同じ式で、違うのは覚えている rate だけです。渡した rate は、あとから書き換えない限り同じ値のまま読まれます

makePointCalculator(0.05)(5000) では、1 組目の括弧で率を覚えた関数を作り、2 組目の括弧で金額を渡してその場で計算しています。括弧が 1 組だけのときに返るのは、計算結果の数値ではなく関数です。

本店と支店で、送料無料になる金額と送料が違います。cartTotals は宣言済みです。

① 無料になる金額と送料を受け取り、合計金額を渡すと送料を返す関数を返す makeShippingFee を書いてください。

② 本店(4000 円以上で無料、送料 500 円)と支店(3000 円以上で無料、送料 350 円)の関数を作ってください。

③ cartTotals の各金額を両方に通し、「◯ 円 → 本店 ◯ 円 / 支店 ◯ 円」と表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

作るたびに変数が増える — ファクトリと let

商品ごとに在庫の残数を減らしていきたいとします。減算に使う変数を 1 つだけ用意して共有すると、シャツを予約した数がキャップの残数にも響きます。商品ごとに別の値を持たせる必要があります。

最初に作った makeDownloadCounter のように、関数を返す関数の中で let を宣言すると、その変数は外側の関数を呼び出すたびに新しく作られます。返された関数どうしは変数を共有しないため、同じ定義から作っても、それぞれが自分の変数だけを読み書きします。

function makeStockReserver(stock) {
  let rest = stock;               // 呼び出しごとに新しく作られる

  return (count) => {
    rest = rest - count;
    return rest;
  };
}

const reserveShirt = makeStockReserver(10);
const reserveCap = makeStockReserver(4);

console.log(reserveShirt(3));     // 7
console.log(reserveCap(1));       // 3
console.log(reserveShirt(2));     // 5(シャツの残りだけが減る)
console.log(reserveCap(1));       // 2
呼ぶたびに新しい変数ができる
makeStockReserver(10) を実行rest = 10 の変数が1 つ作られるmakeStockReserver(4) を実行別の rest = 4 がもう 1 つ作られるreserveShirt(3) とreserveCap(1) を呼ぶ7 と 3 が返り互いに影響しない
同じ定義から作っても、関数ごとに別の rest が用意されます。片方を減らしても、もう片方の残数は動きません

3 つ目の console.log にある reserveShirt(2) はシャツの残数だけを 7 から 5 に減らし、キャップ側は 3 のままです。次の reserveCap(1) が 2 を返すので、キャップの残数も自分の値を覚え続けていると分かります。

作る呼び出しを毎回書くと値は続きません

makeStockReserver(10)(3) を 2 回書くと、どちらも 7 が返ります。呼び出しのたびに別の rest が 10 から作られるためです。減らした残数を次の予約に引き継ぐには、返ってきた関数を reserveShirt のように変数に入れて、その変数を呼び出します。

注文と返品で、それぞれ独立した連番の ID を発行します。変数は自分で宣言します。

① 接頭辞を受け取り、呼ぶたびに「接頭辞-1」「接頭辞-2」と続く ID を返す関数を返す makeIdIssuer を書いてください。

② ORD 用と RTN 用の発番関数を 1 つずつ作ってください。

③ 注文を 2 件、返品を 1 件発行して、それぞれ表示してください。

④ もう一度注文を発行して表示し、続きの番号になるか確かめてください。

JavaScript / TypeScript エディタ

コードを実行してください

ループの中で関数を作る — 周ごとに別の let

ログの種類ごとに、出力用の関数をまとめて用意したいとします。ループの中で関数を作って配列に貯めると、呼び出すのはループが終わったあとです。このとき、関数がどの周の値を覚えているかは、ループ変数をどう宣言したかで変わります。

var はこの講座では使いませんが、let との違いがはっきりするので、ここでは並べて見ます。for の丸括弧で let を使うと、繰り返しの周ごとに新しい変数が作られ、その周に作った関数はその変数を覚えます。var は周をまたいで 1 つの変数を共有するため、すべての関数がループ後の値を読みます。

const levels = ["info", "warn", "error"];

// var は 1 つの変数をすべての周で共有する
const varLoggers = [];
for (var i = 0; i < levels.length; i++) {
  varLoggers.push(() => `[${levels[i]}]`);
}
console.log(varLoggers[0]());     // [undefined]

// let は周ごとに新しい変数を作る
const letLoggers = [];
for (let j = 0; j < levels.length; j++) {
  letLoggers.push(() => `[${levels[j]}]`);
}
console.log(letLoggers[0]());     // [info]
console.log(letLoggers[2]());     // [error]
ループ変数を覚える関数の行き先
for の丸括弧にvar を書く周をまたいで変数は 1 つ3 つの関数が同じ i を読む[undefined]が返るfor の丸括弧にlet を書く周ごとに新しい変数ができる0・1・2 をそれぞれ覚える[info]が返る
var の側は 3 つの関数が同じ変数を読み、let の側は自分の周の値を読みます。覚える対象は、周ごとに作られるかどうかで変わります

var の側が [undefined] になるのは、ループを抜けた時点の i が 3 で、levels[3] が無いからです。let の側でも levels[j] を読むのは呼び出したときですが、読む j がその周に作られた変数なので、周ごとの値が残っています。

覚えるのは作ったときの値ではありません

クロージャが覚えているのは変数そのもので、読むのは呼び出した時点の値です。let rate = 0.05; を読む関数を作ったあとで rate = 0.1; と再代入すると、その関数も 0.1 で計算します。値を固定したい設定は、makePointCalculator(0.05) のようにファクトリの引数として渡します。

支払い方法ごとに受付の通知を表示する関数を作ります。methods・labels・notifiers は宣言済みです。

① for の丸括弧に let を書いて methods をループしてください。

② 周ごとに支払い方法の名前をキーにして、注文番号を受け取り「[銀行振込] A-1002 を受け付けました」と表示する関数を notifiers に入れてください。

③ ループのあと bank に A-1002、cod に A-1003 を渡してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1countDownload() を 2 回呼び、一番外側で total を表示すると何が起きますか?

Q2残数 4 の reserveCap に、reserveShirt で 3 を減らしたあと 1 を渡すと何が返りますか?

Q3for (var i = 0; ...) の中で作った関数を、ループが終わったあとに呼び出すと i は何になっていますか?