[javascript] 루프 내부의 JavaScript 클로저 – 간단한 실제 예

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

이것을 출력합니다 :

나의 가치 : 3
나의 가치 : 3
나의 가치 : 3

반면 출력을 원합니다.

내 값 : 0
내 값 : 1
내 값 : 2


이벤트 리스너를 사용하여 함수 실행 지연이 발생하는 경우에도 동일한 문제점이 발생합니다.

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>

또는 비동기 코드 (예 : 약속 사용) :

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

편집 : 그것은 또한 명백 for in하고 for of반복됩니다 :

const arr = [1,2,3];
const fns = [];

for(i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(f of fns){
  f();
}

이 기본 문제에 대한 해결책은 무엇입니까?



답변

문제는 i각각의 익명 함수 내의 변수가 함수 외부의 동일한 변수에 바인딩되어 있다는 것입니다.

클래식 솔루션 : 마감

당신이하고 싶은 것은 각 함수 내의 변수를 함수 외부의 변하지 않는 별도의 값에 바인딩하는 것입니다.

var funcs = [];

function createfunc(i) {
  return function() {
    console.log("My value: " + i);
  };
}

for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

함수 작성을 새 함수로 랩핑하여 JavaScript에는 함수 범위 만있는 블록 범위가 없으므로 “i”값이 의도 한대로 유지되도록합니다.


ES5.1 솔루션 : forEach

Array.prototype.forEach함수의 가용성이 비교적 넓기 때문에 (2015 년) 반복되는 상황에서 주로 일련의 값을 .forEach()반복하여 모든 반복에 대해 뚜렷한 폐쇄를 얻을 수있는 깨끗하고 자연스러운 방법을 제공 한다는 점은 주목할 가치가 있습니다. 즉, 값 (DOM 참조, 객체 등)을 포함하는 일종의 배열이 있고 각 요소에 특정한 콜백을 설정하는 데 문제가 있다고 가정하면 다음을 수행 할 수 있습니다.

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

.forEach루프 와 함께 사용되는 콜백 함수를 호출 할 때마다 고유 한 클로저가됩니다. 해당 핸들러로 전달 된 매개 변수는 반복의 특정 단계에 고유 한 배열 요소입니다. 비동기 콜백에서 사용되는 경우 반복의 다른 단계에서 설정된 다른 콜백과 충돌하지 않습니다.

jQuery에서 작업 $.each()하는 경우 비슷한 기능을 제공합니다.


ES6 솔루션 : let

ECMAScript 6 (ES6)에는 기반 변수 와 다른 범위의 새로운 키워드 letconst키워드가 도입되었습니다 var. 예를 들어, let인덱스를 기반 으로하는 루프에서 루프를 반복 할 때마다 i루프 범위를 가진 새 변수 가 있으므로 코드가 예상대로 작동합니다. 많은 자료가 있지만 2ality의 블록 범위 게시물 을 훌륭한 정보 출처로 추천합니다.

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

그러나 Edge 14 이전의 IE9-IE11 및 Edge let는 위의 오류를 지원 하지만 위와 i같은 오류가 발생 var합니다. Edge 14는 마침내 그것을 올바르게 얻습니다.


답변

시험:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}

편집 (2014) :

개인적으로 @Aust의 최근 답변은.bind 이러한 종류의 작업을 수행하는 가장 좋은 방법이라고 생각합니다. LO-대시 밑줄의 /도 있습니다 _.partial당신이 필요하거나 건드리고 싶지 않을 때 bindthisArg.


답변

아직 언급되지 않은 또 다른 방법은 Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

최신 정보

@squint와 @mekdev에서 지적했듯이 먼저 루프 외부에서 함수를 만든 다음 루프 내에서 결과를 바인딩하면 성능이 향상됩니다.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


답변

즉시 호출되는 함수 표현식을 사용하여 인덱스 변수를 묶는 가장 간단하고 읽기 쉬운 방법 :

for (var i = 0; i < 3; i++) {

    (function(index) {

        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value:   $.ajax({});

    })(i);

}

이것은 i우리가로 정의한 익명 함수로 반복자 를 보냅니다 index. 이렇게하면 iIIFE 내의 비동기 기능에서 나중에 사용하기 위해 변수 가 저장 되는 클로저가 생성 됩니다.


답변

파티에 늦었지만 오늘이 문제를 탐색하고 있었고 많은 답변이 Javascript가 범위를 처리하는 방법을 완전히 다루지 않았 음을 발견했습니다. 이것이 본질적으로 요약됩니다.

많은 다른 사람들이 언급했듯이 문제는 내부 함수가 동일한 i변수를 참조한다는 것 입니다. 그렇다면 매번 반복 할 때마다 새로운 로컬 변수를 만들고 대신 내부 함수 참조를 사용하지 않겠습니까?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

이전과 마찬가지로 각 내부 함수가에 할당 된 마지막 값을 출력 한 i경우 이제 각 내부 함수는에 할당 된 마지막 값을 출력합니다 ilocal. 그러나 각 반복마다 고유하지 않아야합니다.ilocal 합니까?

