
今日、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エラーの1つです。 「厳格なモード」では、これはエラーメッセージになります。
- プロパティ名またはパラメータ値の重複を防ぎます 。 プロパティ名の重複(たとえば、
var object = {foo: "bar", foo: "baz"};
)または関数への引数の名前が "strict mode"が有効になっているときに検出されると、エラーメッセージが表示されます。 これにより、バグをすばやく検出して修正できます。 -
eval()
潜在的な危険を減らす 。 strictモードでは、eval()
内で宣言された変数と関数は現在のスコープで作成されません。 - 誤って
delete
演算子を使用したときにエラーメッセージを受信する 。 この演算子は、configurable
フラグがfalse
であるオブジェクトのプロパティには適用できません。これを実行しようとすると、エラーメッセージが表示されます。
結論として
JavaScriptがどのように、なぜ機能するかをよく理解すればするほど、コードの信頼性が高くなり、この言語の機能をより効果的に使用できるようになります。 逆に、JavaScriptに埋め込まれたパラダイムの誤解は、ソフトウェア製品に多数のバグを引き起こしています。したがって、言語のニュアンスと繊細さの研究は、プロ意識と生産性を高めるための最も効果的な戦略であり、JavaScriptコードを記述する際の多くの一般的な間違いを避けるのにも役立ちます。