バギーJavaScriptコード:JavaScript開発者が犯す最も一般的な10の間違い

公開: 2022-03-11

今日、JavaScriptは事実上すべての最新のWebアプリケーションの中核となっています。 特に過去数年間は、シングルページアプリケーション(SPA)開発、グラフィックス、アニメーション、さらにはサーバー側のJavaScriptプラットフォーム向けに、強力なJavaScriptベースのライブラリとフレームワークが数多く普及しています。 JavaScriptは、Webアプリ開発の世界で本当に普及しているため、習得することがますます重要なスキルになっています。

一見すると、JavaScriptは非常に単純に見えるかもしれません。 実際、基本的なJavaScript機能をWebページに組み込むことは、JavaScriptを初めて使用する場合でも、経験豊富なソフトウェア開発者にとってはかなり簡単な作業です。 それでも、この言語は、最初に信じられていたよりもはるかに微妙で、強力で、複雑です。 実際、JavaScriptの微妙な点の多くは、JavaScriptの機能を妨げる多くの一般的な問題につながります。そのうちの10個については、ここで説明します。これらは、JavaScriptのマスター開発者になるために認識し、回避することが重要です。

よくある間違い#1: thisへの誤った言及

私はかつてコメディアンが言うのを聞いた:

私は実際にはここにいません。なぜなら、「t」なしで、そこ以外に何があるのでしょうか。

そのジョークは多くの点で、JavaScriptのthisキーワードに関して開発者にしばしば存在するタイプの混乱を特徴づけます。 つまり、 thisは本当にこれなのか、それともまったく別のものなのか? それとも未定義ですか?

JavaScriptのコーディング手法とデザインパターンが年々高度化するにつれて、「これ/その混乱」のかなり一般的な原因であるコールバックとクロージャ内の自己参照スコープの急増に対応して増加しています。

次のサンプルコードスニペットについて考えてみます。

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "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; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

または、新しいブラウザでは、 bind()メソッドを使用して適切な参照を渡すことができます。

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };

よくある間違い#2:ブロックレベルのスコープがあると考える

JavaScript採用ガイドで説明されているように、JavaScript開発者の間でよくある混乱の原因(したがって、よくあるバグの原因)は、JavaScriptが各コードブロックに新しいスコープを作成すると想定しています。 これは他の多くの言語にも当てはまりますが、JavaScriptには当てはまりません。 たとえば、次のコードについて考えてみます。

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

console.log()呼び出しがundefinedを出力するか、エラーをスローすると推測する場合は、誤って推測しました。 信じられないかもしれませんが、 10を出力します。 なんで?

他のほとんどの言語では、変数iの「寿命」(つまりスコープ)がforブロックに制限されるため、上記のコードはエラーにつながります。 ただし、JavaScriptでは、これは当てはまらず、変数iは、 forループが完了した後もスコープ内に残り、ループを終了した後も最後の値を保持します。 (この動作は、ちなみに、可変巻き上げとして知られています)。

ただし、ブロックレベルのスコープのサポートが新しいletキーワードを介してJavaScriptに組み込まれていることは注目に値します。 letキーワードはJavaScript1.7ですでに使用可能であり、ECMAScript6で正式にサポートされるJavaScriptキーワードになる予定です。

JavaScriptは初めてですか? スコープ、プロトタイプなどを読んでください。

よくある間違い#3:メモリリークの作成

メモリリークを回避するために意識的にコーディングしていない場合、メモリリークはほぼ避けられないJavaScriptの問題です。 それらが発生する方法はたくさんあるので、それらのより一般的な発生のいくつかを強調します。

メモリリークの例1:無効なオブジェクトへの参照をぶら下げる

次のコードを検討してください。

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

上記のコードを実行してメモリ使用量を監視すると、1秒あたり1メガバイトのリークという大規模なメモリリークが発生していることがわかります。 そして、手動のGCでさえ役に立ちません。 したがって、 replaceThingが呼び出されるたびにlongStrがリークしているように見えます。 しかし、なぜ?

