JavaScriptを深く理解するシリーズ(2) 名前付き関数式の秘密を解明_JavaScriptスキル

WBOY
リリース: 2016-05-16 17:57:11
オリジナル
944 人が閲覧しました

まえがき
名前付き関数の式について深く議論を重ねている人がインターネット上に見当たりません。そのため、インターネット上ではさまざまな誤解が生じています。この記事では、JavaScript の名前付き関数について原理と実践の両面から説明します。式の利点と欠点。
簡単に言うと、名前付き関数式のユーザーは 1 人だけです。これは、デバッグまたはプロファイラー分析中に関数の名前を記述することになります。関数名を使用して再帰を実装することもできますが、それはすぐにわかります。実際には非現実的です。もちろん、デバッグを気にしないのであれば、心配する必要はありません。そうでない場合でも、互換性について知りたい場合は、読み続けてください。
まず関数式とは何かを見てから、最新のデバッガーがこれらの式をどのように処理するかについて説明します。すでにこれに精通している場合は、このセクションを飛ばしてください。
関数式と関数宣言
ECMAScript では、関数を作成する最も一般的な 2 つの方法は関数式と関数宣言です。ECMA 仕様では 1 つの点のみが明確になっているため、この 2 つの違いは少しわかりにくいです。宣言には識別子 (Identifier) (一般に関数名と呼ばれるもの) が必要ですが、関数式ではこの識別子を省略できます:
関数宣言:
function 関数名 (パラメーター: オプション) { Function body}
関数式:
関数 関数名 (オプション) (パラメーター: オプション) { 関数本体}
したがって、関数名が宣言されていない場合は、式でなければならないことがわかります。しかし、関数名が宣言されている場合、それが関数宣言であるか関数式であるかをどのように判断するのでしょうか? ECMAScript はコンテキストによって区別されます。関数 foo(){} が代入式の一部である場合、関数 foo(){} が関数本体内に含まれている場合、またはプログラムの先頭にある場合、それは関数式です。関数宣言。

コードをコピー コードは次のとおりです。

function foo(){} // 宣言プログラムの一部であるため、
var bar = function foo(){}; // 式、代入式の一部であるため、
new function bar(){}; // 式、 is new Expression
(function(){
function bar(){} // 関数本体の一部であるため宣言
})();

関数式はあまり一般的ではありませんが、括弧で囲まれています (function foo(){})。これらが式である理由は、括弧 () がグループ化演算子であり、その中に式を含めることができるためです。いくつかの例:
コードをコピー コードは次のとおりです:

function foo(){} // 関数宣言
(function foo(){}); // 関数式: グループ化演算子に含まれます
try {
(var x = 5); // グループ化演算子のみが含まれますが、 not ステートメント: var ここにステートメントがあります
} catch(err) {
// SyntaxError
}

eval を使用するとき、JSON が実行されるとき、それを考えることができます。 JSON 文字列は通常、eval('(' json ')') のように括弧で囲まれます。その理由は、グループ化演算子、つまりこの括弧のペアが、パーサーに JSON の中括弧を式に解析させるためです。コードブロックの代わりに。
コードをコピー コードは次のとおりです:

try {
{ "x" : 5 } ; // "{" と "}" はコード ブロックに解析されます
} catch(err) {
// SyntaxError
}
({ "x": 5 }); // グループ化 演算子は、"{" と "}" をオブジェクト リテラルとして強制的に解析します

式と宣言の間には非常に微妙な違いがあります。まず、関数宣言は関数宣言よりも前に解析され評価されます。宣言がコードの最後の行にある場合でも、値は同じスコープ内の最初の式の前に解析および評価されます。次の例を参照してください。ただし、アラートが実行されるとき、fn はすでに定義されています:
コードをコピーします コードは次のとおりです:

alert(fn());
function fn() {
return 'Hello world!'
}

