2021年12月4日

関数オブジェクト, NFE

既にご存知の通り、JavaScriptの関数は値です。

JavaScriptのすべての値は型を持っています。関数は型は何でしょうか?

JavaScriptでは、関数はオブジェクトです。

関数をイメージする良い方法は、呼び出し可能な “アクションオブジェクト” とみなすことです。私たちはそれらを呼び出すだけでなく、オブジェクトとして扱うこともできます: プロパティの追加/削除、参照渡しなど。

“name” プロパティ

関数オブジェクトには、往々にして使用可能なプロパティが少ししかありません。

例えば、関数名は “name” プロパティとしてアクセス可能です:

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

もっと面白いことに、名前を割り当てるロジックはスマートです。 割り当てに使用される関数にも正しい名前を貼り付けます。:

let sayHi = function() {
  alert("Hi");
}

alert(sayHi.name); // sayHi (works!)

デフォルト値を通して行われた代入でも動作します:

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi (works!)
}

f();

仕様では、この機能は “contextual name(文脈上の名前)” と呼ばれます。関数がそれを提供しない場合、代入ではコンテキストから見つけ出されます。

オブジェクトメソッドも名前を持っています。:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

しかし、正しい名前を把握する方法がない場合があります。そのようなとき、name プロパティは次ように空になります。:

// 配列の中で作られた関数
let arr = [function() {}];

alert( arr[0].name ); // <empty string>
// エンジンには正しい名前を設定する術がないので名前はありません

実際には、ほとんどの関数は名前を持っています。

“length” プロパティ

関数パラメータの数を返す別の組み込みのプロパティ “length” があります。例えば:

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

ここで、残りのパラメータはカウントされないことが分かります。

length プロパティは、他の関数上で動作する関数で内省のために使われることがあります。

例えば、下のコードでは、ask 関数は質問するための question と、呼び出すための任意の数の handler 関数を受けます。

ユーザが答えたとき、handler を呼びます。私たちは2つの種類の handler を渡すことができます:

  • 引数なし関数の場合、肯定的な回答の場合にのみ呼ばれます。
  • 引数を持つ関数の場合は、いずれのケースでも呼ばれそこから答えを得ます。

この考え方は、肯定的なケース(最も頻繁に変わるもの)のための単純な引数なしのハンドラ構文があるが、汎用のハンドラも提供できるということです。

handlers を正しい方法で呼ぶために、私たちは length プロパティを調べます:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// 肯定的な解答では、両方のハンドラが呼ばれます
// 否定的な解答では、2つ目だけが呼ばれます
ask("Question?", () => alert('You said yes'), result => alert(result));

これは、いわゆる ポリモーフィズム(polymorphism) と呼ばれる特定のケースです – 引数を型に応じて、または私たちの場合は length に応じた扱いをします。このアイデアはJavaScriptライブラリで使用されています。

カスタムプロパティ

我々は、独自のプロパティを追加することもできます。

ここでは、合計の呼び出しカウントを追跡するための counter プロパティを追加します:

function sayHi() {
  alert("Hi");

  // 何度実行したかカウントしましょう
  sayHi.counter++;
}
sayHi.counter = 0; // 初期値

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // 2度呼ばれました
プロパティは変数ではありません

sayHi.counter = 0 のような関数に割り当てられたプロパティは、関数の中でローカル変数 counter として定義 されません 。言い換えると、プロパティ counter と変数 let counter は2つの無関係なものです。

私たちは、関数をオブジェクトとして扱うことができ、その中にプロパティを格納することが出来ます。しかしそれはその実行には影響を与えません。変数は関数プロパティを使うことはなく、逆も然りです。これらは単なる2つの並列した言葉です。

関数プロパティは時々クロージャを置き換えることができます。例えば、関数プロパティを使うために、チャプター 変数スコープ、クロージャ のカウンターの例を書き換えてみます。:

function makeCounter() {
  // 次の代わり:
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

count は今や外部のレキシカル環境ではなく、関数の中に直接格納されています。

クロージャを使うよりも悪いのでしょうか?それとも良いのでしょうか?

主な違いは、count の値が外部変数にある場合、外部コードはそこへアクセスすることはできないということです。ネストされた関数だけがそれを変更することができます。:

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

なので、どちらを選ぶかは私たちの目的次第です。

名前付き関数式(Named Function Expression)

名前付き関数式、または略して NFE は名前を持つ関数式の用語です。

例えば、一般的な関数式を考えてみましょう:

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};

…そしてそれに名前を追加しましょう:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

追加の "func" の名前の役割は何でしょう?

最初に、私たちはまだ関数式を持っていることに注意してください。function の後に名前 "func" を追加しても、関数宣言にはなりません。なぜなら、それはまだ代入式の一部として作成されているためです。

このような名前を追加しても何も破壊しません。

関数は依然として sayHi() で利用可能です。:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

sayHi("John"); // Hello, John

そこには、名前 func に関して2つの特別なことがあります:

  1. 関数の内側から関数を参照することができます。
  2. 関数の外側からは見えません。