物事をより詳細に調べてみましょう:

theThingオブジェクトには、独自のlongStrオブジェクトが含まれています。 毎秒、 replaceThingを呼び出すと、 priorThing内のtheThingオブジェクトへの参照が保持されます。 ただし、これが問題になるとは思われません。これは、毎回、以前に参照されていたpriorThingが逆参照されるためです( priorThingpriorThing = theThing;を介してリセットされる場合)。 さらに、 replaceThingの本体と、実際には使用されていunusedの関数でのみ参照されます。

ですから、なぜここにメモリリークがあるのか​​疑問に思っています!?

何が起こっているのかを理解するには、JavaScriptで内部的にどのように機能しているかをよりよく理解する必要があります。 クロージャを実装する一般的な方法は、すべての関数オブジェクトに、その字句スコープを表す辞書スタイルのオブジェクトへのリンクがあることです。 replaceThing priorThing使用した場合、 priorThingが何度も割り当てられたとしても、両方が同じオブジェクトを取得することが重要です。したがって、両方の関数は同じ字句環境を共有します。 ただし、変数がクロージャによって使用されるとすぐに、そのスコープ内のすべてのクロージャによって共有される字句環境になります。 そして、その小さなニュアンスが、この厄介なメモリリークにつながるのです。 (これについての詳細はここにあります。)

メモリリークの例2:循環参照

このコードフラグメントを考えてみましょう。

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

ここで、 onClickには、( element.nodeNameを介して) elementへの参照を保持するクロージャーがあります。 また、 onClickelement.clickに割り当てることにより、循環参照が作成されます。 すなわち: element -> onClick > element -> onClick > element

興味深いことに、 elementがDOMから削除された場合でも、上記の循環自己参照により、 elementonClickが収集されなくなり、メモリリークが発生します。

メモリリークの回避:知っておくべきこと

JavaScriptのメモリ管理(特にガベージコレクション)は、主にオブジェクトの到達可能性の概念に基づいています。

次のオブジェクトは到達可能であると想定され、「ルート」と呼ばれます。

  • 現在の呼び出しスタックの任意の場所から参照されるオブジェクト(つまり、現在呼び出されている関数のすべてのローカル変数とパラメーター、およびクロージャースコープのすべての変数)
  • すべてのグローバル変数

オブジェクトは、少なくとも、参照または参照のチェーンを介してルートのいずれかからアクセスできる限り、メモリに保持されます。

ブラウザには、到達不能なオブジェクトによって占有されているメモリをクリーンアップするガベージコレクタ(GC)があります。 つまり、オブジェクトが到達不能であるとGCが判断した場合にのみ、オブジェクトはメモリから削除されます。 残念ながら、実際には使用されなくなったが、GCはまだ「到達可能」であると考えている、機能しなくなった「ゾンビ」オブジェクトになってしまうのはかなり簡単です。

関連: Toptal開発者によるJavaScriptのベストプラクティスとヒント

よくある間違い#4:平等についての混乱

JavaScriptの便利な点の1つは、ブールコンテキストで参照されている値をブール値に自動的に強制することです。 ただし、これは便利であると同時に混乱を招く場合があります。 たとえば、次のいくつかは、多くのJavaScript開発者を噛むことが知られています。

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

最後の2つに関しては、空であるにもかかわらず( falseと評価されると思われる可能性があります)、 {}[]はどちらも実際にはオブジェクトであり、JavaScriptではすべてのオブジェクトがブール値trueに強制変換されます。 ECMA-262仕様に準拠しています。

これらの例が示すように、型強制のルールは泥のように明確な場合があります。 したがって、型強制が明示的に望まれない限り、型強制の意図しない副作用を回避するために、通常は===および!====および!=ではなく)を使用するのが最善です。 ( ==!=は、2つのものを比較するときに自動的に型変換を実行しますが、 ===!==は、型変換なしで同じ比較を実行します。)

