장점 - 루프 내부의 JavaScript 클로저-간단한 실용적인 예




자바스크립트 클로저 장점 (20)

우리는 당신이 선언 할 때 실제로 무슨 일이 확인합니다 varlet하나 하나.

사례 1 : 사용var

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

이제 F12 키 를 눌러 크롬 콘솔 창 을 열고 페이지를 새로 고침하십시오. 배열 안의 모든 3 개의 함수를 소비하십시오. 당신은 .Expand 라는 속성을 보게 될 것입니다. 하나의 배열 객체가 호출 된 것을 볼 수 있습니다. 값이 3 인 객체로 선언 된 속성을 찾을 수 있습니다.[[Scopes]]"Global"'i'

결론:

  1. 'var'함수 밖에서 변수를 선언하면 변수 가 전역 변수가됩니다 (변수를 입력 i하거나 window.i콘솔 창에서 확인할 수 있습니다 . 3을 반환합니다).
  2. 선언 한 annominous 함수는 함수를 호출하지 않는 한 함수 내부의 값을 호출하거나 검사하지 않습니다.
  3. 함수를 호출하면 객체 console.log("My value: " + i)에서 값을 가져와 Global결과를 표시합니다.

사례 2 : let 사용

이제 교체 'var'와 함께'let'

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

똑같은 일을하십시오. 범위로 가십시오. 이제 두 개체 볼 수 "Block""Global". 이제 Block객체를 확장 하면 'i'가 정의되어있는 것을 볼 수 있습니다. 이상한 점은 모든 함수에 대해 값 i이 다르다는 것입니다 (0, 1, 2).

결론:

'let'함수 밖에서도 루프 내에서 변수를 선언하면 이 변수는 전역 변수가 아니며 Block동일한 함수에서만 사용할 수 있는 수준 변수가됩니다. 그 이유는 우리가 i다른 값을 얻고 있기 때문입니다 우리가 함수를 호출 할 때 각 함수에 대해.

더 가깝게 작동하는 방법에 대한 자세한 내용은 멋진 비디오 자습서 https://youtu.be/71AtaJpJHw0

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

다음을 출력합니다.

내 값 : 3
내 값 : 3
내 값 : 3

반면 출력하고 싶습니다 :

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

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

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<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++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

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


이 짧은 것을 시도해보십시오.

  • 배열 없음

  • 여분의 for 루프 없음


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

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

http://jsfiddle.net/7P6EN/


OP가 표시하는 코드의 주요 문제점은 두 번째 루프까지 읽히지 않는다는 것입니다. 시연하려면 코드 내부에 오류가있는 것을 상상해보십시오.

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

funcs[someIndex] 가 실행될 때까지 실제로 오류가 발생하지 않습니다 () . 이 같은 논리를 사용하면 i 값도이 시점까지 수집되지 않는다는 것이 분명해야합니다. 원래 루프가 끝나면, i++3 의 값으로 i 를 가져오고, 결과는 조건 i < 3 실패하고 루프가 끝납니다. 이 시점에서 i3 이므로 funcs[someIndex]() 가 사용될 때마다 i 가 평가됩니다. 매번 3 번입니다.

이 문제를 극복하려면, i 그것이 발생한 것처럼 평가해야합니다. 이것은 이미 funcs[i] (3 개의 고유 인덱스가있는) 형태로 발생했습니다. 이 값을 캡처하는 방법은 여러 가지가 있습니다. 하나는 이미 여러 가지 방법으로 표시된 함수에 매개 변수로 전달하는 것입니다.

또 다른 옵션은 변수를 닫을 수있는 함수 객체를 생성하는 것입니다. 그렇게 성취 될 수있다.

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};

가장 간단한 해결책은,

사용하는 대신:

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

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

"2"를 3 번 ​​경고합니다. 이것은 for 루프에서 생성 된 익명 함수가 동일한 closure를 공유하고 그 closure에서 i 의 값이 동일하기 때문입니다. 공유 종료를 방지하려면 다음을 사용하십시오.

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

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

이 배후의 아이디어는 for 루프의 전체 본문을 IIFE (Immediately-Invoked Function Expression)로 new_i 하고 new_i 를 매개 변수로 전달하고이를 i 로 캡처하는 것입니다. 익명의 함수가 즉시 실행되기 때문에, i 값은 익명의 함수 내부에서 정의 된 각 함수마다 다릅니다.

