よくあるJavaScriptプログラミングの10の間違い





今日、JavaScriptはほとんどの最新のWebアプリケーションの中核です。 同時に、近年では、 シングルページアプリケーション(SPA) 、グラフィックス、アニメーション、さらにはサーバープラットフォームの開発者向けのJavaScriptライブラリとフレームワークが多数登場しています。 JavaScriptはWeb開発に広く使用されており、その結果、コードの品質がますます重要になっています。



一見すると、この言語は非常に単純に見えるかもしれません。 基本的なJavaScript機能をWebページに埋め込むことは、経験のある開発者にとっては問題ではありません。たとえ以前にこの言語に出会ったことがなくてもです。 ただし、JavaScriptは最初よりもはるかに複雑で強力で、ニュアンスに敏感であるため、これは誤解を招く印象です。 この言語の微妙な点の多くは、多くの一般的な間違いにつながります。 今日はそれらのいくつかを見ます。 JavaScriptで完全にプログラムしたい場合は、これらのエラーに特別な注意を払う必要があります。



1.これへの誤ったリンク

近年、JavaScriptプログラミングが非常に複雑になったため、このキーワードと混同されることが多いコールバック関数とクロージャーが出現するケースがそれに応じて増加しています。



たとえば、次のコードを実行すると:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); //   "this"? }, 0); };
      
      





エラーにつながります:

 Uncaught TypeError: undefined is not a function
      
      





なぜこれが起こっているのですか? コンテキストがすべてです。 setTimeout()



を呼び出すとき、実際にはwindow.setTimeout()



呼び出します。 その結果、 setTimeout()



渡される匿名関数は、 clearBoard()



メソッドを持たないwindow



オブジェクトのコンテキストで定義されます。



古いブラウザと互換性のある従来のソリューションでは、クロージャーに格納できる変数にthis



への参照を格納するだけです。

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; //    'this',     'this'! this.timer = setTimeout(function(){ self.clearBoard(); //    }, 0); };
      
      





新しいブラウザの場合、 bind()



メソッドを使用して、関数を実行コンテキストに関連付けることができます。

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); //  'this' }; Game.prototype.reset = function(){ this.clearBoard(); //     'this'! };
      
      





2.ブロックレベルの可視性

開発者は、JavaScriptがコードのブロックごとに新しいスコープを作成すると考えています。 これは他の多くの言語にも当てはまりますが、JavaScriptでは発生しません。 このコードを見てください:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); //   ?
      
      





console.log()



を呼び出すと、 undefined



出力またはエラーが発生すると思われる場合は、間違っています:「10」が表示されます。 なんで? 他のほとんどの言語では、変数i



スコープがfor



ブロックに制限されるため、このコードはエラーを引き起こします。 ただし、JavaScriptでは、この変数はfor



ループの完了後もスコープ内に残り、その最後の値を保持します(この動作は「 var hoisting 」として知られています)。 バージョン1.7以降、 let



記述子を使用してJavaScriptにブロックレベルのスコープが導入されていることに注意してください。



3.メモリリーク

動作中に意識的にそれらを回避しない場合、メモリリークはほとんど避けられません。 リークには多くの理由がありますが、最も頻繁なものにのみ焦点を当てます。



