코딩쌀롱

for문 안의 let 변수에 외부에서 접근하기(Block scope) 본문

개발공부

for문 안의 let 변수에 외부에서 접근하기(Block scope)

이브✱ 2020. 12. 24. 23:51

이해 못한 문제 

let funcArr = [];

for(let i = 0; i < 5; i++) {
   var c = i * 2;
   funcArr.push( (_)=>console.log(c) );
}

funcArr.forEach( fn => fn() )

출력 결과를 예상해보자.

 

반복문이 모두 돌면 var가 함수 레벨 스코프이기 때문에 c = 8이 된다. 그리고 funcArr에는 함수 다섯 개가 들어가 있다. forEach로 funcArr에 있는 함수를 모두 실행시키면 console.log(c)를 다섯 번 실행한다. c의 값은 var로 전역변수로 선언돼있고, 마지막 값인 8이다. 따라서 8 8 8 8 8이 출력된다.

 

위의 예시에서 var를 let으로만 바꿨다. 출력 결과를 예상해보자.

let funcArr = [];

for(let i = 0; i < 5; i++) {
   let c = i * 2;
   funcArr.push( (_)=>console.log(c) );
}

funcArr.forEach( fn => fn() )

let으로 선언한 c의 값은 저 블록 안에서만 유효하니까  funcArr에 들어있는 익명함수들이 c를 출력하려고 할 때 c값을 찾지 못하고 undefined가 5개 나올 것이라고 생각했다. 그런데 0 2 4 6 8이 나온다...충격🤷🏻‍♀️🤷🏻‍♀️🤷🏻‍♀️🤯

 

밖에서 console.log(c)를 하면 undefined가 출력된다. 그러니까 funcArr의 함수들을 실행하는 것이 마치 클로저처럼 원래는 접근이 불가능한 블록 스코프 안의 let 변수에 접근할 수 있다는 것인데,, 클로저는 아니기 때문에 실제로 funcArr 함수들의 스코프에 Closure가 아닌 Block이라고 나온다.

 

이 원인을 찾고, 이해하기 위해 몇 시간 동안 코코아 동료들과 이야기를 했다. 정답일지는 모르겠지만 일단 내린 결론을 작성해보겠다.

 

코드 해석

let funcArr = [];

for(let i = 0; i < 5; i++) {
   let c = i * 2;
   funcArr.push( (_)=>console.log(c) );
}

funcArr.forEach( fn => fn() ) // 0 2 4 6 8

블록 밖에 있는 함수가 어떻게 블록 안에 있는 c값을 찾을 수 있는지, 그리고 c의 값이 다 다르게 출력 되었다는 것은 for문을 돌면서 c의 값이 달라지는데 함수가 어떻게 접근하는 것인지 해석해보았다.

 

1. for문의 렉시컬 환경과 블록 스코프

먼저, for문을 풀어서 작성해보자.

 블록 없이 반복문을 풀어 작성하면 에러가 뜬다.(let 재선언 불가)

 

그리고 for문의 블럭도 반복된다고 가정하고 해봤더니 에러없이 작동한다. 즉, for문을 반복할 때 블럭도 반복된다.

 

(ES6부터) let은 블록 레벨 스코프이고, 블록에 의해 새로운 렉시컬 환경(LexicalEnvironment)이 생성된다. for문의 코드 블록이 실행되기 시작하면 새로운 렉시컬 환경을 생성하고 for문 코드 블록 내의 식별자와 값을 등록한다. 그리고 새롭게 생성된 렉시컬 환경을 현재 실행 중인 실행 컨텍스트의 렉시컬 환경으로 교체한다. for문의 코드 블록의 반복 실행이 모두 종료되면 for문이 실행되기 이전의 렉시컬 환경을 실행 중인 실행 컨텍스트의 렉시컬 환경으로 되돌린다.

 

for문이 반복될 때마다 새로운 Lexical Environment가 생성되고, 그 안에 각각 다른 c값들이 저장되어 있는 것!

 

2. 함수 실행 과정

이제 함수 실행 과정을 살펴보자.

디버깅을 해보면 forEach를 실행할 때마다 push의 콜백함수로 돌아가 실행한다.

똑같은 console.log(c)를 실행한다고 생각할 수 있지만, for문을 풀어써서 생각해보면 그렇지 않다. 

1) { let c = 0; funcArr.push((_) => console.log(c)); }

2) { let c = 2; funcArr.push((_) => console.log(c)); }

3) { let c = 4; funcArr.push((_) => console.log(c)); }

4) { let c = 6; funcArr.push((_) => console.log(c)); }

5) { let c = 8; funcArr.push((_) => console.log(c)); }

forEach로 funcArr를 실행할 때 1번부터 5번까지 push의 콜백함수가 실행된다. 즉, 다른 렉시컬 환경을 가진 다른 함수들을 실행하는 것이다. 그래서 반복문을 돌 때마다 바뀌는 c의 값을 각각 찾아서 출력할 수 있는 것이다.

 

콘솔에 funcArr[0]과 funcArr[1]을 비교해보았다.

 

funcArr 안의 함수는 모두 다른 스코프를 가진 다른 함수라는 것을 확인할 수 있다.

 

아직 확실히 모르겠는 것

funcArr에 push된 함수들을 forEach로 실행할 때, 블록 안으로 가서 실행이 되는데 이는 함수가 참조 데이터이기 때문이 아닐까라고....추측해본다..🤔

 

let으로 했을 때 funcArr의 함수들은 Scope가 Block으로 나온다. 그래서 for문의 블록이 함수들의 스코프를 블록으로 하는 것일까 하고 var로 해봤다. 그런데 var로 했을 때 funcArr의 함수들은 Scope가 Script라고 나온다. 함수의 스코프를 결정하는 것이 함수가 스코프체인을 어디까지 찾아가는지에 따라 달라지는 것 같다.

 

정리

1. 반복문을 돌면서 블록이 반복된다. 즉 블록마다 렉시컬 환경(L.E)이 만들어지고, 블록 레벨 스코프의 변수들 정보를 수집한다.(let, const) 

2. funcArr에 push한 익명 함수들은 다른 스코프를 가진 다른 함수다.

3. 함수들을 forEach로 실행하면, 해당 함수의 블록에서 함수가 실행되고, 실행 컨텍스트가 생성된다.

4. 필요한 변수값들을 L.E로 스코프체인을 통해 찾는다.

5. 함수가 블록 안에서 실행되기 때문에 let 변수 값에 접근할 수 있고, 블록마다의 다른 변수값을 출력할 수 있는 것.

 

 

 


참고했던 사이트

stack overflow 질문 - 블럭이 실행될 때 실행컨텍스트가 생성이 되는가? 

Comments