そして完全に副次的なものとして-しかし、型強制と比較について話しているのでNaN何かNaNでさえ!)と比較すると常にfalseが返されることに言及する価値があります。 したがって、等式演算子( =====!=!== )を使用して、値がNaNであるかどうかを判別することはできません。 代わりに、組み込みのグローバルisNaN()関数を使用してください。

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

よくある間違い#5:非効率的なDOM操作

JavaScriptを使用すると、DOMの操作(つまり、要素の追加、変更、削除)が比較的簡単になりますが、効率的な操作を促進することはできません。

一般的な例は、一連のDOM要素を一度に1つずつ追加するコードです。 DOM要素の追加はコストのかかる操作です。 複数のDOM要素を連続して追加するコードは非効率的であり、うまく機能しない可能性があります。

複数のDOM要素を追加する必要がある場合の効果的な代替策の1つは、代わりにドキュメントフラグメントを使用することです。これにより、効率とパフォーマンスの両方が向上します。

例えば:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

このアプローチの本質的に改善された効率に加えて、アタッチされたDOM要素の作成にはコストがかかりますが、デタッチ中にそれらを作成および変更してからアタッチすると、パフォーマンスが大幅に向上します。

よくある間違い#6: forループ内の関数定義の誤った使用

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

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example 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; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner 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が(スコープされたnum変数を介して)適切なi値を受け取り、使用することが保証されます。

よくある間違い#7:プロトタイプの継承を適切に活用できない

驚くほど高い割合のJavaScript開発者は、プロトタイプの継承の機能を完全に理解しておらず、したがって完全に活用できていません。

これが簡単な例です。 このコードを考えてみましょう:

 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); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

しかし、これを行うとしたらどうなるでしょうか。

 delete secondObj.name;

次に、次のようになります。

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

しかし、これを「デフォルト」に戻す方が良いのではないでしょうか。 次のように、プロトタイプの継承を活用するように元のコードを変更すると、これを簡単に行うことができます。

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

このバージョンでは、 BaseObjectprototypeオブジェクトからnameプロパティを継承し、(デフォルトで) 'default'に設定されます。 したがって、コンストラクターが名前なしで呼び出された場合、名前はデフォルトでdefaultになります。 同様に、 nameプロパティがBaseObjectのインスタンスから削除された場合、プロトタイプチェーンが検索され、 nameプロパティは、値が'default'ままであるprototypeオブジェクトから取得されます。 だから今私たちは得る:

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

よくある間違い#8:インスタンスメソッドへの誤った参照の作成

次のように、単純なオブジェクトを定義し、そのインスタンスを作成してみましょう。

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

ここで、便宜上、 whoAmIメソッドへの参照を作成しましょう。おそらく、長いobj.whoAmI() whoAmI()だけでアクセスできるようにするためです。

 var whoAmI = obj.whoAmI;

そして、すべてが共食いに見えることを確認するために、新しいwhoAmI変数の値を出力してみましょう。

 console.log(whoAmI);

出力:

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

うんいいね。 うまく見えます。

しかし、ここで、 obj.whoAmI()と便利なリファレンスwhoAmI()を呼び出すときの違いを見てください。

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

何が悪かったのか?

ここでの偽物は、割り当てvar whoAmI = obj.whoAmI;を実行したときです。 、新しい変数whoAmIグローバル名前空間で定義されていました。 結果として、 thisの値はwindowであり、 MyObjectobjインスタンスではありません

したがって、オブジェクトの既存のメソッドへの参照を実際に作成する必要がある場合は、 this値を保持するために、そのオブジェクトの名前空間内で必ず作成する必要があります。 これを行う1つの方法は、たとえば、次のようになります。

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

よくある間違い#9: setTimeoutまたはsetIntervalの最初の引数として文字列を指定する

