2022年5月3日

関数オブジェクト、NFE

すでに知っているように、JavaScriptの関数は値です。

JavaScriptのすべての値には型があります。関数の型は何ですか?

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

関数を想像する良い方法は、呼び出し可能な「アクションオブジェクト」として考えることです。それらを呼び出すだけでなく、オブジェクトとして扱うこともできます。プロパティの追加/削除、参照による渡しなどです。

“name” プロパティ

関数オブジェクトはいくつかの使用可能なプロパティを含んでいます。

たとえば、関数の名前は "name" プロパティとしてアクセスできます。

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

alert(sayHi.name); // sayHi

面白いことに、名前の割り当てロジックはスマートです。関数が名前なしで作成され、すぐに代入された場合でも、正しい名前を割り当てます。

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

alert(sayHi.name); // sayHi (there's a name!)

代入がデフォルト値経由で行われた場合も機能します。

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

f();

仕様では、この機能は「コンテキスト名」と呼ばれています。関数が名前を提供しない場合、代入時にコンテキストから判断されます。

オブジェクトメソッドにも名前があります。

let user = {

  sayHi() {
    // ...
  },

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

}

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

しかし、魔法ではありません。正しい名前を判断する方法がない場合があります。その場合、nameプロパティは空になります。例を次に示します。

// function created inside array
let arr = [function() {}];

alert( arr[0].name ); // <empty string>
// the engine has no way to set up the right name, so there is none

実際には、ほとんどの関数に名前があります。

“length” プロパティ

もう1つの組み込みプロパティ「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 関数を受け取ります。

ユーザーが回答を提供すると、関数はハンドラを呼び出します。2種類のハンドラを渡すことができます。

  • ユーザーが肯定的な回答をした場合にのみ呼び出される引数なしの関数。
  • どちらの場合も呼び出され、回答を返す引数付きの関数。

handler を正しい方法で呼び出すために、handler.length プロパティを調べます。

アイデアは、肯定的なケース(最も頻繁なバリアント)のためのシンプルな引数なしのハンドラ構文を用意する一方で、ユニバーサルハンドラもサポートできるということです。

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

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

}

// for positive answer, both handlers are called
// for negative answer, only the second one
ask("Question?", () => alert('You said yes'), result => alert(result));

これは、いわゆる ポリモーフィズム の特定のケースです。型、またはこのケースでは length に応じて引数を異なる方法で処理します。このアイデアは、JavaScriptライブラリでよく使用されます。

カスタムプロパティ

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

ここでは、合計呼び出し数を追跡する counter プロパティを追加します。

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

  // let's count how many times we run
  sayHi.counter++;
}
sayHi.counter = 0; // initial value

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

alert( `Called ${sayHi.counter} times` ); // Called 2 times
プロパティは変数ではない

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

関数をオブジェクトとして扱い、その中にプロパティを格納できますが、それは関数の実行には影響しません。変数は関数のプロパティではなく、その逆も同様です。これらは単に並行した世界です。

関数プロパティはクロージャの代わりにできることがあります。たとえば、変数のスコープ、クロージャの章のカウンタ関数の例を、関数プロパティを使用して書き換えることができます。

function makeCounter() {
  // instead of:
  // 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

したがって、実装の選択は私たちの目的に依存します。

名前付き関数式

名前付き関数式、または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"); // use func to re-call itself
  }
};

sayHi(); // Hello, Guest

// But this won't work:
func(); // Error, func is not defined (not visible outside of the function)

なぜ 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, the nested sayHi call doesn't work any more!

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

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

それを使用してコードを修正してみましょう。

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // Now all fine
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (nested call works)

名前 "func" は関数ローカルであるため、これで動作します。それは外部から取得されず(そこには表示されません)。仕様では、常に現在の関数を参照することが保証されています。

外部コードには、変数 sayHi または welcome がまだあります。そして func は「内部関数名」であり、関数が確実に自身を呼び出すための方法です。

関数宣言にはそのようなものはない

ここで説明した「内部名」機能は、関数式でのみ使用可能であり、関数宣言では使用できません。関数宣言の場合、「内部」名を追加する構文はありません。

信頼できる内部名が必要な場合、関数宣言を名前付き関数式の形式に書き換える理由になります。

まとめ

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

ここでは、それらのプロパティについて説明しました。

  • name – 関数名。通常は関数の定義から取得されますが、ない場合は、JavaScriptはコンテキスト(たとえば代入)から推測しようとします。
  • length – 関数定義の引数の数。残余引数はカウントされません。

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

また、関数は追加のプロパティを持つことができます。多くの有名なJavaScriptライブラリは、この機能を大いに活用しています。

それらは「メイン」関数を作成し、他の多くの「ヘルパー」関数をそれにアタッチします。たとえば、jQuery ライブラリは $ という名前の関数を作成します。lodash ライブラリは関数 _ を作成し、次に _.clone_.keyBy、その他のプロパティをそれに追加します(詳細を知りたい場合は、ドキュメントを参照してください)。実際、それらはグローバルスペースの汚染を軽減するためにそれを行い、1つのライブラリが1つのグローバル変数のみを与えるようにします。これにより、名前の競合の可能性が軽減されます。

したがって、関数はそれ自体で役立つジョブを実行でき、プロパティで他の多くの機能を実行することもできます。

課題

重要度: 5

makeCounter() のコードを、カウンタが減少や数を設定できるように変更します。

  • counter() は(以前と同じように)次の数を返す必要があります。
  • counter.set(value) はカウンタを value に設定する必要があります。
  • counter.decrease() はカウンタを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 関数は実際には1回しか機能しないことに注意してください。関数 f を返します。

次に、後続の呼び出しごとに、f はそのパラメータを合計 currentSum に追加し、自身を返します。

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

これが再帰の例です

function f(b) {
  currentSum += b;
  return f(); // <-- recursive call
}

そして、このケースでは、関数を呼び出すのではなく、関数を返すだけです

function f(b) {
  currentSum += b;
  return f; // <-- does not call itself, returns itself
}

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

サンドボックスでテスト付きの解答を開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点があれば、コメントする代わりにGitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入するには、<code> タグを使用し、複数行の場合は <pre> タグで囲み、10行以上の場合はサンドボックスを使用してください(plnkr, jsbin, codepen…)