例えば、下の関数 sayHi は、who が提供されていない場合、"Guest" で自身を再度呼びます。:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 自身を再度呼ぶために func を使用
  }
};

sayHi(); // Hello, Guest

// しかしこれは動作しません:
func(); // Error, func は未定義(関数の外からは見えません)

なぜ func を使うのでしょう?単に sayHi ではダメなのでしょうか?

実際には、多くのケースでは次のようにできます:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

そのコードの問題は、sayHi の値が変わるかもしれないと言うことです。関数は別の変数になり、コードがエラーを吐くようになるかもしれません。:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error, 入れ子の sayHi 呼び出しはこれ以上動作しません!

関数が外部のレキシカル環境から sayHi を取得するために起こります。ローカルの sayHi がないので、外部変数が使われます。そして 呼び出しの瞬間、sayHinull です。

関数式に入れることができるオプションの名前は、この種の問題を解決するためのものです。

コードを直すためにそれを使ってみましょう:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 今はすべて問題ありません
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (ネスト呼び出しは機能します)

名前 "func" は関数ローカルなのでこれでうまく動作します。それは外部のものではありません(外部からは見えません)。仕様は常に現在の関数を参照することを保証します。

外部コードは依然として変数 sayHi または後の welcome を持っています。そして、func は “内部の関数名” であり、自身を呼び出すためのものです。

関数宣言に対して、このようなことはありません

ここで説明された “内部名” の機能は関数式でのみ利用可能で、関数宣言では利用できません。関数宣言に対して、もう1つの “内部” の名前を追加する構文はありません。

信頼できる内部名が必要なときには、それは関数宣言を名前付けされた関数式の形に書き換える理由になります。

サマリ

関数はオブジェクトです。

ここでは、私たちはそれらのプロパティをカバーしました:

  • name – 関数名です。関数定義で与えられたときだけでなく、代入やオブジェクトのプロパティに対しても存在します。
  • length – 関数定義での引数の数です。残りのパラメータはカウントされません。

もし関数が関数式として宣言され(メインコードフローではない所で)、名前を持っている場合、それは名前付けされた関数式と呼ばれます。その名前は再帰呼び出しなどをするために内部で自身を参照するために使うことができます。

また、関数は追加のプロパティをもつ場合があります。多くの知られているJavaScriptライブラリはこの機能を最大限に活用しています。

それらは “メインの” 関数を作り、それに多くの “ヘルパー” 関数を付与します。例えば、jquery ライブラリは $ という名前の関数を作ります。lodash ライブラリは関数 _ を作ります。そして、 _.clone, _.keyBy や他のプロパティを追加します(これらについてもっと知りたい場合は、docs を参照してください)。実際には、グローバル空間の汚染を少なくするために、1つのライブラリで1つのグローバル変数のみが与えられます。 これにより、名前の競合が発生する可能性が低くなります。

したがって、関数は単独で有益な仕事をすることができ、プロパティには他の機能も備えることができます。

タスク

重要性: 5

makeCounter のコードを、カウンタを減らしたり、数値を設定できるように修正してください:

  • counter() は次の数値を返します (前の通り)
  • counter.set(value)countvalue をセットします
  • counter.decrease()count を 1 減少させます

完全な使用例はサンドボックスのコードを見てください。

P.S. 現在のカウントを維持するために、クロージャまたは関数プロパティを使うことができます。もしくは両方のバリアントを書いてください。

テストと一緒にサンドボックスを開く

解法はローカル変数で count を使いますが、追加のメソッドは counter に書かれています。それらは同じ外部のレキシカル環境を共有し、また現在の count にアクセスすることができます。

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

サンドボックスでテストと一緒に解答を開く

重要性: 2

このように動作する関数 sum を書いてください。:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

P.S. ヒント: 関数のプリミティブ変換にカスタムオブジェクトを設定する必要があるかもしれません。

  1. いずれにしても 全体が動作するために、sum の結果は関数である必要があります。
  2. その関数は呼び出し間で、メモリに現在の値を覚えておく必要があります。
  3. タスクによると、関数は == が使われたとき数値になる必要があります。関数はオブジェクトなので、チャプター オブジェクトからプリミティブへの変換 で説明したように変換が行われ、私たちは数値を返す独自のメソッドを提供することができます。

コードです:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

sum 関数は実際には一度しか動作していないことに注意してください。それは関数 f を返します。

その後、それぞれの呼び出しで f はパラメータを合計 currentSum に加え、自身を返します。

f の最後の行は再帰ではありません

再帰は次のように見えます:

function f(b) {
  currentSum += b;
  return f(); // <-- 再帰呼出し
}

また、我々のケースでは単に関数を返しているだけで、呼び出しはありません:

function f(b) {
  currentSum += b;
  return f; // <-- 自身を呼ばず、返している
}

この f は次の呼び出しで使われ、必要に応じて何度でも再び自身を返却します。次に、数値または文字列として使用すると、toStringcurrentSum を返します。また、ここでは Symbol.toPrimitive または valueOf を変換に使うこともできます。

チュートリアルマップ