루프는 모든 프로그래밍 언어에서 가장 중요한 메커니즘 중 하나입니다. 실용적인 의미(정렬, 쿼리 등)가 있는 거의 모든 컴퓨터 프로그램에는 루프가 포함되어 있지 않습니다. 루프 역시 프로그램 최적화에 있어 매우 골치 아픈 부분입니다. 우리는 종종 프로그램의 복잡성을 지속적으로 최적화해야 하지만 루프로 인해 시간 복잡도와 공간 복잡도 사이의 선택에 얽매이게 됩니다.
자바스크립트에는 for(){}, while(){}, do{} while() 세 가지 종류의 네이티브 루프가 있는데, 그중 for(){}가 가장 많이 사용됩니다.
그러나 for는 JavaScript 엔지니어가 프로그램을 최적화할 때 가장 쉽게 무시하는 루프 유형입니다.
먼저 for의 기본 지식을 복습해 보겠습니다.
JavaScript의 for 구문은 C 언어에서 상속되었습니다. for 루프의 기본 구문을 사용하는 방법에는 두 가지가 있습니다.
1. 루프 배열
for 루프의 기본 구문
자세히 설명하기 위해 예제 코드를 사용합니다.
for (var i = 0, len = array.length; i < len; i) {
sum = array[i];
}
console.log('배열 항목의 합은 %d입니다.', sum);
//=> 배열 항목의 합은 15입니다.
이 코드에서는 먼저 누적할 항목을 저장할 배열과 합계 정수 변수를 정의하고 초기화합니다. 다음으로 루핑을 시작합니다. for 루프의 초기화 코드에서는 i(카운터)와 len(루프 배열의 길이에 대한 별칭)이라는 두 가지 변수도 정의하고 초기화했습니다. i가 len보다 작으면 루프 조건이 설정되고 논리 코드가 생성됩니다. 코드가 실행된 후 로직이 실행될 때마다 i가 1씩 증가합니다.
루프 논리 코드에서는 현재 루프 배열 항목을 합계 변수에 추가합니다.
이 주기는 다음과 같은 흐름도로 표현됩니다.
이 흐름도에서 프로그램의 실제 루프 본문에는 논리 코드뿐만 아니라 루프 자체를 구현하기 위한 실행 판단 및 루프 처리도 포함되어 있음을 쉽게 찾을 수 있습니다. .
이렇게 하면 우리의 최적화 아이디어가 명확해지고 네 가지 측면에서 최적화할 수 있습니다.
1. 루프 본문 앞의 초기화 코드
2. 루프 본문의 실행 판단 조건
3. 논리 코드
4. 논리 코드 뒤의 처리 코드
ps: 첫 번째 지점과 두 번째 지점 사이에는 중요한 관계가 있습니다.
1.1 초기화 코드 및 실행 판단 조건 최적화
먼저 모두에게 매우 친숙한 코드를 살펴보겠습니다.
1. 초기화 코드 - 이 루프는 카운터 변수만 정의하고 초기화합니다.
2. 실행 판단 조건 - 카운터가 리스트 길이보다 작을 때 참입니다.
3. 처리 코드 - 카운터가 1씩 증가합니다.
위의 흐름도를 다시 검토해 보면 잘못된 점이 있나요?
실제 루프 본문에는 논리 코드뿐만 아니라 루프 자체를 구현하기 위한 실행 판단 및 처리 코드도 포함됩니다. 즉, 각 루프 이전에 판단 조건 i < list.length를 실행해야 합니다. JavaScript에서는 객체의 속성이나 메소드를 읽을 때 쿼리가 필요합니다.
뭔가 이해가 가는 것 같나요? 이 판단 조건에는 두 가지 작업이 있습니다. 1. 목록 배열에서 길이 속성을 쿼리합니다. 2. i와 list.length의 크기를 비교합니다.
목록 배열에 n개의 요소가 포함되어 있다고 가정하면 프로그램은 이 루프의 실행 판단에서 2n개의 연산을 수행해야 합니다.
코드를 다음과 같이 변경하면:
이 개선된 코드에서는 루프 본문이 실행되기 전에 초기화 코드에 정의를 추가하고 len 변수를 초기화했습니다. 이 변수는 list.length의 값(변수, 표현식, 포인터 및 값에 대해)을 저장하는 데 사용됩니다. 내용은 2부에서 다루겠습니다). 이렇게 하면 루프 본문의 실행 판단에서 목록 배열의 속성을 다시 쿼리할 필요가 없으며 작업 횟수도 원래 작업의 절반이 됩니다.
위 단계에서 알고리즘의 시간 복잡도를 개선했지만, 계속해서 공간 복잡도를 최적화하려면 어떻게 해야 할까요? 논리 코드가 루프 순서에 의해 제한되지 않는 경우 다음 최적화 방법을 시도해 볼 수 있습니다.
이 코드는 루프 순서를 바꾸고 마지막 요소 인덱스(list.length - 1)에서 i 카운터를 시작한 다음 앞으로 루프를 돌립니다. 루프 및 실행 판단에 필요한 변수 수를 1개로 줄이기 위해 변수 쿼리 수를 줄이고 CPU 명령을 실행하기까지 소요되는 시간을 줄입니다.
1.2 논리 코드 최적화
루프에서는 일부 작업을 수행하거나 사용하기 위해 자연스럽게 루프의 현재 배열 요소를 가져오며, 이로 인해 필연적으로 요소에 대한 여러 호출이 발생하게 됩니다.
for (var i = array.length - 1; i >= 0; --i) {
console.log('이름: %s', array[i].name);
console.log('그/그녀는 %s입니다', array[i].type);
console.log('rn');
}
/*=>
이름: Vill Lin
그/그녀는 moegril입니다
이름: Will Wen Gunn
그/그녀는 헨타이
*/
이 코드에서 프로그램은 각 배열 요소의 이름 및 유형 속성을 쿼리해야 합니다. 배열에 n개의 요소가 있는 경우 프로그램은 4n개의 객체 조회를 수행합니다.
console.log('이름: %s', person. name);
console.log('그/그녀는 %s입니다', person.type);
}
사람 = null;
ps: 수정해 주셔서 감사합니다. 실험을 통해 배열의 요소가 직접 값 전송으로 정의된 경우 루프에서 얻은 값은 포인터가 아닌 값이어야 한다는 것을 알게 되었습니다. 따라서 표현식을 정의하든 변수를 정의하든 추가 메모리 공간 요구 사항이 있습니다.
1.3 최적화된 처리 코드
실제로 루프 본문의 처리 코드에서 최적화할 수 있는 것은 많지 않습니다. i 카운터가 1씩 증가하는 것만으로도 충분합니다.ps: 좋은 제안이나 방법이 있으면 알려주세요. :)
2. 루프 객체(Object)
자바스크립트에서 for는 객체의 속성과 메소드를 순회할 수도 있습니다. for 루프는 객체가 속한 패키징 유형이나 생성자의 프로토타입 속성 및 메서드(프로토타입)를 순회할 수 없다는 점에 유의해야 합니다.구문은 배열을 반복하는 것보다 간단합니다.
우리는 객체를 조작할 때 이 방법을 자주 사용합니다.
for(var key in person) {
value = person[key];
// 값이 배열이면 문자열로 변환
if (value instanceof Array) {
value = value.join(', ');
}
console.log('%s: %s', key, value);
}
/*=>
이름: Will Wen Gunn
유형: hentai
스킬 : 프로그래밍, 사진촬영, 강연 등
*/
mongodb를 사용해 본 적이 있다면 쿼리 메커니즘에 확실히 익숙할 것입니다. mongodb의 쿼리 메커니즘은 API의 영혼과 같기 때문에 유연한 curd 작업 방법은 mongodb의 많은 인기와 개발 추진력을 얻었습니다.
nanodb의 mongo api 구현에서 쿼리 구현은 루프 개체를 광범위하게 사용합니다.
var _cursor = myColl.find({
유형 : 'repo',
언어 : 'JavaScript'
});
_cursor
.sort({
별표: 1
})
.toArray(function(err,rows) {
if (err)
return console.error( 오류);
console.log(행);
});
최적화해야 할 것은 루프 자체가 아니라 탐색해야 하는 객체의 최적화입니다.
예를 들어 nanodb의 nanocollection 클래스는 겉보기에는 배열처럼 보이지만 모든 요소, 즉 객체를 저장하고, 해당 요소의 id를 키로 사용하여 저장합니다.
하지만 그렇지 않습니다. 이전에 밑줄을 사용한 학생들은 _.invert 방법을 알아야 합니다. 이는 전달된 객체의 키와 값을 반대로 바꾸는 다소 흥미로운 방법입니다.
var _inverted = _.invert(person);
console.log(_inverted);
/*=>
{
'Will Wen Gunn' : 'name',
'헨타이' : '유형'
}
*/
루프 개체를 사용하여 개체의 일부 속성 값을 쿼리해야 하는 경우 다음 방법을 시도해 볼 수 있습니다.
var 이름 = 'Will Wen Gunn';
var _inverted = _.invert(사람);
if (_inverted[name] === 'name') {
console.log('Catched!');
}
//=> 잡았습니다!
그러나 객체 쿼리를 사용하여 최적화할 수 있는 것은 많지 않습니다. 모든 것이 실제 요구 사항을 기반으로 해야 합니다. :피
다음으로 다른 두 루프인 while() {}와 while()을 수행하는 방법을 살펴보겠습니다. 나는 컴퓨터 공학 과정을 수강한 사람이라면 누구나 이 두 사이클에 대해 잘 알고 있을 것이라고 믿습니다. 이들 사이의 유일한 차이점은 루프 본문이 실행되는 논리적 순서입니다.
while(){}의 실행 순서는 for(){}와 유사하지만 실행 판단은 논리 코드 이전에 이루어지지만 초기화 및 처리 코드가 생략됩니다.
조건이 주어지면 조건이 더 이상 참이 아닐 때까지 논리 코드가 실행됩니다.
while (sum sum = sum 1;
}
console.log(sum);
//=> 15
do {} while()은 논리 코드 뒤에 실행 판단을 넣습니다. 즉, "먼저 잘라내고 나중에 재생"합니다.
do {
sum = sum 1;
} while (sum < 10);
console.log(sum);
//=> 15
while() {} 및 do {} while()도 카운터가 필요하지 않지만 특정 조건을 사용하여 논리 코드를 실행할지 아니면 계속 실행할지 결정합니다.
3. while () {} 및 {} while ()
while() {} 및 do {} while()은 작업 대기열과 같은 특정 목적을 달성하기 위해 일련의 작업을 지속적으로 수행하기 위해 비즈니스 로직에서 주로 사용됩니다.
그러나 이 두 종류의 루프는 기본적으로 실행 조건에 의해서만 제어되기 때문에 위험합니다. 논리 코드가 실행 판단에 아무런 영향을 미치지 않으면 무한 루프가 발생합니다.
// 경고!
while (sum < 10) {
sum = 1 12
}
이러한 코드는 while(true){}와 동일하므로 사용하기 전에 실행 조건과 실행 조건에 미치는 영향을 명확히 해야 합니다.
4. 루프 제어문을 잘 활용하세요
모든 JavaScript 엔지니어가 break 문을 사용했다고 생각하지만 continue 문은 상대적으로 거의 사용되지 않습니다. 실제로 continue는 많은 우수한 JavaScript 오픈 소스 프로젝트에서 찾을 수 있습니다.
continue 문의 기능을 더 잘 이해하기 위해 먼저 예제 코드를 살펴보겠습니다
var BroadcastServer = net.createServer();
// 클라이언트 스토어
broadcastServer.clients = [];
// 클라이언트 브로드캐스트 방법
net.Socket.prototype.broadcast = function(msg) {
var 클라이언트 = BroadcastServer.clients;
// Standard 컬렉션에 브로드캐스트를 게시하는 클라이언트 가져오기
var index = 클라이언트.indexOf(this);
for (var i = 클라이언트.길이 - 1; i >= 0; --i) {
if (i === index) {
// 게시하는 클라이언트인 경우 브로드캐스트한 다음 현재 루프를 종료합니다
continue;
}
currClient = 클라이언트[i];
if (!currClient.destroyed) {
currClient.write(
util.format(
) 'r[Echo Client %s:%d] %snInput: ',
currClient. RemoteAddress , currClient.remotePort, msg)
);
}
}
};
// 새 클라이언트 연결
broadcastServer.on('connection', function(client) {
BroadcastServer.clients.push(client);
// 환영합니다
client.write('[방송 서버] 환영합니다!nInput:');
client.broadcast(client, 'Joined!');
// 메시지 핸들
client.on('data', function(msg) {
client.broadcast(msg);
client.write('rInput:');
} );
// 연결 끊기 핸들
client.on('end', function() {
client.broadcast('Left!');
})
});
// 바인딩
broadcastServer.listen(8080, function() {
console.log('브로드캐스트 서버 바운드.');
});
이 코드는 node.js의 net 모듈을 기반으로 브로드캐스트 서버를 구현합니다. 브로드캐스트 메서드에서는 continue 문을 사용하여 브로드캐스트 클라이언트를 게시하는 클라이언트를 제외한 모든 설정된 연결에 정보를 배포합니다.
코드의 내용은 매우 간단합니다. 클라이언트가 다른 클라이언트에 브로드캐스트를 게시해야 하는 경우 클라이언트에 해당하는 클라이언트 개체의 브로드캐스트 메서드가 호출됩니다. 브로드캐스트 메서드에서는 프로그램이 먼저 캐시를 가져옵니다. 클라이언트 소켓 컬렉션의 현재 클라이언트 위치 인덱스를 가져온 다음 루프의 모든 클라이언트 소켓을 게시합니다. 루프 카운터가 이전에 얻은 위치 인덱스에 도달하면 현재 루프 본문의 논리 코드를 건너뛰고 다음 루프가 계속됩니다. .
C/C 언어를 공부한 엔지니어라면 여기저기서 "goto 문을 사용하지 마세요"라는 조언을 듣게 될 거라 믿습니다.
이 "악명 높은" goto 문은 실제로 코드 흐름 컨트롤러입니다. 여기서는 goto 문의 세부 사항을 자세히 설명하지 않습니다. 하지만 자바스크립트에는 뚜렷한 goto 문이 없지만, break 문과 continue 문을 보면 자바스크립트에서 goto의 그림자를 찾는 것은 어렵지 않습니다.
이는 break 문과 continue 문이 코드 점프에 대해 정의된 레이블 이름을 허용하기 때문입니다.
mdn에서 제공하는 예제 코드를 살펴보겠습니다.
loop1:
for (i = 0; i < 3; i ) { //첫 번째 for 문은 "loop1"로 표시됩니다.
loop2:
for (j = 0; j < 3; j ) { //두 번째 for 문은 "loop2"로 표시됩니다.
if (i == 1 && j == 1) {
continue loop1;
console.log ("i = " i ", j = " j);
}
}
}
// "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"
// "i = 1, j = 1"과 "i = 1, j = 2"를 모두 건너뛰는 방법에 주목하세요
첫 번째 루프는 loop1의 레이블에 있습니다. 즉, 후속 프로그램에서 continue 문이나 break 문에서 loop1 레이블을 선택하면 가장 바깥쪽 루프가 점프됩니다.
두 번째 수준 루프는 최상위 루프의 loop2 레이블에 있습니다. continue 문이나 break 문에서 loop2 레이블을 선택하면 최상위 루프의 루프 본문으로 돌아갑니다.
5. 고급 루프
5.1 루프 풀기
먼저 두 가지 코드를 살펴보고 어떤 코드가 더 나은 성능을 발휘하는지 추측해 보세요.
// 사례 1
for (var i = array.length - 1; i >= 0; i--) {
for (var j = array[i].length - 1; j >= 0; i--) {
프로세스(배열[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) {
process(array[i][j]);
process(array[i][j - 1]);
process(array[i][j - 2]);
프로세스(배열[i][j - 3]);
프로세스(배열[i][j - 4]);
프로세스(배열[i][j - 5]);
}
}
저희는 如果一个业务环节中需要对大数据集进行迭代处理,而这个数据集从开始迭代起,数据weight不会再改变, 那麼可以考虑採用더프의 이름은 톰 더프의 이름을 딴 것입니다. 그는 제프 그린버그의 이름을 딴 것입니다.其移植到 javascript 中,并经过 앤드류 b . king 修改并提出了一种更为高效的版本。
if (leftover > 0) {
do {
process(values[i ]);
} while (--leftover > 0);
}
do {
프로세스(값[i ]);
프로세스(값[i ]);
프로세스(값[i ]);
프로세스(값[i ]);
프로세스(값 [i ]);
process(values[i ]);
process(values[i ]);
process(values[i ]);
} while (--iterations > 0 );
这种技术的工WORK원리是通过计算values的长道除以 8 以得到需要迭代的次数,并以math.floor()函数来保证结果为整数, 然后再计算不能被 8 整除时的餘数,并对这些元素单独进行处理 ,其餘则 8 次为单次迭代。
我将这种装置再加以封装,可以得到一种带有异步味道的 api。
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) {
//...
});
다음은 위의 세 가지 반복 솔루션에 대한 일련의 성능 테스트 및 결과입니다. http://jsperf.com/spreaded-loop
5.2 비네이티브 루프
모든 프로그래밍 언어에서 루프는 해당 언어에서 제공하는 기본 루프 문을 통해서뿐만 아니라 다른 방법을 통해 간접적으로 구현할 수도 있습니다.
먼저 고등학교 수학의 내용인 수열의 일반 공식을 복습해 보겠습니다.
그러니까
~ a[n] 1 = 2 * a[n - 1] 2 [n - 1] 1) = 2
그런 다음
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);
index --;
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 = 미들웨어[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씩 증가하고 다음 함수 자체가 재귀적으로 호출됩니다.