이 솔루션은 이러한 문제에 부합하는 것으로 보입니다.이 문제를 겪고있는 원래 코드를 최소한으로 변경해야하기 때문입니다. 실제로 이것은 의도적으로 설계된 것이므로 전혀 문제가되지 않습니다.


다음은 Bjorn (apphacker)과 비슷한 기술의 또 다른 변형입니다. 변수 값을 매개 변수로 전달하지 않고 함수 내부에 할당 할 수 있습니다.이 매개 변수는 때로는 더 명확 할 수 있습니다.

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

어떤 기술을 사용 하든지 index 변수는 내부 변수의 반환 된 복사본에 바인딩 된 일종의 정적 변수가됩니다. 즉, 그 값의 변화는 호출간에 보존됩니다. 그것은 매우 편리 할 수 ​​있습니다.


당신이 이해할 필요가있는 변수의 범위는 자바 스크립트에서 함수를 기반으로합니다. 이것은 블록 범위를 가지고있는 C #와는 중요한 차이점이며 for 변수 안에 변수를 복사하는 것만으로도 작동합니다.

apphacker의 대답처럼 함수를 반환하는 것을 평가하는 함수에서 값을 래핑하면 변수가 이제 함수 범위를 갖기 때문에 트릭을 수행합니다.

또한 var 대신 let 키워드가있어 블록 범위 규칙을 사용할 수 있습니다. 이 경우에 for 내부에 변수를 정의하면 트릭을 수행하게됩니다. 즉 let 키워드는 호환성 때문에 실질적인 해결책이 아닙니다.

var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value: " + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}

아직 언급되지 않은 또 다른 방법은 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]();
}


여러 가지 솔루션을 읽은 후에이 솔루션이 작동하는 이유는 범위 체인 의 개념에 의존한다는 것입니다. 실행 중 변수가 JavaScript에 의해 해결되는 방식입니다.

  • 각 함수 정의는 var 및 해당 arguments 선언 된 모든 로컬 변수로 구성된 범위를 형성합니다.
  • 내부 함수가 다른 (외부) 함수 내부에 정의되어 있으면이 함수는 체인을 형성하고 실행 중에 사용됩니다
  • 함수가 실행되면 런타임은 범위 체인 을 검색하여 변수를 평가합니다. 체인의 특정 지점에서 변수를 찾을 수 있으면 검색을 중지하고 사용합니다. 그렇지 않으면 변수는 전역 범위가 도달 할 때까지 계속됩니다.

초기 코드에서 :

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

funcs실행됩니다, 범위 체인이 될 것입니다 function inner -> global. 변수 i는 찾을 수 없으므로 function inner( var인수를 사용하여 선언 하거나 인수로 전달하지 않았 음) i결국 전역의 범위 인 값이 발견 될 때까지 검색을 계속합니다 window.i.

바깥 함수에서 그것을 감싸는 것으로 share 처럼 도우미 함수를 명시 적으로 정의 하거나 share 과 같은 익명의 함수를 사용합니다 :

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

funcs실행됩니다, 지금은 범위 체인이 될 것입니다 function inner -> function outer. 이 시간 i은 for 루프에서 3 번 실행되는 바깥 함수의 범위에서 찾을 수 있으며, 매번 값이 i올바르게 바인딩됩니다. window.i내부 실행시 값을 사용하지 않습니다 .

더 자세한 내용은 here 에서 찾을 수 있습니다. here
에는 루프에서 클로저를 만드는 일반적인 실수뿐만 아니라 클로저와 성능 고려가 필요한 이유가 포함됩니다.


이것은 자바 스크립트에서 클로저를 사용하는 일반적인 실수를 설명합니다.

함수는 새로운 환경을 정의합니다.

중히 여기다:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

makeCounter 가 호출 될 때마다 {counter: 0} 이 생성되어 새로운 객체가 생성됩니다. 또한 새 객체를 참조하기 위해 obj 의 새 복사본이 만들어집니다. 따라서 counter1counter2 는 서로 독립적입니다.

루프의 클로저

루프에서 클로저를 사용하는 것은 까다 롭습니다.

중히 여기다:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

counters[0]counters[1] 은 독립적 이지 않습니다 . 사실, 그들은 같은 obj 에서 작동합니다!

