JavaScript中對(duì)循環(huán)語(yǔ)句的優(yōu)化技巧深入探討
循環(huán)是所有編程語(yǔ)言中最為重要的機(jī)制之一,幾乎任何擁有實(shí)際意義的計(jì)算機(jī)程序(排序、查詢等)都里不開(kāi)循環(huán)。 而循環(huán)也正是程序優(yōu)化中非常讓人頭疼的一環(huán),我們往往需要不斷去優(yōu)化程序的復(fù)雜度,卻因循環(huán)而糾結(jié)在時(shí)間復(fù)雜度和空間復(fù)雜度之間的抉擇。
在 javascript 中,有3種原生循環(huán),for () {}, while () {}和do {} while (),其中最為常用的要數(shù)for () {}。
然而for正是 javascript 工程師們?cè)趦?yōu)化程序時(shí)最容易忽略的一種循環(huán)。
我們先來(lái)回顧一下for的基本知識(shí)。
javascript 的for語(yǔ)法繼承自c語(yǔ)言,for循環(huán)的基本語(yǔ)法有兩種使用方法。
1. 循環(huán)數(shù)組
for循環(huán)的基本語(yǔ)法
for ( /* 初始化 */2 /* 判斷條件 */2 /* 循環(huán)處理 */ ) {
//... 邏輯代碼
}
我們以一段實(shí)例代碼來(lái)進(jìn)行詳細(xì)說(shuō)明。
var array = [1, 2, 3, 4, 5];
var sum = 0;
for (var i = 0, len = array.length; i < len; ++i) {
sum += array[i];
}
console.log('The sum of the array\'s items is %d.', sum);
//=> The sum of the array's items is 15.
在這段代碼中,我們首先定義并初始化了一個(gè)用存儲(chǔ)待累加項(xiàng)的數(shù)組和一個(gè)總和整形變量。 接下來(lái),我們開(kāi)始進(jìn)行循環(huán)。在該for循環(huán)的初始化代碼中,我們也定義并初始化了兩個(gè)變量: i(計(jì)數(shù)器)和len(循環(huán)數(shù)組長(zhǎng)度的別名),當(dāng)i小於len時(shí),循環(huán)條件成立,執(zhí)行邏輯代碼;每次邏輯代碼執(zhí)行完畢以后,i自增1。
在循環(huán)的邏輯代碼中,我們把當(dāng)前循環(huán)的數(shù)組項(xiàng)加到總和變量中。
這個(gè)循環(huán)用流程圖表示為如下:
從這個(gè)流程圖中我們不難發(fā)現(xiàn),程序中真正的循環(huán)體不僅有我們的邏輯代碼,還包含了實(shí)現(xiàn)循環(huán)自身的執(zhí)行判斷和循環(huán)處理。
這樣,我們的優(yōu)化思路就清晰了,我們可以從四個(gè)方面進(jìn)行優(yōu)化。
1.循環(huán)體前的初始化代碼
2.循環(huán)體中的執(zhí)行判斷條件
3.邏輯代碼
4.邏輯代碼后的處理代碼
ps: 其中第一點(diǎn)和第二點(diǎn)存在重要關(guān)系。
1.1 優(yōu)化初始化代碼和執(zhí)行判斷條件
我們先來(lái)看看一段大家都非常熟悉的代碼。
// wrong!
for (var i = 02 i < list.length2 ++i) {
//... 邏輯代碼
}
相信現(xiàn)在大部分寫(xiě)著 javascript 的工程師依然使用著這段看似狠正常的循環(huán)方法,但為什麼我在這里說(shuō)它是錯(cuò)誤的呢?
我們把這個(gè)循環(huán)的所有東西都拆開(kāi)來(lái)看看:
1.初始化代碼 - 這段循環(huán)只定義并初始化了一個(gè)計(jì)數(shù)器變量。
2.執(zhí)行判斷條件 - 當(dāng)計(jì)數(shù)器小於list的長(zhǎng)度時(shí)成立。
3.處理代碼 - 計(jì)數(shù)器自增1。
我們?cè)倩仡櫼幌律厦娴牧鞒虉D,發(fā)現(xiàn)有什麼倪端沒(méi)?
真正的循環(huán)體不僅有我們的邏輯代碼,還包含了實(shí)現(xiàn)循環(huán)自身的執(zhí)行判斷和處理代碼。 也就是說(shuō),i < list.length這個(gè)判斷條件是每一次循環(huán)前都要執(zhí)行的。而 javascript 中,對(duì)對(duì)象的屬性或方法進(jìn)行讀取時(shí),需要進(jìn)行一次查詢。
似乎明白了點(diǎn)什麼了吧?這個(gè)判斷條件存在兩個(gè)操作:1. 從list數(shù)組中查詢length屬性;2. 比較i與list.length的大小。
假設(shè)list數(shù)組含有 n 個(gè)元素,則程序需要在這個(gè)循環(huán)的執(zhí)行判斷中進(jìn)行 2n 次操作。
如果我們把代碼改成這樣:
// Well
for (var i = 0, len = list.length; i < len; ++i) {
//...
}
在這段改進(jìn)后的代碼中,我們?cè)谘h(huán)體執(zhí)行前的初始化代碼中, 增加定義并初始化了一個(gè)len變量,用於存儲(chǔ)list.length的值(關(guān)於變量、表達(dá)式、指針和值的相關(guān)內(nèi)容將在第二篇中討論)。 這樣,我們?cè)谘h(huán)體中的執(zhí)行判斷中就無(wú)需再次對(duì)list數(shù)組進(jìn)行屬性查詢,操作數(shù)為原先的一半。
以上步驟我們完善了算法的時(shí)間復(fù)雜度,而如果要繼續(xù)優(yōu)化空間復(fù)雜度的話,要如何做呢? 如果你的邏輯代碼不受循環(huán)順序限制,那你可以嘗試以下優(yōu)化方式。
for (var i = list.length - 1; i >= 0; --i) {
//...
}
這段代碼通過(guò)把循環(huán)順序倒置,把i計(jì)數(shù)器從最后一個(gè)元素下標(biāo)(list.length - 1)開(kāi)始,向前循環(huán)。 以達(dá)到把循環(huán)所需變量數(shù)減到 1 個(gè),而且在執(zhí)行判斷中,降低了變量查詢的次數(shù),減少了執(zhí)行 cpu 指令前的耗時(shí)。
1.2 優(yōu)化邏輯代碼
在循環(huán)中,我們得到循環(huán)當(dāng)前的數(shù)組元素自然是為了對(duì)其或利用其進(jìn)行一些操作,這不免會(huì)出現(xiàn)對(duì)該元素?cái)?shù)次的調(diào)用。
var array = [
{ name: 'Will Wen Gunn', type: 'hentai' },
{ name: 'Vill Lin', type: 'moegril' }
];
for (var i = array.length - 1; i >= 0; --i) {
console.log('Name: %s', array[i].name);
console.log('He/She is a(n) %s', array[i].type);
console.log('\r\n');
}
/*=>
Name: Vill Lin
He/She is a(n) moegril
Name: Will Wen Gunn
He/She is a(n) hentai
*/
這段代碼中,程序需要對(duì)每個(gè)數(shù)組元素的name和type屬性進(jìn)行查詢。 如果數(shù)組有 n 個(gè)元素,程序就進(jìn)行了 4n 次對(duì)象查詢。
1. array[i]
2. array[i].name
3. array[i]
4. array[i].type
相信此時(shí)你一定想到了解決方法了吧,那就是把當(dāng)前數(shù)組元素的值賦值到一個(gè)變量中,然后在邏輯代碼中使用它。
var array = [
{ name: 'Will Wen Gunn', type: 'hentai' },
{ name: 'Vill Lin', type: 'moegril' }
];
var person = null;
for (var i = array.length - 1; i >= 0 && (person = array[i]); --i) {
console.log('Name: %s', person.name);
console.log('He/She is a(n) %s', person.type);
console.log('\r\n');
}
person = null;
這樣看起來(lái)的確美觀了不少。
1. array[i] => var person
2. person.name
3. person.type
有點(diǎn)像 emcascript5 中的foreach,不過(guò)這兩者之間差別狠大,這里不多做解釋。
ps:感謝大家的指正,經(jīng)過(guò)實(shí)驗(yàn)得知,如果數(shù)組內(nèi)的元素是直接傳值定義的,則在循環(huán)中得到值一定是值,而非指針。 所以無(wú)論是定義表達(dá)式還是變量,都會(huì)有額外的內(nèi)存空間請(qǐng)求。
1.3 優(yōu)化處理代碼
實(shí)際上,循環(huán)體中的處理代碼并沒(méi)有太多東西可以進(jìn)行優(yōu)化,i計(jì)數(shù)器也就是自增1就足夠了。
ps:如果有什麼好的建議或方法,歡迎提供。:)
2. 循環(huán)對(duì)象(object)
在 javascript 中,for還可以對(duì) object 的屬性和方法進(jìn)行歷遍。 需要注意的是,for循環(huán)無(wú)法對(duì)對(duì)象所屬的包裝類(lèi)型或是構(gòu)造函數(shù)中原型屬性、方法(prototype)進(jìn)行歷遍。
語(yǔ)法比循環(huán)數(shù)組還要簡(jiǎn)單。
for (/* 初始化 */ var key in object) {
//... 邏輯代碼
}
我們常常這個(gè)方法來(lái)進(jìn)行對(duì)對(duì)象的操作。
var person = {
'name' : 'Will Wen Gunn',
'type' : 'hentai',
'skill' : ['Programming', 'Photography', 'Speaking', 'etc']
};
for (var key in person) {
value = person[key];
// if the value is array, convert it to a string
if (value instanceof Array) {
value = value.join(', ');
}
console.log('%s: %s', key, value);
}
/*=>
name: Will Wen Gunn
type: hentai
skill: Programming, Photography, Speaking, etc
*/
如果你曾使用過(guò) mongodb,那你對(duì)它的 query 機(jī)制絕對(duì)不會(huì)陌生。 因?yàn)?mongodb 的 query 機(jī)制就像是它的 api 的靈魂,靈活的 curd 操作方式為 mongodb 贏得了不少人氣和發(fā)展動(dòng)力。
而在 nanodb 的 mongo api 實(shí)現(xiàn)中,query 的實(shí)現(xiàn)方式就大面積地使用了循環(huán)對(duì)象。
var myDB = nano.db('myDB');
var myColl = myDB.collection('myColl');
var _cursor = myColl.find({
type : 'repo',
language : 'JavaScript'
});
_cursor
.sort({
star: 1
})
.toArray(function(err, rows) {
if (err)
return console.error(err);
console.log(rows);
});
而我們需要優(yōu)化的,并非循環(huán)本身,而是對(duì)你需要進(jìn)行歷遍的對(duì)象進(jìn)行優(yōu)化。
就比如說(shuō) nanodb 中的 nanocollection 類(lèi),雖然表面看上去就是一個(gè)數(shù)組, 存有所有的元素,或是一個(gè)對(duì)象,用元素的 id 作為鍵,然后對(duì)元素進(jìn)行存儲(chǔ)。
但事實(shí)并非如此,曾經(jīng)使用過(guò) underscore 的同學(xué)應(yīng)該會(huì)知道其中的_.invert方法。 這是一個(gè)相當(dāng)有趣的方法,它把所傳入的對(duì)象的鍵與值反過(guò)來(lái)。
var person = {
'name' : 'Will Wen Gunn',
'type' : 'hentai'
};
var _inverted = _.invert(person);
console.log(_inverted);
/*=>
{
'Will Wen Gunn' : 'name',
'hentai' : 'type'
}
*/
如果你是需要使用循環(huán)對(duì)象來(lái)對(duì)對(duì)象的某些屬性的值進(jìn)行查詢,那你就可以嘗試一下以下方法。
var person = {
'name' : 'Will Wen Gunn',
'type' : 'hentai'
};
var name = 'Will Wen Gunn';
var _inverted = _.invert(person);
if (_inverted[name] === 'name') {
console.log('Catched!');
}
//=> Catched!
然而利用for進(jìn)行對(duì)象查詢并沒(méi)有太大的可優(yōu)化之處,一切都還需從實(shí)際需求出發(fā)。: p
接下來(lái)我們來(lái)看看其他兩種循環(huán),while () {}和do {} while ()。 相信任何接收過(guò)計(jì)算機(jī)科學(xué)課程的朋友都這兩個(gè)循環(huán)都不會(huì)陌生。他們唯一的區(qū)別就在與執(zhí)行循環(huán)體的邏輯順序。
while () {}的執(zhí)行順序與for () {}類(lèi)似,執(zhí)行判斷在邏輯代碼之前,不過(guò)省去了初始化和處理代碼。
當(dāng)給予的條件時(shí),便執(zhí)行邏輯代碼,直到條件不再成立為止。
var sum = 0;
while (sum < 10) {
sum += sum + 1;
}
console.log(sum);
//=> 15
do {} while ()則是把執(zhí)行判斷放到了邏輯代碼之后,也就是“先斬后奏”。
var sum = 0;
do {
sum += sum + 1;
} while (sum < 10);
console.log(sum);
//=> 15
while () {}與do {} while ()同樣不需要計(jì)數(shù)器,而是通過(guò)某些條件來(lái)判斷是否執(zhí)行或繼續(xù)執(zhí)行邏輯代碼。
3. while () {}和do {} while ()
while () {}和do {} while ()主要用於業(yè)務(wù)邏輯中,為達(dá)到某一目的而不斷執(zhí)行一系列操作,如任務(wù)隊(duì)列。
但這兩種循環(huán)是危險(xiǎn)的,因?yàn)樗鼈兡J(rèn)只受執(zhí)行條件的控制,如果一旦邏輯代碼內(nèi)一直沒(méi)有對(duì)執(zhí)行判斷產(chǎn)生任何影響,就會(huì)出現(xiàn)死循環(huán)。
var sum = 02
// warning!
while (sum < 10) {
sum = 1 + 12
}
這樣的代碼無(wú)異於while (true) {},所以在使用之前,必須明確執(zhí)行條件和如何對(duì)執(zhí)行條件產(chǎn)生影響的方法。
4. 善用循環(huán)控制語(yǔ)句
相信所有 javascript 工程師都使用過(guò)break語(yǔ)句,但continue語(yǔ)句卻相對(duì)少用。 實(shí)際上,在不少優(yōu)秀的 javascript 開(kāi)源項(xiàng)目中,都能發(fā)現(xiàn)continue的身影。
為了地瞭解continue語(yǔ)句的作用,我們還是先來(lái)看看一段實(shí)例代碼
// Node.js Broadcast Server
var net = require('net');
var util = require('util');
var broadcastServer = net.createServer();
// Client Store
broadcastServer.clients = [];
// Clients Broadcast Method
net.Socket.prototype.broadcast = function(msg) {
var clients = broadcastServer.clients;
// 獲得發(fā)佈廣播的客戶端在集閤中的下標(biāo)
var index = clients.indexOf(this);
for (var i = clients.length - 1; i >= 0; --i) {
if (i === index) {
// 如果為發(fā)佈廣播的客戶端,則結(jié)束當(dāng)前循環(huán)體
continue;
}
currClient = clients[i];
if (!currClient.destroyed) {
currClient.write(
util.format(
'\r[Echo Client %s:%d] %s\nInput: ',
currClient.remoteAddress, currClient.remotePort, msg)
);
}
}
};
// A new client connected
broadcastServer.on('connection', function(client) {
broadcastServer.clients.push(client);
// Welcome
client.write('[Broadcast Server] Welcome!\nInput:');
client.broadcast(client, 'Joined!');
// Message handle
client.on('data', function(msg) {
client.broadcast(msg);
client.write('\rInput:');
});
// Disconnect handle
client.on('end', function() {
client.broadcast('Left!');
})
});
// Bind
broadcastServer.listen(8080, function() {
console.log('Broadcast Server bound.');
});
這段代碼基於 node.js 的net模塊實(shí)現(xiàn)了一個(gè) broadcast server,在其中的broadcast方法中,我們使用了continue語(yǔ)句, 用以實(shí)現(xiàn)將信息向除發(fā)佈廣播的客戶端外的所有已建立連接的客戶端。
代碼內(nèi)容相當(dāng)簡(jiǎn)單, 當(dāng)某一客戶端需要向其他客戶端發(fā)佈廣播時(shí),則調(diào)用該客戶端所對(duì)應(yīng)client對(duì)象的broadcast方法, 在broadcast方法中,程序會(huì)先獲取當(dāng)前客戶端在以緩存的客戶端 socket 集合中的位置下標(biāo), 然后對(duì)所有客戶端 socket 進(jìn)行循環(huán)發(fā)佈,當(dāng)循環(huán)計(jì)數(shù)器來(lái)到之前獲得的位置下標(biāo),則跳過(guò)當(dāng)前循環(huán)體中的邏輯代碼,繼續(xù)下一個(gè)循環(huán)。
相信學(xué)習(xí)過(guò) c/c++ 語(yǔ)言的工程師都會(huì)從各種地方得到這樣一個(gè)忠告:“不要使用 goto 語(yǔ)句?!?/P>
而這個(gè)“臭名昭著”的goto語(yǔ)句其實(shí)就是一個(gè)代碼流程控制器,關(guān)於goto語(yǔ)句的詳細(xì)內(nèi)容這里不會(huì)詳細(xì)說(shuō)明。 然而 javascript 沒(méi)有明顯的goto語(yǔ)句,但從break語(yǔ)句和continue語(yǔ)句中,不難發(fā)現(xiàn) javascript 中g(shù)oto的影子。
這是因?yàn)閎reak語(yǔ)句和continue語(yǔ)句允許接受由一個(gè)定義好的 label 名稱,以進(jìn)行代碼跳轉(zhuǎn)。
我們來(lái)看看 mdn 提供的實(shí)例代碼。
var i, j;
loop1:
for (i = 0; i < 3; i++) { //The first for statement is labeled "loop1"
loop2:
for (j = 0; j < 3; j++) { //The second for statement is labeled "loop2"
if (i == 1 && j == 1) {
continue loop1;
} else {
console.log("i = " + i + ", j = " + j);
}
}
}
// Output is:
// "i = 0, j = 0"
// "i = 0, j = 1"
// "i = 0, j = 2"
// "i = 1, j = 0"
// "i = 2, j = 0"
// "i = 2, j = 1"
// "i = 2, j = 2"
// Notice how it skips both "i = 1, j = 1" and "i = 1, j = 2"
在這段實(shí)例代碼中,實(shí)現(xiàn)了兩層循環(huán),而在每層循環(huán)外都定義了一個(gè)label,用於之后的continue語(yǔ)句進(jìn)行調(diào)用。
第一層循環(huán)在loop1的 label 中,也就是說(shuō)在后面的程序中,如果在continue語(yǔ)句或break語(yǔ)句選擇了loop1 label,就會(huì)跳出最外層循環(huán)。
第二層循環(huán)在頂層循環(huán)中的loop2的 label 中,若在continue語(yǔ)句或break語(yǔ)句中選擇了loop2 label,就會(huì)回到頂層循環(huán)的循環(huán)體內(nèi)。
通過(guò)使用循環(huán)控制語(yǔ)句,我們可以對(duì)原有的循環(huán)執(zhí)行判斷進(jìn)行干涉,以至於可以構(gòu)建出十分復(fù)雜的邏輯系統(tǒng)。 說(shuō)句題外話,linux kernel 中有非常多的goto語(yǔ)句,至於為什麼還是能經(jīng)常聽(tīng)到不要用goto語(yǔ)句之流的言論,就自己 google 吧。
5. 高級(jí)循環(huán)
5.1 展開(kāi)循環(huán)
我們先來(lái)看看兩段代碼,你猜猜哪一個(gè)的性能更加。
// Setup
var array = [
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"],
["DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA", "DATA"]
];
function process(item) {
// Do something with item
}
// 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]);
}
}
// Case 2
for (var i = array.length - 1; i >= 0; i = i - 4) {
for (var j = array[i].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 - 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]);
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 - 3].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]);
}
}
我需要對(duì)array中的所有子數(shù)組的元素進(jìn)行歷遍,有兩種方案,一種是我們平常所使用的方法,另一種是把循環(huán)任務(wù)展開(kāi)。 答案是 case 2 性能更好,因?yàn)樵诿?6 個(gè)元素之間的執(zhí)行判斷都全部刪除了,自然比往常的都要快。
這里我們來(lái)看看一種更給力的解決方案。 如果一個(gè)業(yè)務(wù)環(huán)節(jié)中需要對(duì)大數(shù)據(jù)集進(jìn)行迭代處理,而這個(gè)數(shù)據(jù)集從開(kāi)始迭代起,數(shù)據(jù)量不會(huì)再改變, 那麼可以考慮採(cǎi)用一種名為 duff 裝置的技術(shù)。這項(xiàng)技術(shù)是以其的創(chuàng)造者 tom duff 的名字來(lái)命名的, 這項(xiàng)技術(shù)最先實(shí)現(xiàn)於 c 語(yǔ)言。后來(lái) jeff greenberg 將其移植到 javascript 中,并經(jīng)過(guò) andrew b. king 修改并提出了一種更為高效的版本。
//credit: Speed Up Up Your Site (New Riders, 2003)
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if (leftover > 0) {
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);
這種技術(shù)的工作原理是通過(guò)計(jì)算values的長(zhǎng)度除以 8 以得到需要迭代的次數(shù),并以math.floor()函數(shù)來(lái)保證結(jié)果為整數(shù), 然后再計(jì)算不能被 8 整除時(shí)的餘數(shù),并對(duì)這些元素單獨(dú)進(jìn)行處理,其餘則 8 次為單次展開(kāi)次數(shù)來(lái)進(jìn)行迭代。
我將這種裝置再加以封裝,可以得到一種帶有異步味道的 api。
function duff(array, mapper) {
var n = Math.floor(array.length / 8);
var l = array.length % 8;
var i = 0;
if (l > 0) {
do {
mapper(array[i++]);
} while (--i > 0);
}
do {
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
mapper(array[i++]);
} while (--n > 0);
}
duff([...], function(item) {
//...
});
這里是一組對(duì)於以上三種迭代解決方案的性能測(cè)試及其結(jié)果。http://jsperf.com/spreaded-loop
5.2 非原生循環(huán)
在任何編程語(yǔ)言中,能夠?qū)崿F(xiàn)循環(huán)的,不止語(yǔ)言所提供的原生循環(huán)語(yǔ)句,還可以通過(guò)其他方式來(lái)間接實(shí)現(xiàn)。
讓我們先來(lái)溫習(xí)一下高中數(shù)學(xué)的一點(diǎn)內(nèi)容——數(shù)列的通項(xiàng)公式。
bacause
a[1] = 1
a[n] = 2 * a[n - 1] + 1
so
a[n] + 1 = 2 * a[n - 1] + 2
= 2 * (a[n - 1] + 1)
(a[n] + 1) / (a[n - 1] + 1) = 2
then
a[n] + 1 = (a[n] + 1) / (a[n - 1] + 1) * (a[n - 1] + 1) / (a[n - 2] + 1) * ... * (a[2] + 1) / (a[1] + 1) * (a[i] + 1)
a[n] + 1 = 2 * 2 * ... * 2 * 2
a[n] + 1 = 2^n
a[n] = 2^n - 1
final
a[n] = 2^n - 1
看了上面這段簡(jiǎn)單的演算,估計(jì)你也猜到我們將要討論的內(nèi)容了吧。 是的,我們還可以使用遞歸來(lái)實(shí)現(xiàn)循環(huán)。
遞歸是數(shù)學(xué)和計(jì)算機(jī)科學(xué)中非常重要的一種應(yīng)用方法,它是指函數(shù)在其使用時(shí)調(diào)用其自身。
在 node.js 社區(qū)中,遞歸被用來(lái)實(shí)現(xiàn)一種非常重要的技術(shù):中間件技術(shù)。 這是一段尚未公佈的新版本的 webjs 中的中間件實(shí)現(xiàn)代碼。
/**
* Middlewares run method
* @param {String} url Current request url
* @param {Object} req the request object
* @param {Object} res the response object
* @param {Function} out Complete Callback
* @return {Function} the server
*/
server.runMiddlewares = function(url, req, res, out) {
var index = -1;
var middlewares = this._usingMiddlewares;
// run the next middleware if it is exists
function next(err) {
index++;
// current middleware
var curr = middlewares[index];
if (curr) {
var check = new RegExp(curr.route);
// Check the route
if (check.test(url)) {
try {
function later() {
debug('A middleware says it need to be later on %s', url);
// The dependencies do not right now
if (middlewares.indexOf(curr) !== middlewares.length - 1) {
_later(curr);
index--;
next();
} else {
debug('A middleware dependencies wrong');
// This middleware can not run
out();
}
}
// Run the middleware
if (utils.isFunc(curr.handler)) {
// Normal middleware function
curr.handler(req, res, next, later);
} else if (utils.isObject(curr.handler) && utils.isFunc(curr.handler.emit)) {
// Server object
curr.handler.emit('request', req, res, next, later);
} else {
// There are something wrong about the middleware
next();
}
} catch(err) {
next();
}
} else {
next();
}
} else {
// Out to next step of the pipeline
out();
}
}
// if the middleware depend on other middlewares,
// it can let it later to run
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);
middlewares = _tmp1;
}
// first middleware
next();
return this;
};
雖然這段代碼看上去狠復(fù)雜,不過(guò)如果我們對(duì)其精簡(jiǎn)之后,就清晰許多了。
server.runMiddlewares = function(url, req, res, out) {
var index = -1;
var middlewares = this._usingMiddlewares;
// run the next middleware if it is exists
function next(err) {
index++;
// current middleware
var curr = middlewares[index];
if (curr) {
var check = new RegExp(curr.route);
// Check the route
if (check.test(url)) {
// run the current middleware
curr.handler(req, res, next);
} else {
next();
}
} else {
// Out to next step of the pipeline
out();
}
}
// first middleware
next();
return this;
};
遞歸之所以可以用於中間件系統(tǒng)的實(shí)現(xiàn),是因?yàn)檫f歸是最適合 node.js 中異步 i/o 的程序流程響應(yīng)方式。
在這段中間件實(shí)現(xiàn)代碼中,this._usingmiddlewares為循環(huán)數(shù)組,function next()是循環(huán)體,其中check.test(url)為執(zhí)行判斷條件, 而循環(huán)處理代碼就是循環(huán)體中最前面的index計(jì)數(shù)器自增 1 和next函數(shù)自身的遞歸調(diào)用。
- javascript 循環(huán)語(yǔ)句 while、do-while、for-in、for用法區(qū)別
- js使用for循環(huán)及if語(yǔ)句判斷多個(gè)一樣的name
- Javascript中for循環(huán)語(yǔ)句的幾種寫(xiě)法總結(jié)對(duì)比
- javascript中的循環(huán)語(yǔ)句for語(yǔ)句深入理解
- 用循環(huán)或if語(yǔ)句從json中取數(shù)據(jù)示例
- JavaScript for循環(huán) if判斷語(yǔ)句(學(xué)習(xí)筆記)
- 簡(jiǎn)單學(xué)習(xí)JavaScript中的for語(yǔ)句循環(huán)結(jié)構(gòu)
- 詳解JavaScript中循環(huán)控制語(yǔ)句的用法
- 詳細(xì)談?wù)凧avaScript中循環(huán)之間的差異
相關(guān)文章
javascript實(shí)現(xiàn)的時(shí)間格式加8小時(shí)功能示例
這篇文章主要介紹了javascript實(shí)現(xiàn)的時(shí)間格式加8小時(shí)功能,涉及javascript日期時(shí)間轉(zhuǎn)換與運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2019-06-06javascript實(shí)現(xiàn)切換td中的值
這篇文章主要介紹了javascript實(shí)現(xiàn)切換td中的值的方法,需要的朋友可以參考下2014-12-12JavaScript表單驗(yàn)證實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了JavaScript表單驗(yàn)證的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05JavaScript?CSS解析B站的彈幕可以不擋人物原理及技巧
這篇文章主要為大家介紹了JavaScript?CSS解析B站的彈幕可以不擋人物原理及技巧,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01原生js添加節(jié)點(diǎn)appendChild、insertBefore方式
這篇文章主要介紹了原生js添加節(jié)點(diǎn)appendChild、insertBefore方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10javascript導(dǎo)出csv文件(excel)的方法示例
這篇文章主要給大家介紹了關(guān)于javascript導(dǎo)出csv文件(excel)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用javascript具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08JavaScript箭頭函數(shù)的五種使用方法及三點(diǎn)注意事項(xiàng)
這篇文章主要介紹了JavaScript箭頭函數(shù)的五種使用方法及三點(diǎn)注意事項(xiàng),箭頭函數(shù)是ES6新增的定義函數(shù)的方式,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,需要的朋友可以參考一下2022-08-08