今日、 JavaScript 事実上すべての最新のWebアプリケーションの中核です。特に過去数年間は、強力なJavaScriptベースのライブラリとフレームワークが幅広く普及しているのを目の当たりにしてきました。 シングルページアプリケーション(SPA) 開発、グラフィックス、アニメーション、さらにはサーバーサイドのJavaScriptプラットフォーム。 JavaScriptは、Webアプリ開発の世界で本当にユビキタスになっているため、習得することがますます重要なスキルになっています。
一見すると、JavaScriptは非常に単純に見えるかもしれません。実際、基本的なJavaScript機能をWebページに組み込むことは、JavaScriptを初めて使用する場合でも、経験豊富なソフトウェア開発者にとってはかなり簡単な作業です。それでも、この言語は、最初に信じられていたよりもはるかに微妙で、強力で、複雑です。 実際、JavaScriptの微妙な点の多くは、JavaScriptが機能しないようにする多くの一般的な問題につながります。そのうちの10個については、ここで説明します。これらは、JavaScriptになるための探求において認識し、回避することが重要です。 マスターJavaScript開発者 。
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()
に渡されますwindow
のコンテキストで定義されていますclearBoard()
を持たないオブジェクト方法。
従来の古いブラウザ準拠のソリューションは、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'! };
私たちで議論されているように JavaScript採用ガイド 、JavaScript開発者の間でよくある混乱の原因(したがって、よくあるバグの原因)は、JavaScriptが各コードブロックに新しいスコープを作成すると想定しています。これは他の多くの言語にも当てはまりますが、 ない JavaScriptではtrue。たとえば、次のコードについて考えてみます。
for (var i = 0; i <10; i++) { /* ... */ } console.log(i); // what will this output?
console.log()
だと思うなら呼び出しはundefined
を出力しますまたはエラーをスローします、あなたは間違って推測しました。信じられないかもしれませんが、10
を出力します。どうして?
他のほとんどの言語では、変数i
の「寿命」(つまりスコープ)が原因で、上記のコードはエラーにつながります。 for
に制限されますブロック。ただし、JavaScriptでは、これは当てはまらず、変数i
for
の後でもスコープ内に留まりますループが完了し、ループを終了した後も最後の値を保持します。 (ちなみに、この動作は次のように知られています。 可変巻き上げ )。
ただし、ブロックレベルのスコープのサポートは注目に値します。 です を介してJavaScriptに移行します 新規let
キーワード 。 let
キーワードはJavaScript1.7ですでに利用可能であり、正式にサポートされるJavaScriptキーワードになる予定です。 ECMAScript 6 。
JavaScriptは初めてですか?よく読んで スコープ、プロトタイプなど。
メモリリークを回避するために意識的にコーディングしていない場合、メモリリークはほぼ避けられない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でも役に立ちません。リークしているようですlongStr
毎回replaceThing
と呼ばれます。しかし、なぜ?
もっと詳しく調べてみましょう。
各theThing
オブジェクトには独自の1MBが含まれていますlongStr
オブジェクト。毎秒、replaceThing
を呼び出すと、前のtheThing
への参照が保持されます。 priorThing
内のオブジェクト。しかし、それでもこれが問題になるとは思わないでしょう。なぜなら、毎回、以前に参照されたpriorThing
逆参照されます(priorThing
がpriorThing = theThing;
を介してリセットされた場合)。さらに、replaceThing
の本体でのみ参照されます。および関数内unused
実際、これは使用されません。
ですから、なぜここにメモリリークがあるのか疑問に思っています!?
何が起こっているのかを理解するには、JavaScriptの内部で物事がどのように機能しているかをよりよく理解する必要があります。クロージャを実装する一般的な方法は、すべての関数オブジェクトに、その字句スコープを表す辞書スタイルのオブジェクトへのリンクがあることです。両方の関数がreplaceThing
内で定義されている場合実際に使用されるpriorThing
、たとえpriorThing
であっても、両方が同じオブジェクトを取得することが重要です。は何度も割り当てられるため、両方の関数が同じ字句環境を共有します。ただし、変数がクロージャによって使用されるとすぐに、そのスコープ内のすべてのクロージャによって共有される字句環境になります。そして、その小さなニュアンスが、この厄介なメモリリークにつながるのです。 (これに関する詳細は入手可能です ここに 。)
メモリリークの例2:循環参照
このコードフラグメントを検討してください。
function addClickHandler(element) { element.click = function onClick(e) { alert('Clicked the ' + element.nodeName) } }
ここで、onClick
element
への参照を保持するクロージャがあります(element.nodeName
経由)。 onClick
も割り当てるelement.click
に、循環参照が作成されます。つまり:element
-> onClick
-> element
-> onClick
-> element
…
興味深いことに、たとえelement
がDOMから削除されると、上記の循環自己参照によりelement
が防止されます。およびonClick
収集されないため、メモリリークが発生します。
メモリリークの回避:知っておくべきこと
JavaScriptのメモリ管理(特に、 ガベージコレクション )は、主にオブジェクトの到達可能性の概念に基づいています。
以下のオブジェクトは、 到達可能 そして「ルーツ」として知られています:
オブジェクトは、少なくとも参照または参照のチェーンを介してルートからアクセスできる限り、メモリに保持されます。
ブラウザには、到達不能なオブジェクトが占有しているメモリをクリーンアップするガベージコレクタ(GC)があります。つまり、オブジェクトはメモリから削除されます 場合に限り GCは、それらが到達不能であると考えています。残念ながら、実際には使用されなくなったが、GCはまだ「到達可能」であると考えている、機能しなくなった「ゾンビ」オブジェクトになってしまうのはかなり簡単です。
関連: ApeeScape開発者によるJavaScriptのベストプラクティスとヒントJavaScriptの便利な点の1つは、ブールコンテキストで参照されている値をブール値に自動的に強制変換することです。しかし、これは便利であると同時に混乱を招く場合があります。たとえば、次のいくつかは、多くのJavaScript開発者を噛むことが知られています。
// All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log('
' == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...
最後の2つに関しては、空であるにもかかわらず(false
と評価されると思われる可能性があります)、両方とも{}
および[]
実際にはオブジェクトであり、 どれか オブジェクトはブール値true
に強制変換されますJavaScriptで、 ECMA-262仕様 。
これらの例が示すように、型強制のルールは泥のように明確な場合があります。したがって、型強制が明示的に望まれない限り、通常は===
を使用するのが最善です。および!==
(==
および!=
ではなく)型強制の意図しない副作用を回避するため。 (==
と!=
は、2つのものを比較するときに自動的に型変換を実行しますが、===
と!==
は、型変換なしで同じ比較を実行します。)
そして完全に副次的なものとして-しかし、型強制と比較について話しているので-比較することは言及する価値がありますNaN
と 何でも (NaN
!でも) 常に false
を返します。 したがって、等価演算子(==
、===
、!=
、!==
)を使用して、値がNaN
であるかどうかを判別することはできません。か否か。 代わりに、組み込みのグローバルisNaN()
を使用してください関数:
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
JavaScriptを使用すると、DOMの操作(要素の追加、変更、削除など)が比較的簡単になりますが、効率的な操作を促進することはできません。
一般的な例は、一連のDOM要素を一度に1つずつ追加するコードです。 DOM要素の追加はコストのかかる操作です。複数のDOM要素を連続して追加するコードは非効率的であり、うまく機能しない可能性があります。
複数のDOM要素を追加する必要がある場合の1つの効果的な代替手段は、 ドキュメントの断片 代わりに、それによって効率とパフォーマンスの両方が向上します。
例えば:
var div = document.getElementsByTagName('my_div'); var fragment = document.createDocumentFragment(); for (var e = 0; e このアプローチの本質的に改善された効率に加えて、アタッチされた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 上記のコードに基づいて、入力要素が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 この改訂版のコードでは、makeHandler
ループを通過するたびに、その時点での値i+1
を受け取るたびに、すぐに実行されます。スコープ付きnum
にバインドします変数。外側の関数は、内側の関数(このスコープのnum
変数も使用します)と要素のonclick
を返します。その内部関数に設定されます。これにより、各onclick
が確実になります適切なi
を受信して使用します値(スコープ付きnum
変数を介して)。
よくある間違い#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';
このバージョンでは、BaseObject
name
を継承しますそのprototype
からのプロパティオブジェクト。(デフォルトで)'default'
に設定されています。したがって、コンストラクターが名前なしで呼び出された場合、名前はデフォルトでdefault
になります。同様に、name
の場合プロパティがBaseObject
のインスタンスから削除され、プロトタイプチェーンが検索され、name
が検索されます。プロパティはprototype
から取得されますその値がまだ'default'
であるオブジェクト。だから今私たちは得る:
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
よくある間違い#8:インスタンスメソッドへの誤った参照の作成
次のように、単純なオブジェクトを定義し、そのオブジェクトを作成してインスタンス化します。
Uber vs lyft2017の運転
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? 'window' : 'MyObj'); }; var obj = new MyObject();
ここで、便宜上、whoAmI
への参照を作成しましょう。メソッド、おそらくwhoAmI()
だけでアクセスできるようにするため長いobj.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
、 ない obj
MyObject
のインスタンス!
したがって、オブジェクトの既存のメソッドへの参照を本当に作成する必要がある場合は、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
、それはに渡されます 関数コンストラクター 新しい関数に変換されます。このプロセスは遅くて非効率的である可能性があり、必要になることはめったにありません。
これらのメソッドの最初の引数として文字列を渡す代わりに、代わりに 関数 。例を見てみましょう。
ここでは、setInterval
のかなり典型的な使用法になりますおよびsetTimeout
、 ストリング 最初のパラメータとして:
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採用ガイド 、「厳密モード」(つまり、JavaScriptソースファイルの先頭に'use strict';
を含める)は、実行時にJavaScriptコードに対してより厳密な解析とエラー処理を自発的に適用し、より安全にする方法です。
確かに、厳密モードを使用しないこと自体は「間違い」ではありませんが、その使用はますます奨励されており、その省略はますます悪い形と見なされるようになっています。
厳密モードの主な利点は次のとおりです。
- デバッグが容易になります。 無視されたり、サイレントに失敗したりするコードエラーは、エラーを生成したり、例外をスローしたりして、コードの問題をより早く警告し、より迅速にソースに誘導します。
- 偶発的なグローバルを防ぎます。 strictモードがない場合、宣言されていない変数に値を割り当てると、その名前のグローバル変数が自動的に作成されます。これは、JavaScriptで最も一般的なエラーの1つです。 strictモードでは、そうしようとするとエラーがスローされます。
-
this
を排除します強制 。厳密モードがない場合、this
への参照nullまたはundefinedの値は、自動的にグローバルに強制変換されます。これは、多くのヘッドフェイクや髪の毛を抜くようなバグを引き起こす可能性があります。厳密モードでは、this
を参照しますnullまたはundefinedの値は、エラーをスローします。 - プロパティ名またはパラメータ値の重複を禁止します。 厳密モードでは、オブジェクト内の重複した名前付きプロパティ(
var object = {foo: 'bar', foo: 'baz'};
など)または関数の重複した名前付き引数(function foo(val1, val2, val1){}
など)を検出するとエラーがスローされ、ほぼ確実にバグが検出されます。コード内では、追跡するのに多くの時間を浪費していた可能性があります。 - eval()をより安全にします。 方法にはいくつかの違いがあります
eval()
厳密モードと非厳密モードで動作します。最も重要なのは、厳密モードでは、eval()
内で宣言された変数と関数です。ステートメントは ない 包含スコープで作成されます( です 非厳密モードの包含スコープで作成されます。これも問題の一般的な原因となる可能性があります)。 -
delete
の無効な使用でエラーをスローします。 delete
演算子(オブジェクトからプロパティを削除するために使用)は、オブジェクトの構成不可能なプロパティには使用できません。非strictコードは、構成不可能なプロパティを削除しようとするとサイレントに失敗しますが、strictモードはそのような場合にエラーをスローします。
要約
他のテクノロジーにも当てはまりますが、JavaScriptが機能する理由と方法をよく理解すればするほど、コードはより堅固になり、言語の真の力を効果的に活用できるようになります。逆に、JavaScriptのパラダイムと概念を正しく理解していないことは、実際に多くのJavaScriptの問題が存在する場所です。
言語のニュアンスと微妙さを完全に理解することは、あなたの習熟度を向上させ、あなたの能力を高めるための最も効果的な戦略です。 生産性 。 JavaScriptが機能していない場合は、JavaScriptでよくある多くの間違いを回避することが役立ちます。
関連: JavaScriptの約束:例を含むチュートリアル