すでにご存知のとおり、JavaScript の関数は値です。
JavaScript のすべての値には型があります。関数とはどのような型ですか?
JavaScript では、関数はオブジェクトです。
関数を呼び出し可能な「アクション オブジェクト」として想像するとわかりやすいでしょう。それらを呼び出すだけでなく、プロパティの追加/削除、参照渡しなど、オブジェクトとして扱うこともできます。
関数オブジェクトには、いくつかの使用可能なプロパティが含まれています。
たとえば、関数の名前は「name」プロパティとしてアクセスできます。
関数sayHi() { アラート("こんにちは"); } アラート(sayHi.name); //こんにちはと言う
面白いことに、名前割り当てのロジックは賢いのです。また、関数が名前なしで作成された場合でも、関数に正しい名前が割り当てられ、すぐに割り当てられます。
let SayHi = function() { アラート("こんにちは"); }; アラート(sayHi.name); //sayHi (名前があります!)
デフォルト値を使用して割り当てが行われた場合にも機能します。
function f(sayHi = function() {}) { アラート(sayHi.name); //sayHi (機能します!) } f();
この仕様では、この機能を「コンテキスト名」と呼びます。関数がそれを提供しない場合、割り当てではコンテキストから判断されます。
オブジェクトのメソッドにも名前があります。
ユーザー = { にします SayHi() { // ... }、 SayBye: function() { // ... } } アラート(user.sayHi.name); //こんにちはと言う アラート(user.sayBye.name); //バイバイ
魔法なんてないけど。正しい名前がわからない場合があります。その場合、次のように name プロパティは空になります。
// 配列内に関数が作成される let arr = [function() {}]; アラート( arr[0].name ); // <空の文字列> // エンジンには正しい名前を設定する方法がないため、何もありません
ただし、実際には、ほとんどの関数には名前があります。
関数パラメータの数を返す別の組み込みプロパティ「length」があります。たとえば、次のようになります。
関数 f1(a) {} 関数 f2(a, b) {} 関数 many(a, b, ...more) {} アラート(f1.length); // 1 アラート(f2.length); // 2 アラート(多くの.長さ); // 2
ここでは、残りのパラメータがカウントされていないことがわかります。
length
プロパティは、他の関数を操作する関数のイントロスペクションに使用されることがあります。
たとえば、以下のコードでは、 ask
関数は尋ねるquestion
と、呼び出す任意の数のhandler
関数を受け入れます。
ユーザーが回答を提供すると、関数はハンドラーを呼び出します。 2 種類のハンドラーを渡すことができます。
引数なしの関数。ユーザーが肯定的な回答をした場合にのみ呼び出されます。
引数を持つ関数。どちらの場合でも呼び出され、応答を返します。
handler
正しい方法で呼び出すために、 handler.length
プロパティを調べます。
考え方としては、肯定的なケース (最も頻繁に使用されるバリアント) に対しては、引数のないシンプルなハンドラー構文を使用しながら、ユニバーサル ハンドラーもサポートできるということです。
関数 ask(質問, ...ハンドラー) { let isYes = 確認(質問); for(ハンドラーのletハンドラー) { if (handler.length == 0) { if (isYes) ハンドラー(); } それ以外 { ハンドラー(はい); } } } // 肯定的な応答の場合、両方のハンドラーが呼び出されます // 否定的な答えの場合は 2 番目のみ ask("質問?", () =>alert('はいと言った'), result =>alert(result));
これはいわゆるポリモーフィズムの特殊なケースであり、引数の型に応じて、またはこの場合はlength
に応じて引数を異なる方法で処理します。このアイデアは JavaScript ライブラリで活用できます。
独自のプロパティを追加することもできます。
ここでは、合計呼び出し数を追跡するためにcounter
プロパティを追加します。
関数sayHi() { アラート("こんにちは"); // 実行回数を数えてみましょう SayHi.counter++; } SayHi.カウンター = 0; // 初期値 こんにちは(); // こんにちは こんにちは(); // こんにちは alert( `${sayHi.counter} 回呼び出されました` ); // 2回呼び出されました
プロパティは変数ではありません
sayHi.counter = 0
のような関数に割り当てられたプロパティは、その内部でローカル変数counter
を定義しません。言い換えれば、プロパティcounter
と変数let counter
は無関係なものです。
関数をオブジェクトとして扱い、その関数にプロパティを格納することはできますが、それは関数の実行には影響しません。変数は関数のプロパティではありませんし、その逆も同様です。これらは単なる並行世界です。
関数のプロパティがクロージャに置き換わることもあります。たとえば、「変数スコープ、クロージャ」の章のカウンター関数の例を、関数プロパティを使用するように書き直すことができます。
関数 makeCounter() { // の代わりに: // カウント = 0 にします 関数カウンター() { counter.count++ を返します。 }; カウンタカウント = 0; 返却カウンター。 } let counter = makeCounter(); アラート(カウンター()); // 0 アラート(カウンター()); // 1
count
、外部の字句環境ではなく、関数に直接保存されるようになりました。
クロージャーを使用するよりも良いのでしょうか、それとも悪いでしょうか?
主な違いは、 count
の値が外部変数に存在する場合、外部コードはその値にアクセスできないことです。ネストされた関数のみがそれを変更できます。そして、それが関数にバインドされている場合、次のようなことが可能です。
関数 makeCounter() { 関数カウンター() { counter.count++ を返します。 }; カウンタカウント = 0; 返却カウンター。 } let counter = makeCounter(); カウンタカウント = 10; アラート(カウンター()); // 10
したがって、実装の選択は目的によって異なります。
名前付き関数式 (NFE) は、名前を持つ関数式を表す用語です。
たとえば、通常の関数式を考えてみましょう。
let SayHi = function(who) { alert(`こんにちは、${who}`); };
そしてそれに名前を追加します:
let SayHi = function func(who) { alert(`こんにちは、${who}`); };
ここで何か達成できたでしょうか?追加の"func"
名の目的は何ですか?
まず、まだ関数式があることに注意してください。 function
の後に"func"
名前を追加しても、関数宣言にはなりません。これは、依然として代入式の一部として作成されるためです。
このような名前を追加しても何も問題はありません。
この関数はsayHi()
として使用できます。
let SayHi = function func(who) { alert(`こんにちは、${who}`); }; SayHi("ジョン"); // こんにちは、ジョン
func
という名前には 2 つの特別な理由があります。
これにより、関数が内部でそれ自体を参照できるようになります。
関数の外部では表示されません。
たとえば、以下の関数sayHi
は、 who
指定されていない場合、 "Guest"
を使用して自分自身を再度呼び出します。
let SayHi = function func(who) { もし(誰が){ alert(`こんにちは、${who}`); } それ以外 { func("ゲスト"); // func を使用して自身を再呼び出しします } }; こんにちは(); // こんにちは、ゲストさん // しかし、これはうまくいきません: 関数(); // エラー、関数が定義されていません (関数の外部では表示されません)
なぜfunc
使うのでしょうか?ネストされた呼び出しには、 sayHi
使用するだけでよいでしょうか?
実際、ほとんどの場合、次のことが可能です。
let SayHi = function(who) { もし(誰が){ alert(`こんにちは、${who}`); } それ以外 { SayHi("ゲスト"); } };
このコードの問題は、 sayHi
外側のコードで変更される可能性があることです。代わりに関数が別の変数に割り当てられると、コードでエラーが発生し始めます。
let SayHi = function(who) { もし(誰が){ alert(`こんにちは、${who}`); } それ以外 { SayHi("ゲスト"); // エラー:sayHi は関数ではありません } }; ようこそ = 言ってみましょう; SayHi = null; いらっしゃいませ(); // エラー。ネストされたsayHi呼び出しはもう機能しません。
これは、関数が外部の語彙環境からsayHi
取得するために発生します。ローカルのsayHi
がないため、外部変数が使用されます。そして、呼び出しの時点では、外側のsayHi
はnull
です。
関数式に追加できるオプションの名前は、まさにこの種の問題を解決することを目的としています。
これを使用してコードを修正しましょう。
let SayHi = function func(who) { もし(誰が){ alert(`こんにちは、${who}`); } それ以外 { func("ゲスト"); // これで大丈夫です } }; ようこそ = 言ってみましょう; SayHi = null; いらっしゃいませ(); // こんにちは、ゲスト (ネストされた呼び出しは機能します)
"func"
という名前は関数ローカルであるため、機能するようになりました。それは外から撮られたものではありません(そしてそこには見えません)。仕様では、常に現在の関数を参照することが保証されています。
外側のコードには変数sayHi
またはwelcome
がまだあります。 func
「内部関数名」であり、関数がそれ自体を確実に呼び出すための方法です。
関数宣言にはそのようなものはありません
ここで説明する「内部名」機能は関数式でのみ使用でき、関数宣言では使用できません。関数宣言の場合、「内部」名を追加するための構文はありません。
信頼できる内部名が必要な場合、それが関数宣言を名前付き関数式フォームに書き換える理由になることがあります。
関数はオブジェクトです。
ここではそれらのプロパティについて説明しました。
name
– 関数名。通常は関数定義から取得されますが、何もない場合、JavaScript はコンテキスト (割り当てなど) から推測しようとします。
length
– 関数定義内の引数の数。残りのパラメータはカウントされません。
関数が (メイン コード フローではなく) 関数式として宣言され、名前が付けられている場合、その関数は名前付き関数式と呼ばれます。この名前は、再帰呼び出しなどのために内部でそれ自体を参照するために使用できます。
また、関数には追加のプロパティが含まれる場合があります。多くのよく知られた JavaScript ライブラリは、この機能をうまく活用しています。
彼らは「メイン」関数を作成し、それに他の多くの「ヘルパー」関数を付加します。たとえば、jQuery ライブラリは$
という名前の関数を作成します。 lodash ライブラリは関数_
を作成し、それに_.clone
、 _.keyBy
およびその他のプロパティを追加します (詳細については、ドキュメントを参照してください)。実際、彼らはグローバル空間の汚染を軽減するためにこれを行っているので、単一のライブラリはグローバル変数を 1 つだけ提供します。これにより、名前の競合が発生する可能性が低くなります。
したがって、関数はそれ自体で便利な仕事をすることができ、またプロパティで他の多くの機能を実行することもできます。
重要度: 5
カウンターも減少させて数値を設定できるように、 makeCounter()
のコードを変更します。
counter()
(前と同様に)次の数値を返す必要があります。
counter.set(value)
カウンターをvalue
に設定する必要があります。
counter.decrease()
カウンターを 1 減らす必要があります。
完全な使用例については、サンドボックス コードを参照してください。
PS 現在のカウントを保持するには、クロージャまたは関数プロパティを使用できます。または、両方のバリエーションを記述します。
テストを含むサンドボックスを開きます。
この解決策ではローカル変数でcount
を使用しますが、加算メソッドはcounter
に直接書き込まれます。これらは同じ外部語彙環境を共有し、現在のcount
にアクセスすることもできます。
関数 makeCounter() { カウント = 0 とします。 関数カウンター() { count++ を返します。 } counter.set = 値 => カウント = 値; counter.decrease = () => count--; 返却カウンター。 }
サンドボックス内のテストを含むソリューションを開きます。
重要性: 2
次のように機能する関数sum
を記述します。
合計(1)(2) == 3; // 1 + 2 合計(1)(2)(3) == 6; // 1 + 2 + 3 合計(5)(-1)(2) == 6 合計(6)(-1)(-2)(-3) == 0 合計(0)(1)(2)(3)(4)(5) == 15
PS ヒント: 関数に対してカスタム オブジェクトからプリミティブへの変換をセットアップする必要がある場合があります。
テストを含むサンドボックスを開きます。
とにかく全体が機能するには、 sum
の結果が関数でなければなりません。
この関数は呼び出しの間に現在の値をメモリに保持する必要があります。
タスクによると、 ==
で使用される場合、関数は数値になる必要があります。関数はオブジェクトであるため、「オブジェクトからプリミティブへの変換」の章で説明されているように変換が行われ、数値を返す独自のメソッドを提供できます。
コードは次のようになります。
関数 sum(a) { currentSum = a; とします。 関数 f(b) { currentSum += b; fを返します; } f.toString = function() { currentSum を返します。 }; fを返します; } アラート( sum(1)(2) ); // 3 アラート( sum(5)(-1)(2) ); // 6 アラート( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15
sum
関数は実際には 1 回しか機能しないことに注意してください。関数f
返します。
その後、後続の各呼び出しで、 f
そのパラメーターを合計currentSum
に追加し、それ自体を返します。
f
の最後の行には再帰はありません。
再帰は次のようになります。
関数 f(b) { currentSum += b; f()を返します; // <-- 再帰呼び出し }
そして私たちの場合、関数を呼び出さずに単に関数を返します。
関数 f(b) { currentSum += b; fを返します; // <-- 自分自身を呼び出さず、自分自身を返します }
このf
次の呼び出しで使用され、必要なだけ何度でもそれ自体を返します。次に、数値または文字列として使用すると、 toString
currentSum
返します。ここで変換にSymbol.toPrimitive
またはvalueOf
使用することもできます。
サンドボックス内のテストを含むソリューションを開きます。