存在しないオブジェクトへのリンク 。 このコードを分析しましょう:

 var theThing = null; var replaceThing = function () { var priorThing = theThing; var unused = function () { // 'unused' -  ,   'priorThing', //  'unused'    if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), //  1M  someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); //  'replaceThing'  
      
      





このコードを実行すると、1秒あたり約メガバイトの速度で大規模なメモリリークを検出できます。 longStr



を呼び出すたびにlongStr



割り当てられたメモリを失うようです。 その理由は何ですか?



theThing



オブジェクトには、独自のtheThing



オブジェクトが含まれています。 replaceThing



呼び出されると、関数replaceThing



1秒ごとに、 theThing



変数に以前のtheThing



オブジェクトへの参照を格納します。 これは問題ではありません。前のpriorThing



リンクが毎回priorThing = theThing;



されるpriorThing = theThing;



priorThing = theThing;



)。 リークの理由は何ですか?



クロージャーを実装する一般的な方法は、各関数オブジェクトと辞書オブジェクトの間の関係を作成することです。これは、この関数の字句スコープです。 someMethod



内で定義された両方の関数( unused



およびsomeMethod



)が実際にreplaceThing



使用する場合、 priorThing



priorThing



書き換えられても、両方の関数が同じレキシカルスコープを使用するため、同じオブジェクトを取得することを理解することが重要priorThing



。 そして、いずれかのクロージャーで変数が使用されるとすぐに、このスコープのすべてのクロージャーで使用される字句スコープに分類されます。 そして、この小さなニュアンスは強力なメモリリークにつながります。



循環リンク 。 コード例を考えてみましょう:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
      
      





ここで、 onClick



は、 element



へのリンクelement



保存されるクロージャーがあります。 onClick



element



click



イベントハンドラとして割り当てることにより、循環リンクを作成しました: onClick



> onClick



> onClick



> onClick



> element



...



DOMからelement



を削除しても、循環リンクによってガベージコレクターからelement



onClick



が非表示になり、メモリリークが発生します。 リークを回避する最良の方法は何ですか? JavaScriptメモリ管理(特にガベージコレクション)は、主にオブジェクトの到達可能性の概念に基づいています。 次のオブジェクトは到達可能と見なされ、ルートと呼ばれます。



オブジェクトは、リンクまたはリンクチェーンによってルートからアクセスできる場合にのみメモリに格納されます。



ブラウザには、到達不能オブジェクトからメモリを消去する組み込みのガベージコレクタがあります。 つまり、オブジェクトは、ガベージコレクターが到達不能であると判断した場合にのみメモリから削除されます。 残念ながら、「到達可能」と見なされる未使用の大きなオブジェクトは非常に簡単に蓄積される可能性があります。



4.平等の誤解

JavaScriptの利点の1つは、ブール値のコンテキストで使用される場合、値をブール値に自動的に変換することです。 ただし、この利便性が誤解を招く場合があります。

 //     'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); //   ! if ({}) // ... if ([]) // ...
      
      





最後の2行を指定すると、空の場合でも{}



[]



は実際にはオブジェクトです。 JavaScriptのオブジェクトはブール値true



対応しtrue



。 ただし、多くの開発者は値がfalse



なると信じています。



上記の2つの例が示すように、自動型変換が干渉することがあります。 一般的に、型変換の副作用を避けるために、 ==



!=



代わりに===



!==



を使用することをお===



!==







ちなみに、 NaN



を何かと比較すると( NaN



でも!)常にfalse



が生成されfalse



。 したがって、等値演算子( ==



===



!=



!==



)を使用して、 NaN



値の対応を判断することはできません。 代わりに、組み込みのグローバル関数isNaN()



使用しisNaN()





 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
      
      





5. DOMの誤用

JavaScriptでは、DOMを簡単に操作(要素の追加、変更、削除を含む)できますが、多くの場合、開発者はこれを非効率的に行います。 たとえば、一連のアイテムを一度に1つずつ追加します。 ただし、要素を追加する操作は非常に高価であり、その順次実装は避ける必要があります。



複数の要素を追加する必要がある場合は、代わりにドキュメントフラグメントを使用できます。

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));
      
      





また、最初に要素を作成および変更してからDOMに追加することをお勧めします。これにより、パフォーマンスも大幅に向上します。



6. forループ内での関数定義の誤った使用

コード例を考えてみましょう:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // ,    10  for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }
      
      





10個の要素のいずれかをクリックすると、「これは要素#10です」というメッセージが表示されます。 その理由は、 onclick



がいずれかの要素によって呼び出されるまでに、上流のfor



ループが完了し、 i



の値が10になるためです。



正しいコードの例:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // ,    10  var makeHandler = function(num) { //   return function() { //   console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }
      
      





makeHandler



はループの各反復で直ちに開始し、現在の値i+1



