ループは、すべてのプログラミング言語で最も重要なメカニズムの 1 つです。実用的な意味を持つほとんどすべてのコンピューター プログラム (並べ替え、クエリなど) にはループが含まれません。 ループはプログラムの最適化において非常に厄介な部分でもあり、プログラムの複雑さを常に最適化する必要があることがよくありますが、ループがあるため、時間の複雑さと空間の複雑さの選択に悩まされます。
JavaScript には、for () {}、while () {}、do {} while () の 3 種類のネイティブ ループがあり、その中で最もよく使用されるのは for () {} です。
ただし、for は、JavaScript エンジニアがプログラムを最適化するときに最も簡単に無視するタイプのループです。
まずはforの基礎知識を復習しましょう。
JavaScript の for 構文は C 言語から継承されています。for ループの基本構文を使用するには 2 つの方法があります。
1. ループ配列
for ループの基本構文
サンプルコードを使用して詳しく説明します。
for (var i = 0, len = array.length; i < len; i) {
sum = array[i];
}
console.log('配列の項目の合計は %d.', sum);
//=> 配列の項目の合計は 15 です。
このコードでは、まず、累算する項目と合計整数変数を格納する配列を定義して初期化します。 次に、ループを開始します。 for ループの初期化コードでは、i (カウンター) と len (ループ配列の長さのエイリアス) という 2 つの変数も定義して初期化しました。i が len より小さい場合、ループ条件が確立され、ロジック コードが実行されます。ロジックが実行されるたびに、コードが実行されると、i が 1 ずつ増加します。
ループ ロジック コードでは、現在のループ配列項目を sum 変数に追加します。
このサイクルは次のようなフローチャートで表されます:
このフローチャートから、プログラム内の実際のループ本体には、ロジック コードだけでなく、ループ自体を実装するための実行判定やループ処理も含まれていることが簡単にわかります。 。
このように、最適化の考え方が明確になり、4つの側面から最適化することができます。
1. ループ本体前の初期化コード
2. ループ本体内の実行判定条件
3. ロジックコード
4. ロジックコード以降の処理コード
追記: 最初のポイントと 2 番目のポイントの間には重要な関係があります。
1.1 初期化コードと実行判定条件の最適化
まず、誰もがよく知っているコードの一部を見てみましょう。
1. 初期化コード - このループはカウンター変数の定義と初期化のみを行います。
2. 実行判定条件 - カウンタがリストの長さ未満の場合に true。
3. コードの処理 - カウンターが 1 増加します。
上のフローチャートをもう一度見直してください。何か間違っている点はありませんか?
実際のループ本体には、ロジック コードだけでなく、ループ自体を実装するための実行判定および処理コードも含まれます。 つまり、各ループの前に判定条件 i < list.length を実行する必要があります。 JavaScript では、オブジェクトのプロパティまたはメソッドを読み取るときにクエリが必要です。
何か理解できたようですね?この判定条件には 2 つの操作があります: 1. list 配列から length 属性を問い合わせる 2. i と list.length のサイズを比較する。
リスト配列の要素数がn個だとすると、プログラムはこのループの実行判定で2n回の演算を行う必要があります。
コードを次のように変更すると、
この改善されたコードでは、ループ本体が実行される前に、初期化コードで定義を追加し、len 変数を初期化しました。これは list.length の値を格納するために使用されます (変数、式、ポインター、および値について)。内容については後半で説明します)。 これにより、ループ本体の実行判定でリスト配列の属性を再度問い合わせる必要がなくなり、演算回数が半分で済みます。
上記の手順でアルゴリズムの時間計算量を改善しましたが、空間計算量を引き続き最適化したい場合は、どうすればよいでしょうか? ロジック コードがループ順序によって制限されていない場合は、次の最適化方法を試すことができます。
このコードはループの順序を逆にし、最後の要素インデックス (list.length - 1) から i カウンターを開始し、順方向にループします。 ループおよび実行判定に必要な変数の数を1つに減らすため、変数のクエリ数が減り、CPU命令実行までの時間が削減されます。
1.2 ロジックコードの最適化
ループでは、何らかの操作を実行したり使用したりするために、ループの現在の配列要素を自然に取得します。これにより、必然的に要素への複数の呼び出しが発生します。
for (var i = array.length - 1; i >= 0; --i) {
console.log('Name: %s', array[i].name);
console.log('彼/彼女は a(n) %s', array[i].type);
console.log('rn');
}
/*=>
名前: Vill Lin
彼/彼女はモーグリルです
名前: ウィル・ウェン・ガン
彼/彼女は変態です
*/
このコードでは、プログラムは各配列要素の名前と型属性をクエリする必要があります。 配列に n 個の要素がある場合、プログラムは 4n 回のオブジェクト検索を実行します。
console.log('名前: %s', person. name);
console.log('彼/彼女は a(n) %s', person.type);
}
person = null;
ps: 修正していただきありがとうございます。実験の結果、配列内の要素が直接値転送によって定義されている場合、ループで取得される値はポインタではなく値でなければならないことがわかりました。 したがって、式を定義するか変数を定義するかに関係なく、追加のメモリ領域が必要になります。
1.3 最適化された処理コード
実際には、ループ本体の処理コードで最適化できる部分はあまりなく、i カウンタが 1 増加するだけで十分です。ps: 何か良い提案や方法があれば、提供してください。 :)
2. ループオブジェクト(オブジェクト)
JavaScript では、for はオブジェクトのプロパティとメソッドをトラバースすることもできます。 for ループは、オブジェクトが属するパッケージング タイプや、コンストラクター内のプロトタイプのプロパティとメソッド (プロトタイプ) をトラバースできないことに注意してください。構文は配列をループするよりも簡単です。
私たちはオブジェクトを操作するためにこのメソッドをよく使用します。
for (個人の var key) {
value = person[key];
// 値が配列の場合、文字列に変換します
if (value instanceof Array) {
value = value.join(', ');
}
console.log('%s: %s', key, value);
}
/*=>
名前: ウィル・ウェン・ガン
タイプ: エロアニメ
スキル: プログラミング、写真、スピーキングなど
*/
mongodb を使用したことがある場合は、そのクエリ メカニズムについてよくご存じでしょう。 mongodb のクエリ メカニズムはその API の魂のようなものであるため、柔軟な curd 操作方法により mongodb は多くの人気と開発の勢いを獲得しました。
nanodb の mongo API 実装では、クエリ実装でループ オブジェクトが広範囲に使用されます。
var _cursor = myColl.find({
タイプ : 'repo',
言語 : 'JavaScript'
});
_cursor
.sort({
star: 1
})
.toArray(function(err, rows) {
if (err)
return console.error(えー);
console.log(rows);
});
最適化する必要があるのは、ループ自体ではなく、通過する必要があるオブジェクトの最適化です。
例えば、nanodb の nanocollection クラスは、表面的には配列のように見えますが、すべての要素、つまりオブジェクトを格納し、要素の ID をキーとして要素を格納します。
しかし、これは当てはまりません。これまでにアンダースコアを使用したことのある生徒は、_.invert メソッドを知っているはずです。 これは、渡されたオブジェクトのキーと値を逆にする、かなり興味深いメソッドです。
var _inverted = _.invert(person);
console.log(_inverted);
/*=>
{
'ウィル・ウェン・ガン' : '名前',
'hentai' : 'type'
}
*/
ループ オブジェクトを使用してオブジェクトの一部のプロパティの値をクエリする必要がある場合は、次の方法を試すことができます。
var name = 'ウィル・ウェン・ガン';
var _inverted = _.invert(人);
if (_inverted[name] === 'name') {
console.log('キャッチ!');
}
//=> キャッチ!
ただし、for オブジェクト クエリを使用して最適化できることはあまりありません。すべては実際のニーズに基づいている必要があります。 :p
次に、他の 2 つのループ、while () {} と do {} while () を見てみましょう。 コンピューター サイエンスのコースを受講したことのある人なら、これら 2 つのサイクルについてよく知っていると思います。それらの唯一の違いは、ループ本体が実行される論理順序です。
while () {} の実行シーケンスは for () {} と同様ですが、実行判定はロジック コードの前にありますが、初期化および処理コードは省略されています。
条件が与えられると、その条件が真でなくなるまでロジック コードが実行されます。
while (sum sum = sum 1;
}
console.log(sum);
//=> 15
do {} while () は、ロジック コードの後に実行判断を置きます。つまり、「最初にカットして後で再生する」ということです。
do {
sum = sum 1;
} while (sum
console.log(sum);
//=> 15
while () {} と do {} while () もカウンタを必要としませんが、特定の条件を使用してロジック コードを実行するか実行を継続するかを決定します。
3. while () {} と do {} while ()
while () {} と do {} while () は主にビジネス ロジックで使用され、タスク キューなどの特定の目的を達成するために一連の操作を継続的に実行します。
しかし、これら 2 種類のループは、デフォルトでは実行条件によってのみ制御されるため、ロジック コードが実行判定に影響を与えない場合、無限ループが発生するため危険です。
// 警告!
while (sum sum = 1 12
}
このようなコードは while (true) {} に等しいため、使用する前に実行条件と実行条件にどのような影響を与えるかを明確にする必要があります。
4. ループ制御ステートメントを上手に活用する
JavaScript エンジニアなら誰もが Break ステートメントを使用したことがあると思いますが、Continue ステートメントは比較的まれに使用されます。 実際、Continue は多くの優れた JavaScript オープンソース プロジェクトで見つかります。
Continue ステートメントの機能をより深く理解するために、まずコード例を見てみましょう
varBroadcastServer = net.createServer();
// クライアント ストア
broadcastServer.clients = [];
// Clients Broadcast Method
net.Socket.prototype.broadcast = function(msg) {
var client = BroadcastingServer.clients;
// コレクションでブロードキャストを公開するクライアントを取得 Standard
varindex = client.indexOf(this);
for (var i = client.length - 1; i >= 0; --i) {
if (i === Index) {
// クライアントがブロードキャストし、現在のループを終了します
continue;
}
currClient = クライアント[i];
if (!currClient.destroyed) {
currClient.write(
util.format(
) 'r[Echo Client %s:%d] %snInput: ',
currClient.リモートアドレス、currClient.remotePort、msg)
);
}
}
};
// 新しいクライアントが接続されました
broadcastServer.on('connection', function(client) {
BroadcastingServer.clients.push(client);
// ようこそ
client.write('[ブロードキャスト サーバー] ようこそ!nInput:');
client.broadcast(client, '参加しました!');
// メッセージハンドル
client.on('data', function(msg) {
client.broadcast(msg);
client.write('rInput:');
} );
// ハンドルを切断します
client.on('end', function() {
client.broadcast('Left!');
})
});
// Bind
broadcastServer.listen(8080, function() {
console.log('ブロードキャストサーバーがバインドされています。');
});
このコードは、node.js の net モジュールに基づいてブロードキャスト サーバーを実装します。ブロードキャスト メソッドでは、ブロードキャスト クライアントを発行するクライアントを除くすべての確立された接続に情報を配布します。
コードの内容は非常に単純です。クライアントが他のクライアントにブロードキャストを公開する必要がある場合、そのクライアントに対応するクライアント オブジェクトのブロードキャスト メソッドが呼び出されます。ブロードキャスト メソッドでは、プログラムはまずキャッシュを取得します。クライアント ソケット コレクション内の現在のクライアントの位置インデックスを取得し、ループ内のすべてのクライアント ソケットをパブリッシュします。ループ カウンタが以前に取得した位置インデックスに達すると、現在のループ本体のロジック コードがスキップされ、次のループが続行されます。 。
C/C 言語を勉強したことのあるエンジニアは、「goto 文を使用しないでください」というアドバイスをさまざまな場所から受け取ると思います。
この「悪名高い」goto ステートメントは、実際にはコード フロー コントローラーです。ここでは goto ステートメントの詳細については説明しません。 ただし、JavaScript には明らかな goto ステートメントはありませんが、break ステートメントと continue ステートメントから、JavaScript における goto の影を見つけるのは難しくありません。これは、break ステートメントと continue ステートメントにより、コード ジャンプ用に定義されたラベル名を受け入れることができるためです。
mdn が提供するサンプルコードを見てみましょう。
for (i = 0; i loop2:
for (j = 0; j if (i == 1 && j == 1) {
continue loop1;
console.log ("i = " i ", j = " j);
}
}
}
// 出力は次のとおりです:
// "i = 0, j = 1"
// "i = 0, j = 2"
// "i = 1, j = 0"
// "i = 2, j = 0"
// "i = 2, j = 1"
// "i = 2, j = 2"
// "i = 1, j = 1" と "i = 1, j = 2"
の両方をスキップすることに注目してください。
最初のループはloop1のラベルにあります。つまり、以降のプログラムでは、 continue文やbreak文でloop1のラベルが選択されていれば、一番外側のループが飛び出すことになります。
第 2 レベルのループは、最上位ループのループ 2 のラベル内にあり、 continue ステートメントまたは Break ステートメントでループ 2 ラベルが選択されている場合、最上位ループのループ本体に戻ります。
ループ制御文を使用することで、本来のループ実行判定に干渉することができ、非常に複雑な論理システムを構築することができます。 余談ですが、Linux カーネルには goto ステートメントがたくさんあります。なぜ今でも goto ステートメントを使用しないでくださいという意見をよく聞きます。グーグルで調べてください。
5.1 アンロールループ
まず 2 つのコードを見て、どちらのパフォーマンスが優れているかを考えてみましょう。
// Case 1
for (var i = array.length - 1; i >= 0; i--) {
for (var j = array[i].length - 1; j >= 0; i--) {
process(array[i][j]);
}
}
// ケース 2
for (var i = array.length - 1; i >= 0; i = i - 4) {
for (var j = array[i].length - 1 ; j >= 0; j = j - 6) {
プロセス(配列[i][j]);
プロセス(配列[i][j - 1]);
プロセス(配列) [i][j - 2]);
process(array[i][j - 3]);
process(array[i][j - 4]);
process(array[i] ][j - 5]);
}
for (var j = array[i - 1].length - 1; j >= 0; j = j - 6) {
process(array [i][j]);
process(array[i][j - 1]);
process(array[i][j - 2]);
process(array[i][ j - 3]);
process(array[i][j - 4]);
process(array[i][j - 5]);
}
for (var j = array[i - 2].length - 1; j >= 0; j = j - 6) {
process(array[i][j]);
process(array[i][j - 1]);
プロセス(配列[i][j - 2]);
プロセス(配列[i][j - 3]);
プロセス(配列[i][j - 4] );
process(array[i][j - 5]);
}
for (var j = array[i - 3].length - 1; j >= 0; j = j - 6) {
プロセス(array[i][j]);
プロセス(array[i][j - 1]);
process(array[i][j - 2]);
プロセス(配列[i][j - 3]);
プロセス(配列[i][j - 4]);
プロセス(配列[i][j - 5]);
}
}
do {
process(values[i ]);
} while (--leftover > 0);
}
do {
process(values[i ]);
process(values[i ]);
process(values[i ]);
process(values[i ]);
process(values [i ]);
process(values[i ]);
process(values[i ]);
process(values[i ]);
} while (--iterations > 0 );
これらの要素は個別に処理され、8 回は 1 回の展開回数として実行されます。
私たちはこのデバイスをさらにパッケージ化して、強烈な風味を持つ API を取得できるようにしました。 var i = 0;
if (l > 0) {
do {
マッパー(array[i ]);
} while (--i > 0);
}
do {
マッパー(配列[i]);
マッパー(配列[i]);
マッパー(配列[i]);
マッパー(配列[i] );
マッパー(配列[i]);
マッパー(配列[i]);
マッパー(配列[i]);
マッパー(配列[i]);
} while (--n > 0);
}
duff([...], function(item) {
//...
});
ここでは、上記の 3 つの反復ソリューションの一連のパフォーマンス テストと結果を示します。 http://jsperf.com/spreaded-loop
5.2 非ネイティブループ
どのプログラミング言語でも、ループはその言語が提供するネイティブ ループ ステートメントを通じてだけでなく、他のメソッドを通じて間接的に実装することもできます。
最初に高校数学の内容、つまり数列の一般式を復習しましょう。
so
a [n] 1 = 2 * a [n -1] 2 [n -1] 1)= 2
then
a[n] 1 = 2 * 2 * ... * 2 * 2
a[n] 1 = 2^n
a[n] = 2^n - 1
最終
再帰は、数学とコンピューターサイエンスにおいて非常に重要な応用方法です。これは、関数が使用されるときにそれ自体を呼び出すことを意味します。
node.js コミュニティでは、再帰は非常に重要なテクノロジであるミドルウェア テクノロジを実装するために使用されます。 これは、まだリリースされていない新しいバージョンの webjs のミドルウェア実装コードの一部です。
var middlewares = this._usingMiddlewares;
// 次のミドルウェアが存在する場合は実行します
function next(err) {
index ;
// 現在のミドルウェア
var curr = middlewares[index];
if (curr) {
var check = new RegExp(curr.route);
// ルートを確認します
if (check.test(url)) {
try {
function Later() {
debug('ミドルウェアは、後で実行する必要があると言っています % s', url);
// 現時点では依存関係はありません
if (middlewares.indexOf(curr) !== middlewares.length - 1) {
_later(curr);
インデックス--;
next();
} else {
debug('ミドルウェアの依存関係が間違っています');
// このミドルウェアは実行できません
out();
}
}
// ミドルウェアを実行します
if (utils.isFunc(curr.handler)) {
// 通常のミドルウェア関数
curr.handler(req, res, next, Later);
} else if (utils.isObject(curr.handler) && utils.isFunc(curr.handler.emit)) {
// サーバー オブジェクト
curr.handler.emit('request', req, res, next, Later);
} else {
// ミドルウェアに問題があります
next();
}
} catch(err) {
next();
}
} else {
next();
}
} else {
// パイプラインの次のステップに進みます
out();
}
}
// ミドルウェアが他のミドルウェアに依存している場合、
// 後で実行できるようにすることができます
function _later(curr) {
var i = middlewares.indexOf(curr);
var _tmp1 = middlewares.slice(0, i);
_tmp1.push(middlewares[i 1], curr);
var _tmp2 = middlewares.slice(i 2);
[].push. apply(_tmp1, _tmp2);
ミドルウェア = _tmp1;
}
// 最初のミドルウェア
next();
これを返します;
};
は、このセグメントのコードを確認した上で、その正確な確認を行った後ではなく、多数のコードを削除します。 >代幣如下:
var middlewares = this._usingMiddlewares;
// 次のミドルウェアが存在する場合は実行します
function next(err) {
Index ;
// 現在のミドルウェア
var curr = middlewares[index];
if (curr) {
var check = new RegExp(curr.route);
// ルートを確認します
if (check.test(url)) {
// 現在のミドルウェアを実行します
curr.handler(req, res, next);
} else {
next();
}
} else {
// パイプラインの次のステップに進みます
out();
}
}
// 最初のミドルウェア
next();
これを返します;
};
ミドルウェア システムの実装で再帰が使用できる理由は、node.js の非同期 I/O に対するプログラム フローの応答方法として再帰が最適であるためです。
このミドルウェア実装コードでは、this._usingmiddlewaresがループ配列、関数next()がループ本体、check.test(url)が実行判定条件、ループ処理コードがループ本体の先頭となります。インデックス カウンターが 1 ずつインクリメントされ、次の関数自体が再帰的に呼び出されます。