さらに、もう 1 つあります。関数宣言は条件ステートメント内で使用できますが、標準化されていないため、環境が異なれば実行結果も異なる可能性があるため、この場合は関数式を使用するのが最善です:
コードをコピー コードは次のとおりです:

// これはやめてください!
// 一部のブラウザは最初の関数を返す一方、一部のブラウザは 2 番目の関数を返すため、
if (true) {
function foo() {
return ' first'
}
}
else {
function foo() {
return 'first';
}
}
foo(); // 逆に、この場合、関数式
var foo;
if (true) {
foo = function() {
return 'first'>}; を使用する必要があります。
else {
foo = function() {
return
}
}
foo();



関数宣言は、プログラムまたは関数本体内でのみ使用できます。構文的には、ブロック ({ ... }) 内 (たとえば、if、while、または for ステートメント内) にこれらを使用することはできません。 Block には Statement ステートメントのみを含めることができ、関数宣言などのソース要素は含めることができないためです。一方、ルールを詳しく見てみると、式がブロック内に現れる唯一の可能性があるのは、それが式ステートメントの一部である場合であることがわかります。ただし、仕様では、式ステートメントをキーワード関数で始めることはできないと明確に述べられています。これが実際に意味するのは、関数式を Statement ステートメントまたはブロック内に使用できないということです (ブロックは Statement ステートメントで構成されているため)。

関数ステートメント
ECMAScript の構文拡張のうち、関数ステートメントは現在、Gecko ベースのブラウザのみが実装しているため、以下の例では学習目的でのみ使用されているようです。一般に、これは推奨されません (Gecko ブラウザ用に開発している場合を除く)。
1. 一般的なステートメントが使用できる場合は、ブロック ブロックを含めて関数ステートメントも使用できます。



コードをコピーコードは次のとおりです。 if (true) {
function f(){ }
}
else {
function f(){ }
}


2. 関数ステートメントは、条件付き実行を含む他のステートメントと同様に解析できます。


コードをコピーコードは次のとおりです。 if (true) {
function foo(){ return 1; }
}
else {
function foo(){ return 2 ; }
}
foo(); // 1
// 注: 他のクライアントは foo を関数宣言に解析します
// したがって、2 番目の foo は最初の foo を上書きします。結果は 1 ではなく 2 を返します


3. 関数ステートメントは変数の初期化中に宣言されず、関数式と同じように実行時に宣言されます。ただし、関数ステートメントの識別子が宣言されると、その識別子は関数のスコープ全体で有効になります。識別子の有効性により、関数ステートメントが関数式と異なります (名前付き関数式の具体的な動作については、次のセクションで説明します)。


コードをコピーします コードは次のとおりです: // この時点で、foo は宣言なし
typeof foo; // "未定義"
if (true) {
// ここに入力すると、スコープ全体で foo が宣言されます
function foo(){ return 1; 🎜>}
else {
// ここには決して来ないので、ここでは foo は宣言されません
function foo(){ return 2; }
}
typeof foo; "


ただし、次の標準準拠のコードを使用して、上記の例の関数ステートメントをモデル化できます:



コードをコピー foo = function foo(){ return 1; > }
else {
foo = function foo() { return 2;
}


4. 関数ステートメントおよび関数宣言 (または名前付き関数式) の文字文字列表現は識別子も含めて同様です:



コードをコピー
コードは次のとおりです: if (true ) { function foo(){ return 1; } }
String(foo) { return 1; }もう 1 つは、初期の Gecko ベースの実装 (Firefox 3 およびそれ以前のバージョン) にバグがあり、関数ステートメントが関数宣言を誤ってオーバーライドするというものです。これらの初期の実装では、関数ステートメントがどういうわけか関数宣言をオーバーライドできませんでした:




コードをコピーします

コードは次のとおりです:

// 関数宣言
function foo(){ return 1; }
if (true) {
//
function foo(){ return 2 を function ステートメントで書き換えます ; }
}
foo(); // FF3 以前の場合は 1 を返し、FF3.5 以降の場合は 2 を返します
// ただし、関数式が前にある場合は役に立ちません
var foo = function(){ return 1; };
if (true) {
function foo(){ return 2; }
}
foo(); 🎜>
上記の例は特定のブラウザーでのみサポートされているため、特別な機能を備えたブラウザーで開発しない限り、これらの例を使用しないことをお勧めします。
名前付き関数式
Web 開発における一般的なパターンは、パフォーマンスの最適化を達成するために特定の特性のテストに基づいて関数定義を偽装することですが、この方法も同様です。スコープでは、基本的に関数式を使用する必要があります:

コードをコピー コードは次のようになります:
//このコードは、Garrett Smith の APE Javascript ライブラリ (http://dhtmlkitchen.com/ape/) からのものです。
var contains = (function() {
var docEl = document.documentElement;
if (typeof docEl.比較文書位置 != '未定義') {
return function(el, b) {
return (el.compareDocumentPosition(b) & 16)
}
; else if (typeof docEl.contains != 'unknown') {
return function(el, b) {
return el !== b && el .contains(b);
}
return function(el, b) {
if (el === b) return false;
while (el != b && (b = b.parentNode) != null); >return el === b;
};
})();


前述の例では、名前が必要です。 foo(){}; は有効な名前付き関数式ですが、覚えておくべきことが 1 つあります。仕様では、識別子は外側のスコープでは有効ではないと規定されているため、この名前は新しく定義された関数のスコープ内でのみ有効です。 🎜>


コードをコピーします
コードは次のとおりです: var f = function foo(){ return typeof foo; // foo は内部スコープ内で有効です } // foo は外部で使用されると非表示になります
typeof foo; // "function"


これは要件なので、名前付き関数式はどのように使用されるのでしょうか?なぜ名前なのでしょうか?
最初に述べたように、名前を付けるとデバッグ プロセスがより便利になります。デバッグするときに、コール スタック内の各項目にそれを説明するための独自の名前があれば、デバッグ プロセスがより効果的になるからです。 、感覚が違います。
デバッガーの関数名
関数に名前がある場合、デバッガーはデバッグ中に呼び出しスタックにその名前を表示します。一部のデバッガ (Firebug) は、その関数を使用する関数と同じ役割を持つように関数に名前を付けて表示することがありますが、通常、これらのデバッガは名前付けのための単純なルールをインストールするだけです。例:



コードをコピー


コードは次のとおりです:
function foo(){ return bar(); } function bar(){ return baz();
function baz(){
}
foo();
// ここでは、名前付きの 3 つの関数宣言を使用します
// したがって、デバッガーがデバッガー ステートメントに移動すると、Firebug 呼び出しスタックは次のようになります。
// 名前が明確であるため、
baz
bar
foo
expr_test.html()


コールスタック情報を表示すると、foo が bar を呼び出し、bar が baz を呼び出していることが明確にわかります。 (そして foo 自体は expr_test.html ドキュメントのグローバル スコープで呼び出されます) ただし、もう 1 つ優れた場所があります。つまり、先ほど述べた Firebug は、匿名式に名前を付ける関数です:




コードをコピー

コードは次のとおりです。

function foo() { return bar() var bar = function(){ return baz(); function baz(){ } foo(); stack baz
bar() //見えますか?
foo
expr_test.html()


その後、関数式がもう少し複雑になると、デバッガはそれほど賢くなく、コールスタックでしか確認できません。マーク:




コードをコピー


コードは次のとおりです:

function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function( ){
return baz();
}
else if (window.attachEvent) {
return function() {
return baz(); ;
}
})();
関数 baz(){
}
foo()// 呼び出しスタック
baz
(?)() // これは疑問符です
foo
expr_test.html()


また、関数を複数の変数に代入する場合、コマンドも表示されます憂鬱な質問:



コードをコピー
コードは次のとおりです: function foo(){ 戻り baz();
var bar = function(){
デバッガー;
var baz = bar = function(); >alert('spoofed');
foo()
// 呼び出しスタック:
bar()
foo
expr_test.html()


この時点で、コールスタックは foo が bar を呼び出していることを示していますが、実際にはそうではありません。この問題の理由は、baz と、alert('spoofed') を含む別の関数が参照交換を行っているためです。引き起こされた。
最終的な分析として、最も委任された方法は、関数式に名前を付けること、つまり名前付き関数式を使用することです。名前付き式を使用して上記の例を書き直してみましょう (すぐに呼び出される式ブロックで返される 2 つの関数の名前は bar であることに注意してください):




コードをコピーします

コードは次のとおりです。

function foo(){ return bar(); } var bar = (function() { if (window.addEventListener) { return 関数 bar(){ return baz();
}
else if (window.attachEvent) {
return 関数bar() {
return baz();
}
})();
関数 baz(){
}
foo ();
// クリアなコールスタック情報が再び表示されました!
baz
bar
foo
expr_test.html()


OK、別のトリックを学びましたか?しかし、興奮しすぎる前に、珍しい JScript を見てみましょう。
JScript のバグ
さらに悪いことに、IE の JScript の ECMAScript 実装は名前付き関数式をひどく混乱させており、多くの人が名前付き関数式に反対する原因となっており、最新バージョン (IE8、バージョン 5.8 が使用されています) でも依然として次の点が残っています。問題。
IE が実装時に犯した間違いを見てみましょう。よく言われるように、敵を知ることによってのみ無敵になれます。次の例を見てみましょう:
例 1: 関数式の識別子が外部スコープに漏れる




コードをコピー

コードは次のとおりです:


var f = function g(){};
typeof g; // "function"
前述したとおり上記の名前付け関数式の識別子は外部スコープでは無効ですが、JScript は明らかにこの仕様に違反しています。上記の例の識別子 g は関数オブジェクトに解析されており、見つけるのが難しいバグが多くあります。この理由のためです。 例 2: 名前付き関数式を関数宣言と関数式として同時に処理します
コードをコピー


コードは次のとおりです

typeof g; // "function"
var f = function g(){};
機能環境では、関数の宣言が優先されます。上の例は、JScript が実際の宣言の前に g を解析するため、名前付き関数式を実際に関数宣言として扱うことを示しています。 この例は次の例につながります。 例 3: 名前付き関数式は、2 つのまったく異なる関数オブジェクトを作成します。
コードをコピー


コードは次のとおりです:


var f = function g(){} ;
f === g; // false
f.expando = 'foo'; // 未定義

コードをコピー


コードは次のとおりです:

var f = function g() {
return 1;
if (false) {
f = function g(){
return 2; 🎜>};
}
g(); // 2


このバグは見つけるのが非常に困難ですが、バグの原因は非常に単純です。まず、g は関数宣言として解析されます。JScript の関数宣言は条件付きコード ブロックの対象ではないため、この厄介な if 分岐では、g は別の関数 function g(){ return 2 } として扱われ、再度宣言されただけです。 。次に、すべての「正規」表現が評価され、新しく作成された別のオブジェクトへの参照が f に与えられます。式が評価されるときに忌まわしい if 分岐 "" が入力されることはないため、 f は最初の関数 function g(){ return 1 } を参照し続けます。これを分析すると、問題は非常に明確になります。十分注意して f で g を呼び出すと、無関係な g 関数オブジェクトが呼び出されます。
arguments.callee を使用してさまざまなオブジェクトを比較する場合、何が違うのか疑問に思うかもしれません。


コードをコピーします コードは次のとおりです:
var f = function g( ){
return [
argument.callee == f,
arguments.callee == g
]; // [true, false]
g(); false, true]


ご覧のとおり、arguments.callee の参照は常に呼び出される関数です。これも後で説明します。興味深い例は、宣言を含まない代入ステートメントで名前付き関数式を使用していることです:



コードをコピー
コードは次のとおりです。 : (function(){ f = function f(){}; })();

コードに従ってください分析では、最初はグローバル属性 f を作成するつもりでした (名前付きの life を使用する一般的な匿名関数と混同しないように注意してください)。まず、式を関数宣言として解析します。左側の f をローカル変数として宣言し(一般的な無名関数での宣言と同じ)、関数を実行すると f が定義済みで、右側の関数 f(){} が直接宣言されます。

JScript がいかに異常であるかを理解した後、まず、外部スコープからの識別子の漏洩を防ぐ必要があります。関数名として使用される識別子は決して引用しないでください。前の例の迷惑な識別子 g を覚えておいてください。 g が存在しないようにすることができれば、どれだけ不要なトラブルを回避できるでしょうか。つまり、重要なのは常に参照することです。 f または argument.callee を介して関数に追加します。名前付き関数式を使用する場合は、その名前をデバッグ中にのみ使用してください。最後に、誤って作成された関数をクリーンアップしてください。 > 上記の最後の点については、もう一度説明する必要があります。
JScript のメモリ管理
これらの非準拠コード解析のバグを理解した後、実際に問題があることがわかります。例を見てみましょう:




コードをコピーします

コードは次のとおりです: var f = (function(){ if (true) { 戻り関数 g(){}; }
戻り関数 g(){};
})();


この匿名関数は返された関数 (識別子 g を持つ関数) を呼び出し、それを外部 f に代入することがわかります。また、名前付き関数式によって、返される関数オブジェクトと同じではない冗長な関数オブジェクトが生成されることもわかっています。したがって、この冗長な g 関数は return 関数のクロージャで終了し、メモリの問題が発生しました。これは、if ステートメント内の関数が g と同じスコープで宣言されているためです。この場合、g 関数への参照を明示的に切断しない限り、g 関数は常にメモリを占有します。



コードをコピー

コードは次のとおりです。 var f = (function(){ var f, g; if (true) { f = function g(){};
else {
f = function g(){}; >}
// g を null に設定すると、メモリを占有しなくなります
g = null
})();
g を null に設定すると、ガベージ コレクターは g によって参照される暗黙の関数を再利用します。コードを検証するために、メモリが再利用されていることを確認するためにいくつかのテストを実行します。
テスト
テストは非常に簡単で、名前付き関数式を使用して 10,000 個の関数を作成し、配列に保存するだけです。しばらく待って、これらの関数がどれだけのメモリを使用するかを確認します。次に、これらの参照を切断し、プロセスを繰り返します。以下はテスト コードです:
コードをコピー コードは次のとおりです:

function createFn (){
return (function(){
var f;
if (true) {
f = function F(){
return 'standard';
};
}
else if (false) {
f = function F(){
return
};
else {
f = function F(){
return
}
// var F = null;
}; 🎜>var arr = [ ];
for (var i=0; iarr[i] = createFn();


Windows XP で実行する場合 SP2 のタスク マネージャーで次の結果が表示されます:



コードをコピーします

コードは次のとおりです。 : IE6: `null` なし: 7.6K -> 20.3K `null` あり: 7.6K -> 18K IE7:
なし`null`: 14K -> 29.7K
with `null`: 14K -> 27K


予想どおり、ディスプレイの切断によりメモリが解放される可能性がありますが、解放されるメモリはそれほど多くありません、10,000 個の関数オブジェクトは約 3M しかリリースされません。これは一部の小さなスクリプトでは問題ありませんが、大規模なプログラムや低メモリのデバイスで長時間実行する場合には非常に必要です。

Safari 2.x にも JS の解析にバグがいくつかありますが、バージョンが低いためここでは紹介しません。知りたい場合は英語の情報をよく確認してください。 。
SpiderMonkey の特徴
ご存知のとおり、名前付き関数式の識別子は、関数のローカル スコープ内でのみ有効です。しかし、この識別子を含むローカル スコープはどのようなものになるでしょうか?実はとてもシンプルなのです。名前付き関数式が評価されると、名前が関数識別子に対応し、値がその関数に対応するプロパティを保持することを唯一の目的とする特別なオブジェクトが作成されます。このオブジェクトは、現在のスコープ チェーンの先頭に挿入されます。次に、「拡張された」スコープ チェーンが初期化関数で使用されます。
ここで非常に興味深い点の 1 つは、ECMA-262 がこの「特別な」オブジェクト (関数識別子を保持する) を定義する方法です。標準では、このオブジェクトを「new Object() 式を呼び出しているかのように」作成するように定められています。この文を文字通りに理解すると、このオブジェクトはグローバル オブジェクトのインスタンスであるはずです。ただし、これを文字通りに行う実装は 1 つだけあり、その実装は SpiderMonkey です。したがって、SpiderMonkey では、Object.prototype の拡張が関数のローカル スコープに干渉する可能性があります。




コードをコピー

コードは次のとおりです。 : Object.prototype.x = 'outer'; (function(){ var x = 'inner'; /*
関数のスコープチェーンfoo には、関数の識別子を保存するために使用される特別なオブジェクトがあります。この特別なオブジェクトは、実際には { foo: のローカル環境で解析されます。 Object.prototype から継承されるため、x はここにあります。
x の値は Object.prototype.x (外側) の値でもあります (x の役割を含みます)。 = 'inner') は解析されません
*/
(function foo(){
alert(x); // プロンプト ボックスには次のように表示されます: external
})();
})();


ただし、SpiderMonkey の後のバージョンでは、おそらくセキュリティ上の脆弱性とみなされたため、上記の動作が変更されました。言い換えれば、「特別な」オブジェクトは Object.prototype を継承しなくなりました。ただし、Firefox 3 以前を使用している場合は、この動作を「見直す」ことができます。
内部オブジェクトをグローバル Object オブジェクトとして実装するもう 1 つのブラウザは、Blackberry ブラウザです。現在、そのアクティビティ オブジェクト (Activation Object) は依然として Object.prototype を継承しています。ただし、ECMA-262 では、アクティブ オブジェクトを「new Object() 式の呼び出しと同じように」 (または NFE 識別子を保持するオブジェクトを作成するように) 作成する必要があるとは述べていません。 他の人の標準では、アクティビティ オブジェクトが標準のメカニズムであるとしか述べられていません。
次に、BlackBerry で何が起こっているかを見てみましょう:
コードをコピーします コードは次のとおりです:

Object.prototype.x = 'outer';
(function(){
var x = 'inner';
(function(){
/*
内部 スコープチェーンで x を解析するとき、最初にローカル関数のアクティブなオブジェクトが検索されます。 もちろん、このオブジェクトには x が見つかりません。
ただし、アクティブなオブジェクトは Object.prototype を継承しているため、次のオブジェクトが検索されます。の x が検索され、
Object.prototype には x の定義が含まれます。その結果、x の値は、前の例と同様に、
として解析されます。 x = 'inner'。外部関数 (アクティブ オブジェクト) のスコープは解決されません。 })();


しかし、驚くべきことに、関数内の変数は Object.prototype の既存のメンバーとさえ競合します。次のコードを見てください。

コードをコピーします

コードは次のとおりです。 (function(){ varconstructor = function(){ return 1; }; (function(){ constructor(); // 評価結果は 1
constructor = ではなく、{} (Object.prototype.constructor() の呼び出しと同等) です。 == Object.prototype.constructor; // true
toString === Object.prototype.toString; // true
// ……
})()
});


この問題を回避するには、Object.prototype で toString、valueOf、hasOwnProperty などのプロパティ名を使用しないようにします。

JScript ソリューション



コードをコピー

コードは次のとおりです: var fn = (function(){ // 関数を参照する変数を宣言します var f; // 条件付きで名前付き関数
を作成し、// その参照を f に割り当てます
if ( true) {
f = function F(){ }
}
else if (false) {
f = function F(){ }
}
else {
f = function F(){ }
}
// 関数名(識別子)に対応する変数を宣言し、null に代入
// これは実際には対応する関数によって参照される関数ですidentifier オブジェクトはマークされます。
// ガベージ コレクターがリサイクルできることを認識します。
var F = null
// 条件に従って定義された関数を返します。
return f; >}) ();


最後に、クロスブラウザーの addEvent 関数コードである上記のテクノロジーを適用するアプリケーション例を示します。 🎜> コードをコピーします


コードは次のとおりです:

// 1) 独立したスコープを使用してステートメントを含めます
var addEvent = (function( ){
var docEl = document.documentElement;
// 2) 関数を参照する変数を宣言します var fn; if (docEl.addEventListener) { // 3) 意図的に関数にわかりやすい識別子を与えます。 fn = function addEvent(element,eventName, callback) { element.addEventListener(eventName, callback, false); }
}
else if (docEl) .attachEvent) {
fn = function addEvent(要素, イベント名, コールバック) {
要素.attachEvent('on' イベント名, コールバック)
}
}
else {
fn = function addEvent(element ,eventName, callback) {
element['on'eventName] = callback;
}
}
// 4) JScript によって作成された addEvent 関数をクリアします
// 代入の前に必ず var キーワードを使用してください
// addEvent が関数の先頭で宣言されていない限り
var addEvent = null
// 5) 最後に fn return fn;
} )();


代替
実際、この説明的な名前が不要な場合は、次のような最も単純な形式で行うことができます。 (関数式の代わりに) 関数内で関数を宣言し、関数を返します:




コードをコピー


コードは次のとおりです:

var hasClassName = (function(){
// プライベート変数を定義します
var queue = { };
// 関数宣言を使用します
function hasClassName(element, className) {
var _className = '(?:^|\s )' className '(?:\s |$)';
var re = キャッシュ[_className] || (cache[_className] = new RegExp(_className) ));
return re.test(element.className)
}
// 関数
return
})();明らかに、この解決策は、複数の分岐関数定義がある場合には機能しません。ただし、可能と思われるパターンがあります。つまり、関数宣言を使用してすべての関数を事前に定義し、これらの関数に異なる識別子を指定します。


コードをコピー コードは次のとおりです: var addEvent = (function(){
var docEl = document.documentElement;
function addEventListener(){
/* .. */
}
functionattachEvent(){
/* ... */
}
function addEventAsProperty(){
/* . .. */
}
if (typeof docEl.addEventListener != '未定義') {
return addEventListener;
}
elseif (typeof docEl.attachEvent != '未定義') {
returnattachEvent;
}
return addEventAsProperty;
})();


このソリューションには欠点がないわけではありません。まず、異なる識別子の使用により、名前付けの一貫性が失われます。これが良いか悪いかは言うまでもありませんが、少なくとも十分に明確ではありません。同じ名前を使いたい人もいますが、言葉遣いの違いをまったく気にしない人もいます。しかし結局のところ、名前が異なると、使用されている実装が異なることが思い出されます。たとえば、デバッガーでattachEventを確認すると、addEventがattachEventの実装に基づいていることがわかります。もちろん、実装に基づいた名前付けが常に機能するとは限りません。 API を提供し、関数に inner という名前を付けるとします。 API ユーザーは、対応する実装の詳細によって簡単に混乱する可能性があります。
この問題を解決するには、もちろん、より合理的な名前付けスキームを考えなければなりません。しかし重要なのは、これ以上トラブルを引き起こさないことです。今考えられる解決策は次のとおりです:
'addEvent', 'altAddEvent', 'fallbackAddEvent'
// または
'addEvent', 'addEvent2', 'addEvent3'
//または
'addEvent_addEventListener'、'addEvent_attachEvent'、'addEvent_asProperty'
コードをコピー
さらに、このモードにはメモリ使用量の増加という小さな問題もあります。名前の異なる関数をあらかじめ N 個作成すると、N-1 個の関数が使用されなくなります。具体的には、document.documentElement にattachEvent が含まれている場合、addEventListener と addEventAsProperty はまったく必要ありません。ただし、これらはすべてメモリを占有します。さらに、JScript の厄介な名前付き式と同じ理由で、このメモリは決して解放されません。両方の関数は、返された関数のクロージャに「閉じ込められます」。
しかし、メモリ使用量の増加の問題は実際には大したことではありません。 Prototype.js などのライブラリがこのパターンを採用すると、さらに 100 ~ 200 の関数が作成されるだけです。これらの関数が繰り返し (実行時) に作成されるのではなく、(ロード時に) 1 回だけ作成される限り、心配する必要はありません。
WebKit の表示名
WebKit チームは、この問題に関して、やや代替的な戦略を採用しました。匿名関数と名前付き関数は表現力が非常に低いため、WebKit では「特別な」displayName 属性 (基本的には文字列) が導入されており、開発者が関数のこの属性に値を割り当てると、この属性の値がデバッグ中に使用されます。プログラマーまたはプロファイラーの関数「name」の代わりに。 Francisco Tolmasky が、この戦略の原則と実装について詳しく説明します。

将来の考慮事項
ECMAScript-262 の将来のバージョン 5 (現在はまだドラフト) では、いわゆる strict モードが導入されます。厳密モードの実装を有効にすると、言語を不安定、信頼性、安全性を低下させる機能が無効になります。セキュリティ上の理由から、arguments.callee 属性は strict モードでは「ブロック」されると言われています。したがって、厳密モードでは、arguments.callee にアクセスすると TypeError が発生します (ECMA-262 第 5 版、セクション 10.6 を参照)。ここで厳密モードについて言及する理由は、第 5 版標準に基づく実装で再帰操作を実行するために argument.callee を使用できない場合、名前付き関数式を使用する可能性が大幅に高まるためです。この意味で、名前付き関数式のセマンティクスとバグを理解することはさらに重要です。




コードをコピー コードは次のとおりです:

// 以前は、arguments.callee
(function(x) {
if (x return x * argument.callee( x - 1);
})(10);
// ただし、厳密モードでは、名前付き関数式を使用できます。 1) return 1;
return x * fastial(x - 1);
})(10); または、柔軟性の低い関数宣言を使用します
x) {
if (x return x *階乗(x - 1)
}
階乗(10); >謝辞
Richard Cornford は、JScript の名前付き関数式のバグを最初に説明しました。 Richard はこの投稿で言及したバグのほとんどを説明しているので、彼の説明をチェックすることを強くお勧めします。また、2003 年に comp.lang.javascript フォーラムで NFE の問題について言及し、議論してくれた Yann-Erwan Perio と Douglas Crockford にも感謝したいと思います。
ジョン-デイビッド・ダルトンは「最終解決策」について素晴らしい提案をしています。
トビー・ランギのアイデアは、私が「The Alternative」で使用しました。
Garrett Smith と Dmitry Soshnikov は、この記事のさまざまな側面を追加および修正しました。

英語原文: http://kangax.github.com/nfe/
参考翻訳: 接続アクセス (SpiderMonkey's Quirks 以降の章については、この記事を参照してください)
同期と推奨事項
この記事 ディレクトリインデックスに同期: JavaScript シリーズを深く理解する
JavaScript シリーズを深く理解するオリジナル記事、翻訳記事、再版記事、その他の種類の記事が役に立ちましたら、お勧めください。おじさんの執筆意欲を高めるためにサポートします。
関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート