ホームページ >ウェブフロントエンド >jsチュートリアル >JSの有名関数表現を徹底分析_JavaScriptスキル
例 #1: 関数式の識別子が外側のスコープにリークする
例 #1: 関数式の識別子が外側のスコープにリークする
var f = function g(){}
typeof g; // "function"
名前付き関数式の識別子は囲みスコープでは使用できないと述べたことを覚えていますか?上記の例は関数オブジェクトに解決されますが、これは最も広く観察されている矛盾であり、それは、外側のスコープ (グローバル スコープである可能性もあります) を、当然のことながら、追加の識別子で汚染してしまう可能性があります。
名前付き関数式の識別子には外部スコープからアクセスできないと先ほど述べました。ただし、JScript はこの点で標準に準拠していません。上記の例では、 g は関数オブジェクトです。これは広く観察できる違いです。この方法では、周囲のスコープを追加の識別子 (おそらくグローバル スコープ) で汚染しますが、これは非常に危険です。もちろん、この汚染は、処理と追跡が難しいバグの原因である可能性があります
例 #2: 名前付き関数式は、関数宣言 AND 関数式の両方として扱われます
例 #2: 名前付き関数式は両方として扱われます - 関数宣言 AND 関数式 二重処理、関数式と関数宣言
typeof g; // "function"
var f = function g(){};
前に説明したように、関数宣言は特定の実行コンテキストで他の式よりも先に解析されます。上記の例は、JScript が名前付き関数式を実際に関数宣言としてどのように扱うかを示しています。
前に説明したように、関数宣言は特定の実行環境のすべての式より前に解釈されます。上の例は、JScript が名前付き関数式を実際に関数宣言として扱うことを示しています。実際の発言の前に彼が説明されていることがわかります。
これにより、次の例が表示されます。
これに基づいて、次の例を紹介します。
例 #3: 名前付き関数式は 2 つの DISCTINCT 関数オブジェクトを作成します!
例 #3: 名前付き関数式は 2 つの異なる関数オブジェクトを作成します。
var f = function g(){};
f === g; // false
f.expando = 'foo'; / unknown
ここが興味深いところです。むしろ、完全におかしな点です。ここでは、2 つの異なるオブジェクトを処理する必要があることがわかります。一方を拡張しても、もう一方は変更されません。たとえば、キャッシュ メカニズムを使用して f のプロパティに何かを保存し、それを操作しているのと同じオブジェクトであると考えて g のプロパティとしてアクセスしようとした場合、非常に面倒になる可能性があります。 🎜>ここからが、事態がもう少し面白くなる、あるいは完全にクレイジーになるところです。ここでは、2 つの異なるオブジェクトを処理しなければならない危険性がわかります。つまり、一方が拡張されても、他方はそれに応じて変更されません。キャッシュ機構を使用して f の属性に何かを格納し、g の属性でのみアクセスしようとすると、それらは同じオブジェクトを指していると考えられ、非常に面倒になります
もう少し複雑なものを見てみましょう。
さらに複雑な例をいくつか見てみましょう。
例 #4: 関数宣言は順番に解析され、条件ブロックの影響を受けません。
例 #4: 関数宣言は順番に解析され、条件ブロックの影響を受けません。
var f =関数 g() {
戻り値 1;
};
if (false) {
関数 g(){
戻り値 2;
}; 🎜>g(); // 2
このような例では、バグの追跡がさらに難しくなる可能性があります。まず、g は関数宣言として解析されます。 JScript は条件ブロックから独立しており、g は「デッド」 if 分岐 - function g(){ return 2 } から関数として宣言され、すべての「正規」式が評価され、f に別の新たな代入が行われます。式の評価時にブランチに入らない場合は関数オブジェクトが作成され、そのため f は最初の関数 function g(){ return 1 } を参照し続けることになります。 f 内から g を呼び出すと、まったく関係のない g 関数オブジェクトを呼び出すことになります。
このような例では、バグの追跡が非常に困難になる可能性があります。ここでの問題は非常に単純です。まず g は関数宣言として解釈され、JScript の宣言は条件ブロックから独立しているため、無効な if 分岐関数 g(){ return 2 } から g が関数として宣言されます。次に、通常の式が評価され、f が別の新しく作成された関数オブジェクトに割り当てられます。式が実行されると、if 条件分岐には入らないため、f は最初の関数 function g(){ return 1 } への参照のままになります。注意を怠って f 内で g を呼び出すと、まったく無関係な g 関数オブジェクトを呼び出すことになることは明らかです。
このようなさまざまな関数オブジェクトの混乱が、arguments.callee とどのように比較されるのか疑問に思っているかもしれません。呼び出し先は f または g を参照していますか? 見てみましょう:
これがどのように混乱しているのか疑問に思っているかもしれません。異なる関数オブジェクトを argument.callee と比較すると、オブジェクトと argument.callee を比較した結果はどうなりますか? callee は f または g を指しますか?
var f = function g(){
return [
arguments.callee == f,
arguments.callee == g
]; を見てみましょう。 >};
f(); // [true, false]
ご覧のとおり、arguments.callee は f 識別子と同じオブジェクトを参照します。これは実際には良いことです。
後で説明するように、arguments.callee が f 識別子と同じオブジェクトを参照していることがわかります。これは良いニュースです
JScript の欠陥を見ると、何が問題であるかが非常に明確になります。まず、識別子の漏洩に注意する必要があります (囲んでいるスコープを汚さないように)。次に、関数名として使用される識別子を決して参照しないでください。 g の存在を忘れていれば、どれほど多くのあいまいさが避けられたかに注目してください。常に f または argument.callee を介して関数を参照することが重要です。名前付きの式を使用する場合は、その名前を目的としてのみ使用するものとして考えてください。デバッグ目的です。最後に、NFE 宣言中に誤って作成された無関係な関数を常にクリーンアップするという利点があります。
JScript の欠点を理解したので、何を避けるべきかは非常に明確です。まず、識別子の漏洩に注意する必要があります (周囲のスコープを汚染しないように)。第 2 に、識別子を関数名として引用すべきではありません。前の例でわかるように、g は問題のある識別子です。 g の存在を忘れれば、多くのあいまいさを回避できることに注意してください。通常、最も重要なことは、f または argument.callee を通じて関数を参照することです。名前付き式を使用する場合、その名前はデバッグ目的でのみ存在することに注意してください。最後に、追加のポイントは、間違って宣言された名前付き関数式によって作成された追加関数を常にクリーンアップすることです
最後の点については少し説明が必要だと思います:
最後の点については少し説明が必要だと思います説明の詳細:
JScript のメモリ管理
JScript の不一致に慣れてきたので、これらのバグのある構造を使用するときにメモリ消費に関する潜在的な問題が発生することがわかります。
の簡単な例を見てみましょう。
おなじみ JScript と仕様の違いを理解すると、これらの問題のある構成要素を使用するときのメモリ消費に関連する潜在的な問題がわかります
var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();
この匿名呼び出し内から関数が返されたことがわかります- g 識別子を持つもの - は外側の f に割り当てられています。また、名前付き関数式が余分な関数オブジェクトを生成し、このオブジェクトが返された関数と同じではないこともわかっています。ここでのメモリの問題は、この無関係な g 関数によって引き起こされます。これは、内部関数が厄介な g 関数と同じスコープで宣言されているために発生します。明示的に g 関数への参照を中断しない限り、メモリを消費し続けることになります。匿名呼び出しから返された関数、つまり識別子として g を持つ関数が外部 f にコピーされることがわかります。また、名前付き関数式が追加の関数オブジェクトを作成し、このオブジェクトが返されたオブジェクトと同じ関数ではないこともわかります。ここでのメモリの問題は、関数を返すクロージャに文字通りキャプチャされた役に立たない g 関数が原因で発生します。これは、内部関数がクソ関数と同じスコープで宣言されているためです。 g 関数への参照を明示的に破棄しない限り、常にメモリを占有します。
var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
//関係のない関数から参照されないよう、g に null を代入します。無関係な関数はもう参照しません
g = null;
})();
も明示的に宣言していることに注意してください。 g = null 割り当てでは、準拠するクライアント (つまり、JScript 以外のクライアント) ではグローバル g 変数が作成されません。g への参照を null にすることで、g が参照するこの暗黙的に作成された関数オブジェクトをガベージ コレクターが消去できるようになります。
g を明示的に宣言したため、g=null 割り当てでは準拠クライアント (非 JScirpt エンジンなど) 用のグローバル変数は作成されないことに注意してください。 g に null 参照を与えることで、g によって参照される暗黙的に作成された関数オブジェクトをガベージ コレクションでクリーンアップできるようになります。
JScript NFE のメモリ リークに対処するとき、g を null にすることで実際にメモリが解放されることを確認するために、一連の簡単なテストを実行することにしました。問題が明らかになったとき、私は一連の簡単なテストを実行することにしました。 g 関数に null 参照を与えると実際にメモリが解放されることを確認します。
テスト
テストは単純で、名前付き関数式を介して 10000 個の関数を作成し、それらを配列に格納します。その後、参照を null にして、手順を再度繰り返します。
このテストは非常に簡単です。彼は、名前付き関数式から 1000 個の関数を作成し、配列に格納します。 1 分ほど待って、メモリ使用量がどのくらいかを確認します。 null 参照を追加し、上記のプロセスを繰り返すだけです。以下は、
function createFn(){
return (function(){
var f;
if (true) {
f = function F を使用する簡単なテスト ケースです。 (){
return 'standard';
}
}
else if (false) {
f = function F(){
return 'alternative';
}
else {
f = function F(){
return 'fallback';
}
}
// var F = null; ;
})();
}
var arr = [ ];
for (var i=0; iarr[i] =
Windows XP SP2 の Process Explorer で確認された結果は次のとおりです:
結果は Windows XP SP2 で実行され、Process Explorer を通じて取得されました
IE6:
`null` なし: 7.6K -> 20.3K
`null` あり: 7.6K -> 18K
IE7:
なし`null`: 14K -> 29.7K
with `null`: 14K -> 27K
結果は、余分な参照を明示的に null にすることでメモリを解放しましたが、消費量の違いは次のとおりです。 10,000 個の関数オブジェクトの場合、最大 3MB の差が生じる可能性があります。これは、大規模なアプリケーション、またはメモリが限られたデバイスで実行されるアプリケーションを設計するときに必ず念頭に置いておく必要があります。
結果は、役に立たない参照に null 値を与えるとメモリが解放されることを示していますが、その違いはおそらく重要ではありません。内寸の消費量はそれほど大きくないようです。 1000 個の関数オブジェクトの場合、約 3M の差が生じるはずです。ただし、大規模なアプリケーションを設計する場合、アプリケーションは長時間実行されるか、メモリが限られたデバイス (モバイル デバイスなど) で実行されることは明らかです。小さなスクリプトの場合、違いはそれほど重要ではないかもしれません。
やっとすべてが終わったと思うかもしれませんが、まだ完全に終わったわけではありません :) 言及しておきたい細かい点があります。その詳細は Safari 2.x
これで終わりだと思うかもしれませんが、まだ終わりではありません。また、Safari の古いバージョン、つまり Safari 2.x シリーズに存在するいくつかの細かい点についても説明します。これは、Safari 2.x が NFE をまったくサポートしていないという主張を Web 上で目にしたことです。そうではありません。Safari はこれをサポートしていますが、その実装にはバグがあり、それはすぐにわかります。
これは、Safari の初期バージョンでは発見されていませんでしたが、名前付き関数式のバグです。 Safari 2.x バージョンでは。しかし、Web 上で、Safari 2.x は名前付き関数式をまったくサポートしていないという主張をいくつか見かけました。これは真実ではありません。 Safari は名前付き関数式をサポートしていますが、後で説明するように、その実装にはバグがあります
特定のコンテキストで関数式に遭遇した場合、Safari 2.x はプログラムを完全に解析できません。
特定の実行環境で関数式が発生すると、Safari 2.x はプログラム全体の解釈に失敗します。エラー (SyntaxError など) はスローされません。以下のように表示されます。
(function f(){})(); // alert(1); // 前の式はプログラム全体が失敗したため、前の式ではプログラム全体が失敗するため、この行には到達しません
さまざまなテスト ケースを試した結果、名前付き関数式が代入式の一部でない場合、Safari 2.x は名前付き関数式の解析に失敗するという結論に達しました。代入式の例は次のとおりです:
いくつかのテスト ケースでテストした結果、名前付き関数式が代入式の一部ではない場合、Safari は解釈に失敗するという結論に達しました。代入式の例は次のとおりです。
// 変数宣言の一部
var f = 1;
// 単純な代入の一部
f = 2 , g = 3 ;
// return ステートメントの一部
(function(){
return (f = 2);
})(); これは、名前を付けたことを意味します関数式を代入に入れると Safari が「幸せ」になります:
これは、名前付き関数式を代入に入れると Safari が「幸せ」になることを意味します。
(function f(){}); // 失敗します。
var f = function f(){}; // 正常に動作します
(function(){
return function f(){}; // 失敗します
}) ();
(function(){
return (f = function f(){}); // 正常に動作します
})(); f(){ }, 100); // 失敗します
これは、代入なしで名前付き関数式を返すような一般的なパターンを使用できないことも意味します:
これは、次のことも意味します。代入式なしでこの通常のパターンを戻りの名前付き関数式として使用することはできません
//この Safari-2x 互換ではない構文の代わりに、
(function() {
if (featureTest) {
関数 f(){};
}
関数 f(){})(); / ここでは、もう少し冗長な代替案を使用する必要があります:
(function( ){
var f;
if (featureTest) {
f = function f(){};
}
else {
f = function f(){};
}
return f;
// またはその別のバリエーション:
(function(){
var f;
if (featureTest) {
return (f = function f(){});
}
return (f = function f(){ });
})();
/*
残念ながら、これを行うと、返される関数のクロージャーに閉じ込められる関数
への追加の参照が導入されてしまいます。余分なメモリ使用を防ぐため、
すべての名前付き関数式を 1 つの変数に割り当てることができます。
残念ながら、これを行うと関数への別の参照が導入されてしまいます。
それは返される関数のクロージャーに含まれてしまいます
>過度のメモリ使用を防ぐために、すべての名前付き関数式を別の変数
*/
var __temp;
(function(){
if (featureTest) {
return (__temp = function f(){});
return (__temp = function
})(); ..
(function(){
if (featureTest2) {
return (__temp = function g(){});
}
return (__temp = function g( ){ });
})();
/*
その後の割り当ては以前の参照を破棄するため、
過剰なメモリ使用が防止されます。 >*/
Safari 2.x の互換性が重要な場合は、「互換性のない」構造がソース内に存在しないことを確認する必要があります。これはもちろん非常に腹立たしいことですが、特に、確実に実現可能です。問題の根本がわかっている場合
Safari2.x との互換性が非常に重要である場合。互換性のない構造がコード内に現れないようにする必要があります。もちろん、これは非常に迷惑ですが、特に問題の原因がわかっている場合は、確かに可能です。
Safari 2.x で関数を NFE として宣言すると、関数表現に関数識別子が含まれないという別の小さな問題が発生することにも言及する価値があります。
Safari では、関数を名前付き関数式として宣言するときのもう 1 つの小さな問題は、関数表現に関数識別子が含まれていないことです (おそらく toString の問題)。 🎜>
// 関数表現に `g` 識別子が欠けていることに注意してください
String(g); // function () { }
すでに述べたように、これは実際には大したことではありません。前に述べたように、関数の逆コンパイルは依存すべきではありません。
これは大きな問題ではありません。前にも言ったように、関数の逆コンパイルはいかなる状況でも信頼できません。
Solution
var fn = (function(){
//関数オブジェクトを
var f に割り当てる変数を宣言します ;
// 条件付きで作成します名前付き関数
// そしてその参照を f に割り当て、その参照を `f` に割り当てます
if (true) {
f = function F(){ }
}
else if (false) {
f = function F(){ }
}
else {
f = function F (){ }
}
// `null を代入` 関数名に対応する変数に
// これにより、関数オブジェクトを作成できます ( これにより、関数オブジェクト (その識別子によって参照される) がマークされます
// ガベージ コレクションに使用できます
var F = null;
//return 条件付きで定義された関数 return a 条件付きで定義された関数
return f;
})(); 最後に、これをどのように適用するかを示します。実際の生活では、クロスブラウザーの addEvent 関数のようなものを記述するとき:
最後に、クロスブラウザーの 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(要素, イベント名, コールバック) {
element.attachEvent('on' イベント名, コールバック)
}
}
else {
fn = 関数 addEvent (要素, イベント名, コールバック) {
要素['on' イベント名] = コールバック;
}
}
// 4 )JScript によって作成された `addEvent` 関数をクリーンアップします
// 必ず代入の先頭に `var` を追加するか、
// 関数の先頭で addEvent を宣言するか、関数の先頭で `addEvent` を宣言してください
var addEvent = null;
// 5) 最後に `fn` によって参照される関数を返します
return fn;
})();
代替ソリューション
実際には代替方法が存在することを言及する価値があります。
コールスタックに記述的な名前を付ける。
名前付き関数式を使用する必要がない方法 まず、このオプションでは、
関数を定義することができます。
は、複数の関数を作成する必要がない場合にのみ実行可能です。
これは、実際にはコール スタックに記述名 (関数名) を表示する純粋な代替方法であることに注意してください。 。名前付き関数式の使用を必要としないメソッド。まず、式を使用する
の代わりに、宣言を使用して関数を定義できることがよくあります。このオプションは通常、複数の関数を作成する必要がない場合にのみ適しています。
var hasClassName = (function(){
// いくつかのプライベート変数を定義します いくつかのプライベート変数を定義します
var queue = { };
// 関数定義を使用します関数宣言を使用します
function hasClassName(element, className) {
var _className = '(?:^|\s )' className '(?:\s |$)'; _className] || (cache[_className] = new RegExp(_className));
return re.test(element.className)
// return 関数 return function
return hasClassName;
})();
これは関数定義をフォークするときには明らかに機能しません。
それにもかかわらず、
Tobie Langel が使用しているのを初めて見たのです。これが機能する方法は、関数宣言を使用してすべての関数を
事前に定義することですが、それらにわずかに異なる
識別子を与えることです:
この方法は明らかに多方向関数定義には適用されません。しかし、Tobie
Langel が使用しているのを初めて見た、興味深い方法があります。これにより、関数宣言を使用してすべての関数が定義されますが、関数宣言にはわずかに異なる識別子が与えられます。
var addEvent = (function(){
var docEl = document.documentElement;
function addEventListener(){
/* ... */
}
functionattachEvent(){
/* ... */
}
function addEventAsProperty(){
/* ... */
}
if (typeof docEl.addEventListener != '未定義') {
return addEventListener;
}
elseif (typeof docEl.attachEvent != 'unknown') {
returnattachEvent;
return addEventAsProperty
})();
これはエレガントなアプローチですが、独自の欠点もあります。まず、
異なる識別子を使用すると、名前の一貫性が失われます。それが
良いことなのか悪いことなのかはよくわかりません。同一の
名を使用することを好む人もいれば、さまざまな名前を気にしない人もいます。結局のところ、さまざまな
名が、使用される実装について「語る」ことがよくあります。たとえば、デバッガで
「attachEvent」 を見ると、それが addEvent のattachEvent ベースの実装であることがわかります。一方、
実装関連の名前にはまったく意味がない可能性があります。
API を提供し、このような方法で「内部」関数に名前を付ける場合、API のユーザーは
これらの実装の詳細すべてに簡単に迷う可能性があります。
これは比較的上品な方法ですが、独自の問題もあります。 まず、異なる標識を使用することで、去った命名の一貫性が失われます。
一部の人は 1 つのサポートの名前を使用することを望んでいますが、一部の人は名前を変更できません。通常、異なる名前は異なる結果を表します。
は、addEvent が attentEvent に基づいて実行されることを知ることができます。 さらに、関連する名前を実行するのに根本的に意図が存在しない可能性があります。 API が提供され、このメソッドで
内部の関数に名前を指定した場合、API の使用者は問題を解決する可能性があります。
この問題の解決策は、別の
命名規則を採用することかもしれません。余計な冗長さを持ち込まないように注意してください。思い浮かぶいくつかの
代替案は次のとおりです。
この問題を解決する 1 つの方法は、異なる命名名を使用することです。 🎜>
`addEvent`、`altAddEvent`、`fallbackAddEvent`
// または
`addEvent`、`addEvent2`、`addEvent3`
// または
`addEvent_addEventListener`、` addEvent_attachEvent`、`addEvent_asProperty`
このパターンのもう 1 つの軽微な問題は、メモリ
消費量の増加です。すべての関数バリエーションを事前に定義すると、
N-1 個の未使用関数が暗黙的に作成されます。ご覧のとおり、attachEvent が document.documentElement で見つかった場合、
実際には addEventListener も addEventAsProperty も使用されません。それでも、
すでにメモリを消費しています。 JScript のバグのある名前付き式と同じ
理由でメモリの割り当てが解除されることはありません。両方の関数が 1 を返すクロージャに
「閉じ込められ」ます。
このモードのもう 1 つの問題は、内部に保存されているボタンが追加されることです。すべての上部の関数を変更することによって、含まれる N-1 個の関数が構築されます。 document.documentElement で発生しますが、addEventListener と addEventAsProperty は両方とも
によって使用されていません。ただし、既に消費されている内部記憶と、Jscript の有名な表形式のバグの原因による内部記憶は、関数を返すと同時に解放されません。
この消費量の増加は、もちろんほとんど問題にはなりません。 Prototype.js などのライブラリ
がこのパターンを使用する場合、追加の関数オブジェクトは 100 ~ 200 個以上作成されることはありません
。関数がそのような方法で繰り返し (実行時)
作成されるのではなく、(読み込み
時) 1 回だけ作成される限り、おそらく心配する必要はありません。
この機能の内部メモリの使用は、Prototype.js のような場合にこのモードを使用する必要がある場合、さらに 100 ~ 200 個の関数オブジェクトを作成する必要があります。関数がこの方法で (実行中に) 繰り返し作成されない場合、ロード時にのみ作成される場合は、この問題を気にしないで済む可能性があります。