【JavaScriptクイズ】第6問:「関数を作る関数」の作り方

2023/04/05に公開

JavaScriptの文法や便利な使い方について、気軽なクイズ形式で解説する記事を書いていきます。

今回のテーマは「関数の性質」です。では、さっそく問題です!

問題

JavaScriptでは、「関数を変数に代入できる」ということを知っているでしょうか?

例えば、次のようなプログラムがあったとしましょう。あいさつを表示する関数greetを定義して、それを呼び出しています。

"use strict";

function greet(name) {
  console.log("こんにちは、" + name + "さん!");
}

let f = greet; // 関数を変数に代入
f("太郎"); // 変数を介した関数呼び出し

実行結果
こんにちは、太郎さん!

関数greetを、変数fに代入している様子が分かるでしょう。こうすると、変数を介して関数を呼び出せるようになります。

このプログラムは、次のように短く書くこともできます。

"use strict";

let f = function(name) {
  console.log("こんにちは、" + name + "さん!");
}

f("太郎"); // 変数を介した関数呼び出し

こんどは関数名greetを省略して関数を定義し、すぐに変数fに代入しています。関数自体が名前をもっていなくても、変数に割り当てておけば呼び出せるということが分かるでしょう。このような名前のない関数は、「無名関数」と呼ばれます。

さて、ここからが本題です。プログラムの穴埋め問題をやってみましょう。

quiz.js
"use strict";

// 「あいさつを表示する関数」を作る関数
function makeGreeter(left, right) {
  // ここには何が入る?
}

// 使用例1
let greeter1 = makeGreeter("こんにちは、", "さん!");
greeter1("太郎");

// 使用例2
let greeter2 = makeGreeter("前略、", "さま。");
greeter2("花子");

期待される実行結果
こんにちは、太郎さん!
前略、花子さま。

実行結果から、変数greeter1greeter2には、それぞれ異なるあいさつを表示する関数が代入されていることが分かります。これらの関数は、どちらも関数makeGreeterによって作られます。

でも、makeGreeterの中身が空欄になっています。ここは、どのように書けばいいでしょうか?

ヒントを見る?

makeGreeterは「関数を作る関数」です。つまり、「あいさつを表示する関数をその場で定義して、returnで返す」という動作をします。

答えを見る?

あいさつを表示する関数をmakeGreeterの内側で定義して、returnで返せばOKです。

// 「あいさつを表示する関数」を作る関数
function makeGreeter(left, right) {
  function greet(name) {
    console.log(left + name + right);
  }

  return greet;
}

無名関数を使って、次のように短く書けるとベターです。

// 「あいさつを表示する関数」を作る関数
function makeGreeter(left, right) {
  return function(name) {
    console.log(left + name + right);
  };
}

解説

「関数を作る関数」のメリットは、必要な関数をプログラムの実行中にいくつでも作れることです。今回の問題ではあいさつを表示する関数を2つ作っていますが、もっと作ってもいいのです。

例えばこんな風に……

// 使用例3
let greeter3 = makeGreeter("ハロー、", "!");
greeter3("アリス");

// 使用例4
let greeter4 = makeGreeter("いつもありがとう、", "君。");
greeter4("ボブ");

……

こうしたことが可能なのは、JavaScriptの関数が特別な性質を備えているからです。

関数がもつ2つの性質

JavaScriptの関数には、重要な2つの性質があります。

  • 性質1:関数は、変数に代入できる
  • 性質2:関数の内側から、その関数が定義された場所にある変数にアクセスできる

「性質1」については、最初に説明したとおりです。ただし、変数に代入できるというのは、引数や戻り値にもできることを意味する点は補足しておいたほうがいいかもしれません。今回の「答え」でも、makeGreeterは関数を戻り値にしていますね。

// 「あいさつを表示する関数」を作る関数
function makeGreeter(left, right) {
  
  // 関数makeGreeterが呼び出されるたびに、新たに関数greetを定義する
  function greet(name) {
    console.log(left + name + right);
  }

  return greet; // 上で定義した関数greetを、関数makeGreeterの戻り値とする
}

「性質2」は変数のスコープ、つまり、変数の有効範囲はどこまでかという話です。今回の「答え」では、makeGreeterの引数になっている2つの変数leftrightに注目しましょう。

// 「あいさつを表示する関数」を作る関数
function makeGreeter(left, right) {
  // 変数leftとrightは、関数makeGreeterの内側で有効
  
  function greet(name) {
    // 変数leftとrightは、ここからもアクセスできる
    
    console.log(left + name + right);
  }

  return greet;
}

変数leftrightは、関数makeGreeterの内側でのみ有効です。そして、この場所では関数greetが定義されています。すると、関数greetの内側からでも、変数leftrightにアクセスできるのです。

この性質はイメージしづらいところだと思うので、もう少し詳しく見ていきましょう。

関数の内側から見える変数はどれ?

次のプログラムでは、3つの変数が使われています。

  • i:関数の内側で宣言された変数
  • p:関数に引数として渡された変数
  • o:関数の外側で宣言された変数

このプログラムを実行してみると……

"use strict";

let o = "outer";

function example(p) {
  let i = "inner";

  console.log(i);
  console.log(p);
  console.log(o);
}

example("parameter");

実行結果
inner
parameter
outer

実行結果から、関数exampleの内側では3つの変数すべてにアクセスできることが分かります。変数ipは、そもそも関数exampleの内側を有効範囲とする変数ですね。また、変数oは一番外側で定義されいるためグローバルであり、どこからでもアクセスできます。

では、関数のネスト(入れ子)がある場合はどうなるでしょうか?

"use strict";

let o = "outer";

function example(p) {
  let i = "inner";

  function nested() { // ネストされた(関数の内側で定義された)関数
    console.log(i);
    console.log(p);
    console.log(o);
  }

  nested(); // ネストされた関数nestedを呼び出す
}

example("parameter");

実行結果
inner
parameter
outer

やはり、3つの変数すべてにアクセスできました。ネストされた関数nestedからは、グローバル変数oだけでなく、外側の関数exampleを有効範囲とする変数ipにもアクセスできるのだと分かります。

こんどは、ネストされた関数を戻り値にしてみましょう。それを変数fに代入してから、変数を介して呼び出します。

"use strict";

let o = "outer";

function example(p) {
  let i = "inner";

  function nested() { // ネストされた(関数の内側で定義された)関数
    console.log(i);
    console.log(p);
    console.log(o);
  }

  return nested; // ネストされた関数nestedを、関数exampleの戻り値とする
}

// console.log(i); ← こう書くとエラー:ここから変数iにはアクセスできない
// console.log(p); ← こう書くとエラー:ここから変数pにはアクセスできない

let f = example("parameter"); // ネストされた関数nestedが代入される
f(); // 変数を介して関数nestedを呼び出す

実行結果
inner
parameter
outer

この場合も、3つの変数すべてにアクセスできていることが分かります。関数nestedを最終的に呼び出している場所は関数exampleの外側ですが、定義されている場所が関数exampleの内側なので、そこにあった変数にアクセスできるということです。

まとめ

JavaScriptの関数には、重要な2つの性質があります。

  • 性質1:関数は、変数に代入できる
  • 性質2:関数の内側から、その関数が定義された場所にある変数にアクセスできる

このような2つの性質を備えた関数は、一般的に「クロージャ(closure)」と呼ばれます。実はJavaScript以外にも、クロージャはいろいろなプログラミング言語で使えます。

なお、今回の問題は、下記のC言語の問題をJavaScript向けにアレンジしたものでした。C言語の文法にクロージャはありませんが、今回とよく似た「あいさつを表示する関数」が題材になっています。

https://curiocube.team-aries.com/cquiz-q24/

また、下記の本ではJavaScriptとC言語を含む、10種類のプログラミング言語を比較しています。よろしければチェックしてみてください。

https://zenn.dev/teamariesdotcom/books/af56bae422969b

Discussion