아마도 성능상의 이유로 루프의 모든 반복에 걸쳐 공유되는 obj 복사본이 하나뿐이기 때문입니다. 각 반복마다 {counter: 0} 이 (가) 새로운 객체를 생성하더라도 obj 의 동일한 사본은 가장 최근의 객체에 대한 참조로 업데이트됩니다.

해결 방법은 다른 도우미 함수를 사용하는 것입니다.

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

이것은 함수 범위의 지역 변수와 함수 인자 변수가 입력시 새로운 사본으로 할당되기 때문에 작동합니다.

자세한 내용은 JavaScript closure 함정 및 사용법을 참조하십시오.


이제 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/

문서 도구 :

하지만 IE9-IE11과 Edge Edge 14를 지원하기 전에는 위와 같은 오류가 발생합니다 (매번 새로운 i 생성하지 않으므로 var 를 사용하는 경우처럼 위의 모든 함수가 3을 기록합니다). Edge 14가 마침내 제대로되었습니다.


Immediately-Invoked Function Expression을 사용하여, 인덱스 변수를 둘러싸는 가장 간단하고 읽기 쉬운 방법 :

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);

}

이것은 iterator iindex 정의한 익명 함수로 보냅니다. 이것은 클로저를 생성하는데, 여기에서 변수 i 는 나중에 IIFE 내의 비동기 기능에서 사용할 수 있도록 저장됩니다.


forEach (IE9로 돌아 가기)를 사용하는 간단한 해결책은 다음과 같습니다.

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

인쇄물:

My value: 0
My value: 1
My value: 2

ES6의 새로운 기능으로 블록 레벨 범위가 관리됩니다.

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

OP 질문에 코드 let대신 대신 사용됩니다 var.


우선이 코드의 잘못된 점을 이해하십시오.

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

여기서 funcs[]배열이 초기화 될 때 i증가되고 funcs배열은 초기화되고 func배열 의 크기 는 3이됩니다 i = 3,. 이제 funcs[j]()호출 될 때, i이미 3으로 증가 된 변수를 다시 사용합니다 .

이제이 문제를 해결하기 위해 많은 옵션이 있습니다. 아래는 두 가지입니다.

  1. 우리는 초기화 할 수 있습니다 i와 함께 let또는 새로운 변수 초기화 index와를 let하고하는 것이 동등하게 i. 호출이 이루어지면 호출이 index사용되며 초기화 후 해당 범위가 끝납니다. 전화를 index걸면 다시 초기화됩니다.

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
    
  2. 다른 옵션은 tempFunc실제 함수를 반환하는를 소개 할 ​​수 있습니다 :

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

closure 구조를 사용하면 추가 for 루프가 줄어 듭니다. 당신은 하나의 for 루프에서 그것을 할 수있다 :

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

query-js (*) 와 같은 데이터 목록에 선언 모듈을 사용할 수 있습니다 . 이러한 상황에서 나는 개인적으로 선언적 접근이 덜 놀랍다 고 생각한다.

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

그런 다음 두 번째 루프를 사용하여 예상 결과를 얻을 수 있습니다.

funcs.iterate(function(f){ f(); });

(*) 나는 query-js의 저자이고, 그것을 사용하는쪽으로 편향되어 있으므로 선언적 접근을 위해서만이 라이브러리에 대한 추천으로 제 단어를 사용하지 마십시오.


나는 forEachpseudo range를 생성하는 함수 를 사용하는 것을 선호한다 .

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

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

그것은 다른 언어의 범위보다 못 생겼지 만, IMHO는 다른 솔루션보다 덜 위험합니다.


나는 아무도 아직 forEach지역 변수를 더 잘 (다시) 피하기 위해 함수를 사용하도록 제안했다는 사실에 놀랐다 . 사실, 나는 for(var i ...)이런 이유로 더 이상 전혀 사용하지 않을 것입니다.

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

// forEach지도 대신 사용하도록 수정되었습니다 .


원래 예제가 작동하지 않는 이유는 루프에서 만든 모든 클로저가 동일한 프레임을 참조했기 때문입니다. 실제로, 하나의 i변수 만으로 하나의 객체에 3 개의 메소드가 있습니다. 그들은 모두 동일한 가치를 인쇄했습니다.


이 질문은 실제로 JavaScript의 역사를 보여줍니다! 이제 화살표 함수를 사용하여 블록 범위 지정을 피하고 Object 메서드를 사용하여 DOM 노드에서 직접 루프를 처리 할 수 ​​있습니다.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>





closures