Function declared in a loop contains unsafe references to variable(s)

심심치 않게 만날 수 있는 오류

문제상황

run = (timeout: number) => new Promise<void>((resolve) => {
  let direction = 'r';
  const todos = [];
  for (let i = 0; i < 15; i += 1) {
    if (i === 6) direction = 'l';
    if (i === 10) direction = 'r';
    todos.push(() => { this.dragonBGmove(`${direction + 4}`, timeout); });
    todos.push(() => { this.dragonBGmove(`${direction + 3}`, timeout); });
  }
  resolve();
});

7,8번째 줄에서 다음과 같은 에러가 뜬다.

Function declared in a loop contains unsafe references to variable(s) ‘direction’.

왜 이런 에러메세지가 나왔는지 짐작은간다. todos를 루프 돌리면서 안에있는 함수를 호출할때는 이미 direction이 내가 의도한 값이 아니기 때문이다.

예를들어서,

class TEST {
  run = () => {
    let a = 10;
    const todos = [];
    for (let i = 0; i < 4; i += 1) {
      if (i%2 === 0) a += 1;
      todos.push(function(){ this.move(a); }.bind(this));
      todos.push(() => { this.move(a) })
    }
    todos.forEach((fn) => {
      fn();
    });
  }
  move = (variable) => {
    console.log(variable);
  }
}
var obj = new TEST();
obj.run();
// 12
// 12
// 12
// 12
// 12
// 12
// 12
// 12

실행해보면 12만 계속 나온다. 당연하다! 실행시점에서 a를 찾아 찾아 위로 올라갔을때(scope chain) 이미 a값은 12가 되어있기 때문이다. 그래서 todos에 push하는 시점의 a값을 잡아놔야한다.

조사

Bind를 쓰라!

https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example/19323214#19323214

class TEST {
  run = () => {
    let a = 10;
    const todos = [];
    for (let i = 0; i < 4; i += 1) {
      if (i%2 === 0) a += 1;
      todos.push(function(val){ this.move(val); }.bind(this, a));
      todos.push(((val) => { this.move(val) }).bind(this, a));
    }
    todos.forEach((fn) => {
      fn();
    });
  }
  move = (variable) => {
    console.log(variable);
  }
}
var obj = new TEST();
obj.run();
// 11
// 11
// 11
// 11
// 12
// 12
// 12
// 12

해결책 정리

bind를 사용해서 아래와 같이 수정했다.

run = (timeout: number) => new Promise<void>((resolve) => {
  let direction = 'r';
  const todos = [];
  for (let i = 0; i < 15; i += 1) {
    if (i === 6) direction = 'l';
    if (i === 10) direction = 'r';
    todos.push(this.dragonBGmove.bind(this, direction + 4, timeout));
    todos.push(this.dragonBGmove.bind(this, direction + 3, timeout));
  }
  resolve();
});

Leave a Reply

Your email address will not be published.