手始めに、ここで何かを明確にしましょうsetTimeoutまたはsetIntervalの最初の引数として文字列を指定すること自体は、それ自体が間違いではありません。 これは完全に正当なJavaScriptコードです。 ここでの問題は、パフォーマンスと効率の問題です。 まれにしか説明されないのは、内部では、 setTimeoutまたはsetIntervalの最初の引数として文字列を渡すと、それが関数コンストラクターに渡されて新しい関数に変換されるということです。 このプロセスは遅くて非効率的である可能性があり、必要になることはめったにありません。

これらのメソッドの最初の引数として文字列を渡す代わりに、代わりに関数を渡すこともできます。 例を見てみましょう。

ここでは、 setIntervalsetTimeoutのかなり一般的な使用法であり、最初のパラメーターとして文字列を渡します。

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

より良い選択は、最初の引数として関数を渡すことです。 例えば:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

よくある間違い#10:「厳密モード」の使用の失敗

JavaScript採用ガイドで説明されているように、「strictモード」(つまり、JavaScriptソースファイルの先頭にある「usestrict 'use strict';を含む)は、実行時にJavaScriptコードに対してより厳密な解析とエラー処理を自発的に実施する方法でもあります。それをより安全にするように。

確かに、厳密モードを使用しないこと自体は「間違い」ではありませんが、その使用はますます奨励されており、その省略はますます悪い形と見なされるようになっています。

厳密モードの主な利点は次のとおりです。

  • デバッグが容易になります。 無視されたり、サイレントに失敗したりするコードエラーは、エラーを生成したり、例外をスローしたりして、コードの問題をより早く警告し、より迅速にソースに誘導します。
  • 偶発的なグローバルを防ぎます。 厳密モードがない場合、宣言されていない変数に値を割り当てると、その名前のグローバル変数が自動的に作成されます。 これは、JavaScriptで最も一般的なエラーの1つです。 厳密モードでは、そうしようとするとエラーがスローされます。
  • this強制を排除します。 strictモードがない場合、 this nullまたはundefinedの値への参照は、自動的にグローバルに強制変換されます。 これは、多くのヘッドフェイクや髪の毛を抜くようなバグを引き起こす可能性があります。 厳密モードでは、 this nullまたはundefinedの値を参照すると、エラーがスローされます。
  • プロパティ名またはパラメータ値の重複を禁止します。 厳密モードでは、オブジェクト内の重複した名前付きプロパティ(たとえば、 var object = {foo: "bar", foo: "baz"}; )または関数の重複した名前付き引数(たとえば、 function foo(val1, val2, val1){} )。これにより、コードのバグである可能性があり、それ以外の場合は追跡に多くの時間を浪費していた可能性があります。
  • eval()をより安全にします。 strictモードとnon-strictモードでのeval()の動作にはいくつかの違いがあります。 最も重要なことは、strictモードでは、 eval()ステートメント内で宣言された変数と関数が包含スコープで作成されないことです(これら非strictモードで包含スコープで作成されます。これも問題の一般的な原因となる可能性があります)。
  • deleteの無効な使用でエラーをスローします。 delete演算子(オブジェクトからプロパティを削除するために使用)は、オブジェクトの構成不可能なプロパティには使用できません。 非strictコードは、構成不可能なプロパティを削除しようとするとサイレントに失敗しますが、strictモードはそのような場合にエラーをスローします。

要約

他のテクノロジーにも当てはまりますが、JavaScriptが機能する理由と方法をよく理解すればするほど、コードはより堅固になり、言語の真の力を効果的に活用できるようになります。 逆に、JavaScriptのパラダイムと概念を正しく理解していないことは、実際に多くのJavaScriptの問題が存在する場所です。

言語のニュアンスと微妙さを完全に理解することは、習熟度を向上させ、生産性を向上させるための最も効果的な戦略です。 JavaScriptが機能していない場合は、JavaScriptでよくある多くの間違いを回避することが役立ちます。

関連: JavaScriptの約束:例を含むチュートリアル