を取得して変数num



保存します。 外部関数は、内部関数( num



変数も使用)を返し、 onclick



ハンドラーとして設定します。 これにより、すべてのonclick



が正しいi



値を受け取り、使用するようになります。



7.プロトタイプによる不適切な継承

驚くべきことに、多くの開発者はプロトタイプによる継承のメカニズムを明確に理解していません。 コード例を考えてみましょう:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } }; var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // ->  'default' console.log(secondObj.name); // ->  'unique'
      
      





しかし、次のように書いた場合:

 delete secondObj.name;
      
      





あなたは得るでしょう:

 console.log(secondObj.name); // ->  'undefined'
      
      





しかし、値をdefault



に戻すことは良くありませんか? これは、プロトタイプを介して継承を適用​​する場合、簡単に実行できます。

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
      
      





BaseObject



インスタンスは、そのプロトタイプのnameプロパティを継承し、 default



値が割り当てられます。 したがって、コンストラクタがname



なしで呼び出された場合、 name



プロパティはdefault



なりdefault



。 同様に、 name



プロパティがBaseObject



インスタンスから削除された場合、プロトタイプチェーンが検索され、 prototype



オブジェクトからname



プロパティが取得されますが、このプロパティはdefault



ままdefault





 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // ->  'unique' delete thirdObj.name; console.log(thirdObj.name); // ->  'default'
      
      





8.インスタンスメソッドへの誤った参照の作成

単純なコンストラクターを定義し、それを使用してオブジェクトを作成します。

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
      
      





便宜上、 whoAmI



メソッドへのリンクを作成します。

 var whoAmI = obj.whoAmI;
      
      





新しいwhoAmI



変数の値をwhoAmI



ます。

 console.log(whoAmI);
      
      





コンソールには以下が表示されます。

 function () { console.log(this === window ? "window" : "MyObj"); }
      
      





obj.whoAmI()



whoAmI()



呼び出すときの違いに注目してください。

 obj.whoAmI(); //  "MyObj" (  ) whoAmI(); //  "window"
      
      





何が悪かったのですか? var whoAmI = obj.whoAmI;



を割り当てたとき 、新しい変数がグローバル名前空間に定義されました。 その結果、 this



MyObject



インスタンスに対してobj



ではなくwindow



であることが判明しました。 したがって、オブジェクトの既存のメソッドへの参照を本当に作成する必要がある場合、このオブジェクトの名前空間内でこれを行う必要があります。 例:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; //     obj.whoAmI(); //  "MyObj" (  ) obj.w(); //  "MyObj" (  )
      
      





9. setTimeoutまたはsetIntervalの最初の引数として文字列を使用する

これ自体は間違いではありません。 そして、パフォーマンスだけではありません。 問題は、文字列変数をsetTimeout



またはsetInterval



最初の引数として渡すと、それがFunction



コンストラクターに渡されて新しい関数に変換されることです。 このプロセスは遅く、非効率的です。 別の方法は、最初の引数として関数を使用することです:

 setInterval(logTime, 1000); //   logTime  setInterval setTimeout(function() { //     setTimeout logMessage(msgValue); // (msgValue    ) }, 1000);
      
      





10.「厳格モード」の使用の拒否

これは、実行可能コードに多数の制限が課されるモードです。これにより、セキュリティが向上し、エラーの発生を防ぐことができます。 もちろん、「厳格モード」の使用を拒否すること自体は間違いではありません。 この場合、次のような多くの利点がありません。





結論として

JavaScriptがどのように、なぜ機能するかをよく理解すればするほど、コードの信頼性が高くなり、この言語の機能をより効果的に使用できるようになります。 逆に、JavaScriptに埋め込まれたパラダイムの誤解は、ソフトウェア製品に多数のバグを引き起こしています。



したがって、言語のニュアンスと繊細さの研究は、プロ意識と生産性を高めるための最も効果的な戦略であり、JavaScriptコードを記述する際の多くの一般的な間違いを避けるのにも役立ちます。



All Articles