그게 문제입니다. 각 반복은 동일한 범위를 공유하므로 첫 번째 이후의 모든 반복은 그냥 덮어 씁니다 ilocal. 에서 MDN :

중요 : JavaScript에는 블록 범위가 없습니다. 블록과 함께 도입 된 변수는 포함 함수 또는 스크립트로 범위가 지정되며 변수 설정의 효과는 블록 자체를 넘어서 유지됩니다. 다시 말해, 블록 명령문은 범위를 도입하지 않습니다. “독립형”블록은 유효한 구문이지만 C 또는 Java에서 이러한 블록과 같은 것으로 생각되면 자신이 생각하는대로하지 않기 때문에 JavaScript에서 독립형 블록을 사용하고 싶지 않습니다.

강조를 위해 반복 :

JavaScript에는 블록 범위가 없습니다. 블록으로 도입 된 변수는 포함 함수 또는 스크립트 범위

ilocal각 반복에서 선언하기 전에 확인하여이를 확인할 수 있습니다 .

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

이것이 바로이 버그가 너무 까다로운 이유입니다. 변수를 다시 선언하더라도 Javascript는 오류를 발생시키지 않으며 JSLint는 경고를 발생시키지 않습니다. 또한이를 해결하는 가장 좋은 방법은 클로저를 활용하는 것입니다. 이는 본질적으로 자바 스크립트에서 내부 범위가 외부 범위를 “포함”하기 때문에 내부 함수가 외부 변수에 액세스 할 수 있다는 아이디어입니다.

폐쇄

이는 내부 함수가 외부 변수를 “유지”하고 외부 함수가 리턴 되더라도 계속 유지함을 의미합니다. 이를 활용하기 위해 순전히 래퍼 함수를 ​​생성하고 호출하여 새 범위를 만들고 새 범위에서 선언 ilocal한 다음 사용하는 내부 함수를 반환합니다 ilocal(아래 설명 참조).

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

랩퍼 함수 내에 내부 함수를 작성하면 내부 함수에만 액세스 할 수있는 “클로저”개인 환경이 제공됩니다. 따라서 래퍼 함수를 ​​호출 할 때마다 자체 별도의 환경으로 새로운 내부 함수를 만들어 ilocal변수가 서로 충돌하고 덮어 쓰지 않도록합니다. 몇 가지 사소한 최적화는 다른 많은 SO 사용자가 제공 한 최종 답변을 제공합니다.

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

최신 정보

이제 ES6을 주류로 삼 으면 새로운 let키워드를 사용하여 블록 범위 변수를 만들 수 있습니다.

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

지금 얼마나 쉬운 지보세요! 자세한 내용은 내 정보가 기반으로하는 this answer을 참조하십시오 .


답변

ES6가 널리 지원됨에 따라이 질문에 대한 최상의 답변이 변경되었습니다. ES6는 이 정확한 상황에 대한 letconst키워드를 제공합니다 . 클로저를 어지럽히 지 let않고 다음과 같이 루프 범위 변수를 설정하는 데 사용할 수 있습니다 .

var funcs = [];

for (let i = 0; i < 3; i++) {
    funcs[i] = function() {
      console.log("My value: " + i);
    };
}

val그런 다음 루프의 특정 회전과 관련된 객체를 가리키고 추가 클로저 표기법없이 올바른 값을 반환합니다. 이것은 분명히이 문제를 단순화시킵니다.

const 비슷하다 let초기 할당 후 변수 이름을 새 참조로 리바운드 할 수 없다는 추가 제한 사항과 합니다.

최신 버전의 브라우저를 대상으로하는 사용자를위한 브라우저 지원이 제공됩니다. const/ let는 현재 최신 Firefox, Safari, Edge 및 Chrome에서 지원됩니다. 또한 Node에서도 지원되며 Babel과 같은 빌드 도구를 활용하여 어디서나 사용할 수 있습니다. 실제 예제를 볼 수 있습니다 : http://jsfiddle.net/ben336/rbU4t/2/

여기 문서 :

그러나 Edge 14 이전의 IE9-IE11 및 Edge let는 위의 오류를 지원 하지만 위와 i같은 오류가 발생 var합니다. Edge 14는 마침내 그것을 올바르게 얻습니다.


답변

그것을 말하는 또 다른 방법은 i함수를 만들 때가 아니라 함수를 실행할 때 함수 의 in이 바인딩되어 있다는 것입니다.

클로저를 만들 때 i 와 같이 변수의 복사본이 아닌 외부 범위에 정의 된 변수에 대한 참조입니다. 실행 시점에 평가됩니다.

다른 답변의 대부분은 값을 변경하지 않는 다른 변수를 만들어서 해결하는 방법을 제공합니다.

명확성을 위해 설명을 추가한다고 생각했습니다. 해결책은 개인적으로 Harto와 함께 할 것입니다 .Harto의 답변에서 여기에 대한 가장 자명 한 방법이기 때문입니다. 게시 된 코드는 모두 작동하지만 새 변수 (Freddy 및 1800)를 선언하거나 이상한 내장 폐쇄 구문 (apphacker)을 선언하는 이유를 설명하기 위해 주석 더미를 작성 해야하는 폐쇄 공장을 선택합니다.