JavaScriptでグローバル変数を回避する方法は?


84

グローバル変数はベストプラクティスではないことは誰もが知っています。しかし、それらなしでコーディングするのが難しい場合がいくつかあります。グローバル変数の使用を避けるためにどのような手法を使用していますか?

たとえば、次のシナリオでは、グローバル変数をどのように使用しないのでしょうか。

JavaScriptコード:

var uploadCount = 0;

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        startUpload();
        return false;
    }
}

function startUpload() {
    var fil = document.getElementById("FileUpload" + uploadCount);

    if (!fil || fil.value.length == 0) {
        alert("Finished!");
        document.forms[0].reset();
        return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
}

関連するマークアップ:

<iframe src="test.htm" name="postHere" id="postHere"
  onload="uploadCount++; if(uploadCount > 1) startUpload();"></iframe>

<!-- MUST use inline JavaScript here for onload event
     to fire after each form submission. -->

このコードは、複数のが含まれるWebフォームから取得されます<input type="file">。大量のリクエストを防ぐために、ファイルを一度に1つずつアップロードします。これを行うには、iframeにPOSTを実行し、iframeのオンロードを起動する応答を待ってから、次の送信をトリガーします。

この例に具体的に答える必要はありません。グローバル変数を回避する方法を考えることができない状況を参照するために提供しているだけです。


3
即時呼び出し関数式(IIFE)を使用する詳細については、codearsenal.net

回答:


69

最も簡単な方法は、コードをクロージャでラップし、グローバルに必要な変数のみをグローバルスコープに手動で公開することです。

(function() {
    // Your code here

    // Expose to global
    window['varName'] = varName;
})();

Crescent Freshのコメントに対処するには、シナリオからグローバル変数を完全に削除するために、開発者は質問で想定されている多くのことを変更する必要があります。次のようになります。

Javascript:

(function() {
    var addEvent = function(element, type, method) {
        if('addEventListener' in element) {
            element.addEventListener(type, method, false);
        } else if('attachEvent' in element) {
            element.attachEvent('on' + type, method);

        // If addEventListener and attachEvent are both unavailable,
        // use inline events. This should never happen.
        } else if('on' + type in element) {
            // If a previous inline event exists, preserve it. This isn't
            // tested, it may eat your baby
            var oldMethod = element['on' + type],
                newMethod = function(e) {
                    oldMethod(e);
                    newMethod(e);
                };
        } else {
            element['on' + type] = method;
        }
    },
        uploadCount = 0,
        startUpload = function() {
            var fil = document.getElementById("FileUpload" + uploadCount);

            if(!fil || fil.value.length == 0) {    
                alert("Finished!");
                document.forms[0].reset();
                return;
            }

            disableAllFileInputs();
            fil.disabled = false;
            alert("Uploading file " + uploadCount);
            document.forms[0].submit();
        };

    addEvent(window, 'load', function() {
        var frm = document.forms[0];

        frm.target = "postMe";
        addEvent(frm, 'submit', function() {
            startUpload();
            return false;
        });
    });

    var iframe = document.getElementById('postHere');
    addEvent(iframe, 'load', function() {
        uploadCount++;
        if(uploadCount > 1) {
            startUpload();
        }
    });

})();

HTML:

<iframe src="test.htm" name="postHere" id="postHere"></iframe>

にインラインイベントハンドラーは必要ありません<iframe>。このコードを使用すると、ロードごとに起動します。

ロードイベントについて

これは、インラインonloadイベントが必要ないことを示すテストケースです。これは、同じサーバー上のファイル(/emptypage.php)を参照することに依存します。そうでない場合は、これをページに貼り付けて実行できるはずです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>untitled</title>
</head>
<body>
    <script type="text/javascript" charset="utf-8">
        (function() {
            var addEvent = function(element, type, method) {
                if('addEventListener' in element) {
                    element.addEventListener(type, method, false);
                } else if('attachEvent' in element) {
                    element.attachEvent('on' + type, method);

                    // If addEventListener and attachEvent are both unavailable,
                    // use inline events. This should never happen.
                } else if('on' + type in element) {
                    // If a previous inline event exists, preserve it. This isn't
                    // tested, it may eat your baby
                    var oldMethod = element['on' + type],
                    newMethod = function(e) {
                        oldMethod(e);
                        newMethod(e);
                    };
                } else {
                    element['on' + type] = method;
                }
            };

            // Work around IE 6/7 bug where form submission targets
            // a new window instead of the iframe. SO suggestion here:
            // http://stackoverflow.com/q/875650
            var iframe;
            try {
                iframe = document.createElement('<iframe name="postHere">');
            } catch (e) {
                iframe = document.createElement('iframe');
                iframe.name = 'postHere';
            }

            iframe.name = 'postHere';
            iframe.id = 'postHere';
            iframe.src = '/emptypage.php';
            addEvent(iframe, 'load', function() {
                alert('iframe load');
            });

            document.body.appendChild(iframe);

            var form = document.createElement('form');
            form.target = 'postHere';
            form.action = '/emptypage.php';
            var submit = document.createElement('input');
            submit.type = 'submit';
            submit.value = 'Submit';

            form.appendChild(submit);

            document.body.appendChild(form);
        })();
    </script>
</body>
</html>

Safari、Firefox、IE 6、7、8で送信ボタンをクリックするたびにアラートが発生します。


または、ある種のアクセサを提供します。同意する。
アッパーステージ2009

3
人々が投票するときに、投票する理由を説明するのに役立ちます。
まぶたさ2009

6
私は反対票を投じませんでした。ただし、window ['varName'] = varNameと言うことは、クロージャの外部でグローバルvar宣言を行うことと同じです。 var foo = "bar"; (function() { alert(window['foo']) })();
Josh Stodola

あなたは質問ではなく、タイトルに答えました。私はそれが好きではありませんでした。インラインイベントハンドラーからの参照のコンテキスト内にクロージャーイディオムを配置することは(質問の要点として)より良かったでしょう。
クレセントフレッシュ

1
クレセントフレッシュ、私は質問に答えました。グローバルな機能を回避するために、質問の仮定を再設計する必要があります。この回答は、ガイドラインとして、質問の前提条件(たとえば、インラインイベントハンドラー)を採用しており、開発者は、すべてがグローバルスコープにあるのではなく、必要なグローバルアクセスポイントのみを選択できます。
まぶたさ2009

59

モジュールパターンをお勧めします

YAHOO.myProject.myModule = function () {

    //"private" variables:
    var myPrivateVar = "I can be accessed only from within YAHOO.myProject.myModule.";

    //"private" method:
    var myPrivateMethod = function () {
        YAHOO.log("I can be accessed only from within YAHOO.myProject.myModule");
    }

    return  {
        myPublicProperty: "I'm accessible as YAHOO.myProject.myModule.myPublicProperty."
        myPublicMethod: function () {
            YAHOO.log("I'm accessible as YAHOO.myProject.myModule.myPublicMethod.");

            //Within myProject, I can access "private" vars and methods:
            YAHOO.log(myPrivateVar);
            YAHOO.log(myPrivateMethod());

            //The native scope of myPublicMethod is myProject; we can
            //access public members using "this":
            YAHOO.log(this.myPublicProperty);
        }
    };

}(); // the parens here cause the anonymous function to execute and return

3
これを理解していて非常に役立つので+1しますが、グローバル変数を1つだけ使用する状況でこれがどれほど効果的かはまだわかりません。間違っている場合は訂正してください。ただし、この関数を実行して返すと、返されたオブジェクトYAHOO.myProject.myModuleがグローバル変数であるに格納されます。正しい?
Josh Stodola

11
@Josh:グローバル変数は悪ではありません。グローバル変数_S_は悪です。グローバルの数をできるだけ少なくします。
erenon 2009

'モジュール'のパブリックプロパティ/メソッドの1つにアクセスするたびに、匿名関数全体が実行されますか?
upTheCreek 2011

@UpTheCreek:いいえ、できません。プログラムが最後の行で終了()を検出すると、1回だけ実行され、返されたオブジェクトは、myPrivateVarとmyPrivateMethodを含むクロージャを使用してmyModuleプロパティに割り当てられます。
erenon 2011

美しい、まさに私が探していたもの。これにより、ページロジックをjqueryイベント処理から分離できます。質問ですが、XSS攻撃があったとしても、攻撃者はYAHOO.myProject.myModuleにアクセスできますか?外側の関数を無名のままにして();を付けるのは良いことではないでしょうか。最後に、$(document).readyの周りにも?たぶんYAHOO.myProct.myModuleプロパティのメタオブジェクトの変更?私はちょうどかなりの時間をjs理論に投資し、今それを組み合わせようとしています。
デール

8

まず、グローバルJavaScriptを回避することは不可能であり、何かが常にグローバルスコープにぶら下がっています。名前空間を作成する場合でも、それは良い考えですが、その名前空間はグローバルになります。

ただし、グローバルスコープを乱用しないための多くのアプローチがあります。最も簡単な2つは、クロージャを使用するか、追跡する必要のある変数が1つしかないため、それを関数自体のプロパティとして設定することです(static変数として扱うことができます)。

閉鎖

var startUpload = (function() {
  var uploadCount = 1;  // <----
  return function() {
    var fil = document.getElementById("FileUpload" + uploadCount++);  // <----

    if(!fil || fil.value.length == 0) {    
      alert("Finished!");
      document.forms[0].reset();
      uploadCount = 1; // <----
      return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
  };
})();

*の増分に注意してください uploadCountここで内部的に行われていることにください

関数プロパティ

var startUpload = function() {
  startUpload.uploadCount = startUpload.count || 1; // <----
  var fil = document.getElementById("FileUpload" + startUpload.count++);

  if(!fil || fil.value.length == 0) {    
    alert("Finished!");
    document.forms[0].reset();
    startUpload.count = 1; // <----
    return;
  }

  disableAllFileInputs();
  fil.disabled = false;
  alert("Uploading file " + startUpload.count);
  document.forms[0].submit();
};

uploadCount++; if(uploadCount > 1) ...条件は常に真であるように見えるので、なぜ必要なのかわかりません。ただし、変数へのグローバルアクセスが必要な場合は、上記で説明した関数プロパティメソッドを使用すると、変数を実際にグローバルにすることなくアクセスできます。

<iframe src="test.htm" name="postHere" id="postHere"
  onload="startUpload.count++; if (startUpload.count > 1) startUpload();"></iframe>

ただし、その場合は、オブジェクトリテラルまたはインスタンス化されたオブジェクトを使用して、通常のOOの方法でこれを実行する必要があります(モジュールパターンが気になる場合は使用できます)。


1
グローバルスコープを絶対に回避できます。'Closure'の例では、最初から 'var startUpload ='を削除するだけで、その関数は完全に囲まれ、グローバルレベルでアクセスできなくなります。実際には、多くの人が単一の変数を公開することを好みます。その中には、他のすべてへの参照が含まれています
derrylwc 2013年

1
@derrylwcこの場合、startUploadは、参照している単一の変数です。var startUpload = クロージャの例から削除すると、内部関数への参照がないため、内部関数を実行できなくなります。グローバルスコープの汚染を回避する問題は、uploadCountによって使用される内部カウンター変数に関するものstartUploadです。さらに、OPは、このメソッドの外部のスコープが内部で使用されるuploadCount変数で汚染されないようにしようとしていると思います。
ジャスティンジョンソン

匿名クロージャ内のコードがアップローダーをイベントリスナーとして追加する場合、もちろん、適切なイベントが発生するたびに「実行できるようになります」。
ダミアン・イェリック

6

JavaScriptにグローバル変数を含めることが理にかなっている場合があります。しかし、そのように窓から直接ぶら下げたままにしないでください。

代わりに、グローバルを含む単一の「名前空間」オブジェクトを作成します。ボーナスポイントについては、メソッドを含むすべてをそこに入れてください。


どうやってやるの?グローバルを含む単一の名前空間オブジェクトを作成しますか?
p.matsinopoulos 2013

5
window.onload = function() {
  var frm = document.forms[0];
  frm.target = "postMe";
  frm.onsubmit = function() {
    frm.onsubmit = null;
    var uploader = new LazyFileUploader();
    uploader.startUpload();
    return false;
  }
}

function LazyFileUploader() {
    var uploadCount = 0;
    var total = 10;
    var prefix = "FileUpload";  
    var upload = function() {
        var fil = document.getElementById(prefix + uploadCount);

        if(!fil || fil.value.length == 0) {    
            alert("Finished!");
            document.forms[0].reset();
            return;
         }

        disableAllFileInputs();
        fil.disabled = false;
        alert("Uploading file " + uploadCount);
        document.forms[0].submit();
        uploadCount++;

        if (uploadCount < total) {
            setTimeout(function() {
                upload();
            }, 100); 
        }
    }

    this.startUpload = function() {
        setTimeout(function() {
            upload();
        }, 100);  
    }       
}

onloadiframeのハンドラー内でuploadCountをインクリメントするにはどうすればよいですか?これは非常に重要です。
Josh Stodola

1
OK、あなたがここで何をしたかわかります。残念ながら、これは同じではありません。これにより、すべてのアップロードが個別に発生しますが、同時に発生します(技術的には、それらの間に100ミリ秒あります)。現在のソリューションはそれらを順番にアップロードします。つまり、最初のアップロードが完了するまで2番目のアップロードは開始されません。そのため、インラインonloadハンドラーが必要です。ハンドラーのプログラムによる割り当ては、最初に起動するだけなので機能しません。インラインハンドラーは(何らかの理由で)毎回起動します。
Josh Stodola

ただし、これはグローバル変数を非表示にする効果的な方法であることがわかっているため、まだ+1します
Josh Stodola

アップロードごとにiframeを作成して、個々のコールバックを維持できます。
ジャスティンジョンソン

1
これは最終的には素晴らしい答えだと思いますが、特定の例の要件によってわずかに曇っています。基本的には、テンプレート(オブジェクト)を作成し、「new」を使用してインスタンス化するという考え方です。これは、グローバル変数を真に回避するため、つまり妥協することなく、おそらく最良の答えです
PandaWood

3

これを行う別の方法は、オブジェクトを作成してからメソッドを追加することです。

var object = {
  a = 21,
  b = 51
};

object.displayA = function() {
 console.log(object.a);
};

object.displayB = function() {
 console.log(object.b);
};

このようにして、オブジェクト 'obj'のみが公開され、メソッドがそれにアタッチされます。名前空間に追加するのと同じです。


2

いくつかのものはグローバル名前空間にあります-つまり、インラインJavaScriptコードから呼び出す関数は何でもです。

一般に、解決策はすべてをクロージャでラップすることです。

(function() {
    var uploadCount = 0;
    function startupload() {  ...  }
    document.getElementById('postHere').onload = function() {
        uploadCount ++;
        if (uploadCount > 1) startUpload();
    };
})();

インラインハンドラーは避けてください。


この状況では、インラインハンドラーが必要です。フォームがiframeに送信されると、プログラムで設定されたonloadハンドラーは起動しません。
Josh Stodola

1
@ジョシュ:おっ、本当に?iframe.onload = ...と同等ではありません<iframe onload="..."か?
クレセントフレッシュ

2

中小規模のプロジェクトでは、クロージャを使用しても問題ない場合があります。ただし、大規模なプロジェクトの場合は、コードをモジュールに分割して、別のファイルに保存することをお勧めします。

したがって、私は問題を解決するためにjQuerySecretプラグインを作成しました。

このプラグインを使用した場合、コードは次のようになります。

JavaScript:

// Initialize uploadCount.
$.secret( 'in', 'uploadCount', 0 ).

// Store function disableAllFileInputs.
secret( 'in', 'disableAllFileInputs', function(){
  // Code for 'disable all file inputs' goes here.

// Store function startUpload
}).secret( 'in', 'startUpload', function(){
    // 'this' points to the private object in $.secret
    // where stores all the variables and functions
    // ex. uploadCount, disableAllFileInputs, startUpload.

    var fil = document.getElementById( 'FileUpload' + uploadCount);

    if(!fil || fil.value.length == 0) {
        alert( 'Finished!' );
        document.forms[0].reset();
        return;
    }

    // Use the stored disableAllFileInputs function
    // or you can use $.secret( 'call', 'disableAllFileInputs' );
    // it's the same thing.
    this.disableAllFileInputs();
    fil.disabled = false;

    // this.uploadCount is equal to $.secret( 'out', 'uploadCount' );
    alert( 'Uploading file ' + this.uploadCount );
    document.forms[0].submit();

// Store function iframeOnload
}).secret( 'in', 'iframeOnload', function(){
    this.uploadCount++;
    if( this.uploadCount > 1 ) this.startUpload();
});

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        // Call out startUpload function onsubmit
        $.secret( 'call', 'startUpload' );
        return false;
    }
}

