それほど単純ではない
一見すると、JavaScriptはかなり単純な言語のように見えるかもしれません。 おそらく、これはかなり柔軟な構文によるものです。 または、Javaなどの他の有名な言語との類似性のため。 まあ、またはJava、Ruby、または.NETと比較して、データ型の数が比較的少ないためです。
しかし実際には、JavaScriptの構文は、最初は思われるほど単純でなく、明白ではありません。 JavaScriptの最も特徴的な機能のいくつかはまだ誤解されており、特に経験豊富な開発者の間では完全に理解されていません。 これらの機能の1つは、データ(プロパティと変数)取得のパフォーマンスと、結果として生じるパフォーマンスの問題です。
JavaScriptでは、データ検索はプロトタイプの継承とスコープチェーンの2つのことに依存します。 開発者にとって、これらの2つのメカニズムを理解することは絶対に必要です。これは、構造の改善につながり、多くの場合、コードパフォーマンスの改善にもつながるからです。
プロトタイプチェーンのプロパティの取得
JavaScriptでプロパティにアクセスすると、オブジェクトプロトタイプのチェーン全体がスキャンされます。
JavaScriptのすべての関数はオブジェクトです。 関数が
new
演算子で呼び出されると、新しいオブジェクトが作成されます。
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var p1 = new Person('John', 'Doe'); var p2 = new Person('Robert', 'Doe');
上記の例では、
p1
と
p2
2つの異なるオブジェクトであり、それぞれが
Person
コンストラクターを使用して作成されます。 次の例が示すように、これらは
Person
独立したインスタンスです。
console.log(p1 instanceof Person); // 'true' console.log(p2 instanceof Person); // 'true' console.log(p1 === p2); // 'false'
JavaScriptオブジェクトに関数が入ると、プロパティを設定できます。 彼らが持っている最も重要なプロパティは
prototype
と呼ばれます。
オブジェクトであるprototypeは、最上位に到達するまで親プロトタイプから何度も継承されます。 これはよくプロトタイプチェーンと呼ばれます。 チェーンの先頭は常に
Object.prototype
(つまり、プロトタイプチェーンの最上位)。 メソッド
toString()
、
hasProperty()
、
isPrototypeOf()
などが含まれます。
![](https://habrastorage.org/getpro/habr/post_images/18e/642/d7f/18e642d7f5fb6448ad928bb17ed3fdbf.png)
各関数のプロトタイプは、独自のメソッドとプロパティによって拡張できます。
オブジェクトの新しいインスタンスを作成するとき(
new
演算子で関数を呼び出すことにより)、プロトタイプを介してすべてのプロパティを継承します。 ただし、インスタンスはプロトタイプオブジェクトに直接アクセスできず、そのプロパティにのみアクセスできることに注意してください。
// Person // 'getFullName': Person.prototype.getFullName = function() { return this.firstName + ' ' + this.lastName; } // p1 console.log(p1.getFullName()); // 'John Doe' // p1 'prototype'... console.log(p1.prototype); // 'undefined' console.log(p1.prototype.getFullName()); //
これは重要かつ微妙な点です。たとえ
getFullName
メソッドが
getFullName
前に
p1
が作成されたとしても、そのプロトタイプは
Person
プロトタイプであるため、引き続きアクセスできます。
(ブラウザがプロトタイプリンクを
__proto__
プロパティに保存することは言及する価値がありますが、少なくともECMAScript標準にないため、その使用は本当にカルマを台無しにします。 使用しないでください )。
Person
p1
のインスタンスはプロトタイプオブジェクトに直接アクセスできないため、
p1
の
getFullName
メソッドを次のように書き換える必要があります。
// p1.getFullName, ** p1.prototype.getFullName, // p1.prototype : p1.getFullName = function(){ return ' '; }
現在、
p1
は独自の
getFullName
プロパティがあります。 ただし、インスタンス
p2
は、このプロパティの独自の実装はありません。 したがって、
p1.getFullName
を呼び出すと、オブジェクト
p1
ネイティブメソッドがプルされ、
p2.getFullName()
呼び出すと、プロトタイプチェーンが
Person
上がります。
console.log(p1.getFullName()); // ' ' console.log(p2.getFullName()); // 'Robert Doe'
![画像](https://habrastorage.org/getpro/habr/post_images/115/c48/8fb/115c488fb1d76ae14f5d40d73d74cd42.png)
もう1つ注意が必要なのは、オブジェクトのプロトタイプを動的に変更する機能です。
function Parent() { this.someVar = 'someValue'; }; // Parent, 'sayHello' Parent.prototype.sayHello = function(){ console.log('Hello'); }; function Child(){ // // . Parent.call(this); }; // Child 'otherVar'... Child.prototype.otherVar = 'otherValue'; // ... Child Parent // ( 'otherVar', // Child 'otherVar' ) Child.prototype = Object.create(Parent.prototype); var child = new Child(); child.sayHello(); // 'Hello' console.log(child.someVar); // 'someValue' console.log(child.otherVar); // 'undefined'
プロトタイプの継承を使用する場合、親からの継承の後に子プロトタイプのプロパティを設定する必要があることに注意してください。
![](https://habrastorage.org/getpro/habr/post_images/fb2/298/d22/fb2298d2288256b9e19f0ff7edd6b852.png)
そのため、プロトタイプチェーンでプロパティを取得する方法は次のとおりです。
- オブジェクトに目的の名前のプロパティがある場合、それが返されます。 (
hasOwnProperty
メソッドを使用すると、これがオブジェクトのプロパティhasOwnProperty
であるhasOwnProperty
を確認できます)。 - オブジェクトに類似したものがない場合は、オブジェクトのプロトタイプを見てください。
- ここに何もない場合は、彼自身のプロトタイプをすでに確認しています。
- など、目的のプロパティが見つかるまで。
- Object.prototypeに到達しても何も見つからない場合、プロパティは設定されていないと見なされます。
プロトタイプの継承がどのように機能するかを理解することは開発者にとって一般的に重要ですが、それ以上はパフォーマンスへの影響(場合によっては顕著)のため重要です。 V8のドキュメントに書かれているように、ほとんどのJavaScriptエンジンは辞書のようなデータ構造を使用してプロパティを保存します。 したがって、任意のプロパティを呼び出すには、目的のプロパティを見つけるための動的検索が必要です。 これにより、JavaScriptのプロパティへのアクセスは、JavaやSmalltalkなどの言語のインスタンス変数へのアクセスよりもはるかに遅くなります。
スコープチェーンを介した変数検索
別のJavaScript検索エンジンは、閉鎖に基づいています。
これがどのように機能するかを理解するには、実行コンテキストなどの概念を導入する必要があります 。
JavaScriptには、2種類の実行コンテキストがあります。
- グローバル。JavaScriptの実行時に作成されます。
- ローカル、関数が呼び出されたときに作成
実行コンテキストはスタックとして編成されます。 スタックの一番下には、常に各プログラムに固有のグローバルコンテキストがあります。 関数が検出されるたびに、新しい実行コンテキストが作成され、スタックの最上部に配置されます。 関数が完了するとすぐに、そのコンテキストはスタックからスローされます。
// var message = 'Hello World'; var sayHello = function(n){ // 1 var i = 0; var innerSayHello = function() { // 2 console.log((i + 1) + ': ' + message); // 2 } for (i = 0; i < n; i++) { innerSayHello(); } // 1 }; sayHello(3); // : // 1: Hello World // 2: Hello World // 3: Hello World
各実行コンテキストには、変数の解決に使用されるスコープチェーンと呼ばれる特別なオブジェクトがあります。 チェーンは基本的に、現在からグローバルまで、利用可能な実行コンテキストのスタックです。 (より正確には、スタックの一番上にあるオブジェクトはActivation Objectと呼ばれ、実行可能関数のローカル変数への参照、関数の指定された引数、2つの「特別な」オブジェクト:
this
および
arguments
を含み
arguments
)。
![](http://habrastorage.org/files/e7a/a2b/452/e7aa2b4525914966bf81f7f1146427c3.png)
図では、
this
がデフォルトで
window
オブジェクトを指し、グローバルオブジェクトには
console
や
location
などの他のオブジェクトが含まれていることに注意してください。
スコープのチェーンを介して変数を解決しようとすると、最初に現在のコンテキストで目的の変数がチェックされます。 一致するものが見つからない場合、チェーン内の次のコンテキストオブジェクトがチェックされ、必要なものが見つかるまで続きます。 何も見つからない場合、
ReferenceError
ます。
さらに、
try-catch
または
with
ブロックが検出される
try-catch
、新しいスコープが追加されることに注意することが重要です。 これらのすべての場合において、新しいオブジェクトが作成され、可視領域のチェーンの上部に配置されます。
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }; function persist(person) { with (person) { // 'person' // "with", // 'firstName' 'lastName', person.firstName // person.lastName if (!firstName) { throw new Error('FirstName is mandatory'); } if (!lastName) { throw new Error('LastName is mandatory'); } } try { person.save(); } catch(error) { // , 'error' console.log('Impossible to store ' + person + ', Reason: ' + error); } } var p1 = new Person('John', 'Doe'); persist(p1);
スコープ環境で変数がどのように解決されるかを完全に理解するには、JavaScriptが現在ブロックレベルでスコープを持たないことに注意することが重要です。
for (var i = 0; i < 10; i++) { /* ... */ } // 'i' ! console.log(i); // '10'
他のほとんどの言語では、変数
i
の「寿命」(つまりスコープ)がforブロックに制限されるため、上記のコードはエラーになります。 ただし、JavaScriptにはありません。 iは、スコープチェーンの最上部のActivationオブジェクトに追加され、オブジェクトが削除されるまでそこに残ります。これは、実行コンテキストがスタックから削除された後に発生します。 この動作は、変数のフローティングとして知られています。
新しい
let
キーワードの出現により、ブロックレベルのスコープがJavaScriptに導入されたことに言及する価値があります。 JavaScript 1.7で既に利用可能であり、ECMAScript 6から正式にサポートされるキーワードになるはずです。
パフォーマンスへの影響
変数とプロパティを見つけて解決する方法はJavaScriptの重要な機能の1つですが、同時に理解するのが最も微妙で難しいポイントの1つでもあります。
プロトタイプチェーンまたはスコープに沿って説明した検索操作は、プロパティまたは変数が呼び出されるたびに繰り返されます。 これがループまたは別の重い操作中に発生すると、特に複数の操作が同時に実行されることを防ぐJavaScriptのシングルスレッドの性質の背景に対して、コードパフォーマンスへの影響がすぐに感じられます。
var start = new Date().getTime(); function Parent() { this.delta = 10; }; function ChildA(){}; ChildA.prototype = new Parent(); function ChildB(){} ChildB.prototype = new ChildA(); function ChildC(){} ChildC.prototype = new ChildB(); function ChildD(){}; ChildD.prototype = new ChildC(); function ChildE(){}; ChildE.prototype = new ChildD(); function nestedFn() { var child = new ChildE(); var counter = 0; for(var i = 0; i < 1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += child.delta; } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
上記のコードでは、長い継承ツリーと3つのネストされたループがあります。 最も深いループでは、カウンターは
delta
変数の値で増分します。 しかし、デルタの値は継承ツリーの最上部で決定されます! つまり、
child.delta
を呼び出す
child.delta
ツリー全体がtopからbottomにスキャンされます。 これはパフォーマンスに悪影響を与える可能性があります。
これを実現した後、
nestedFn
の値を
delta
変数に
child.delta
にキャッシュすることで、
nestedFn
のパフォーマンスを簡単に改善できます。
function nestedFn() { var child = new ChildE(); var counter = 0; var delta = child.delta; // cache child.delta value in current scope for(var i = 0; i < 1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += delta; // no inheritance tree traversal needed! } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
当然、
child.delta
の値がループの実行中に変化しないことが確実にわかっている場合にのみ、これを行うことができます。 そうでない場合は、変数値を現在の値で定期的に更新する必要があります。
それでは、
nestedFn
両方のバージョンを実行し、それらの間に顕著なパフォーマンスの違いがあるかどうかを確認しましょう。
diego@alkadia:~$ node test.js Final result: 10000000000 Total time: 8270 milliseconds
完了するのに約8秒かかりました。 これはたくさんあります。
では、最適化されたバージョンで何ができるか見てみましょう
diego@alkadia:~$ node test2.js Final result: 10000000000 Total time: 1143 milliseconds
今回はほんの一瞬です。 ずっと速く!
重いクエリを防ぐためのローカル変数の使用は、プロパティの検索(プロトタイプチェーンに沿った)と変数の解決(スコープを介した)の両方に使用されます。
さらに、そのような値の「キャッシング」(つまり、ローカル変数内)は、いくつかの一般的なJavaScriptライブラリを使用する場合に利益をもたらします。 たとえば、jQueryを取り上げます。 1つ以上のDOM要素を取得するメカニズムである「セレクター」をサポートしています。 これが起こりやすいことは、セレクターの検索がどれだけ難しい操作であるかを忘れるのに役立ちます。 したがって、変数に検索結果を保存すると、パフォーマンスが大幅に向上します。
// DOM $('.container') "n" for (var i = 0; i < n; i++) { $('.container').append(“Line “+i+”<br />”); } // ... // , $('.container') , // DOM "n" var $container = $('.container'); for (var i = 0; i < n; i++) { $container.append("Line "+i+"<br />"); } // ... // DOM $('.container') , // DOM var $html = ''; for (var i = 0; i < n; i++) { $html += 'Line ' + i + '<br />'; } $('.container').append($html);
2番目のアプローチでは、特に多数の要素があるページで、最初のアプローチよりもパフォーマンスが大幅に向上します。
まとめると
JavaScriptデータ検索は他の言語とはまったく異なり、多くのニュアンスがあります。 したがって、この言語を真に習得するには、その概念を完全かつ最も重要に正しく理解する必要があります。 この知識は、よりクリーンで信頼性の高いコードとパフォーマンスの向上をもたらします。