関連するマークアップ:

<iframe src="test.htm" name="postHere" id="postHere" onload="$.secret( 'call', 'iframeOnload' );"></iframe>

Firebugを開くと、グローバルはまったく見つかりません。関数も見つかりません:)

完全なドキュメントについては、を参照してくださいここに

デモページは、以下を参照してくださいこれを

GitHubのソースコード


1

クロージャを使用します。このようなものは、グローバル以外のスコープを提供します。

(function() {
    // Your code here
    var var1;
    function f1() {
        if(var1){...}
    }

    window.var_name = something; //<- if you have to have global var
    window.glob_func = function(){...} //<- ...or global function
})();

これまでの私のアプローチは、特定のグローバル変数オブジェクトを定義し、それにすべてのグローバル変数をアタッチすることでした。これをクロージャでどのようにラップしますか?残念ながら、コメントボックスの制限により、一般的なコードを埋め込むことができません。
デビッドエドワーズ

1

個々のグローバル変数を「保護」する場合:

function gInitUploadCount() {
    var uploadCount = 0;

    gGetUploadCount = function () {
        return uploadCount; 
    }
    gAddUploadCount= function () {
        uploadCount +=1;
    } 
}

gInitUploadCount();
gAddUploadCount();

console.log("Upload counter = "+gGetUploadCount());

私はJSの初心者で、現在1つのプロジェクトでこれを使用しています。(コメントや批判はありがたいです)


1

私はそれをこのように使用します:

{
    var globalA = 100;
    var globalB = 200;
    var globalFunc = function() { ... }

    let localA = 10;
    let localB = 20;
    let localFunc = function() { ... }

    localFunc();
}

すべてのグローバルスコープには「var」を使用し、ローカルスコープには「let」を使用します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.