https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8

 

I never understood JavaScript closures

Until someone explained it to me like this …

medium.com

 

JS에서 코드를 실행한다면 코드가 실행된 범위는 거의 둘 중 하나이다. Global or Function 거의 여기에서 실행된다. 거의

우리가 프로그램을 실행시킨다고 가정하자. 우선 우리는 global 실행 문맥( context, 이거 뭐라해야됨.. ) 안에서 실행하게 될거다. 뭐 몇 개의 변수들이 global안에 선언 되어있을거고, 우린 이걸 호출할거다. 변수 호출을 그렇다 치지만 함수를 실행하게 되면 어떻게 될까?

1. JS는 새로운 실행문맥( 함수 객체 )을 만들거다. 이건 local 실행 문맥이된다.

2. 이 local 실행 문맥은 자신의 몇 개의 변수들을 갖고 있을거고 이 변수들 또한 실행 문맥에 따라 local 변수가 될거다.

3. 만들어진 실행 문맥은 실행 스텍( execution stack )에 들어가게 된다. 

 

스텍이 끝나서 함수가 끝나면 ?

1. 스텍에서 local 실행문맥이 빠져나온다.

2. 함수는 자신의 리턴 값을 자신을 실행한 문맥으로 보낸다. 그 문맥이 실행 문맥이고 이게 global이나 local 실행 문맥일 수 있다. 리턴값이 없다면 undefined가 리턴된다.

3. 다음으로 local 실행 문맥이 박살난다. 그 안에 있던 로컬 변수들 까지 다 박살난다. 없어져버린다. 이게 중요하다. 사라진다 완전.

 

간단한 예시를 보자

let a = 3

function addTwo(x) {
	let ret = x + 2
	return ret
}

let b = addTwo(a)

console.log(b)

1. 우린 글로벌 실행 문백에 변수 a를 선언했다. 그리고 3을 할당했다.

2. 그리고 우린 글로벌 실행 문맥에 새로운 변수 addTwo를 선언했다. 그리고 함수를 addTwo에 할당했다. 함수의 괄호 안에 무엇이 들어있든지 일단 변수에 다 저장한다.

3. 그리고 우린 b라는 변수를 글로벌 실행 문맥에 선언했다. b라는 변수가 선언 되자마자  변수는 일단 undefined 값을 갖게 된다. 

4. 변수 b의 = 연산자 다음에 있는 문법이 함수를 호출한다. 함수가 리턴값으로 뭘 뱉어내던지 그걸 변수 b에 할당한다.

5. 근데 함수가 호출되기 전에 JS는 글로벌 실행 문맥의 메모리에 먼저 가서 addTwo라는 변수가 있는지 확인한다. 있네? 심지어 함수네? 오키 실행.

6. 다음으로 파라미터로 받은 a를 글로벌 실행 문맥의 메모리에서 찾는다. 찾았다. 까보니까 3이 할당 되어 있다. 그럼 3을 들고 addTwo 함수에 넣는다. 이제 실행할 준비가 되었다.

7. 지금 이 순간 실행 문맥이 전환된다. 새로운 local 실행 문맥이 만들어졌다. 이 local 실행 문맥의 이름은 addTwo가 된다. addTwo 실행 문맥은 콜 스텍에 넣어진다. 

8. addTwo 실행문맥은 이제 뭘 하게 될까? 제일 먼저 function addTwo( x )의 x를 로컬 실행 문맥에 변수로 선언한다. 그리고 파라미터로 받은 3을 변수 x에 넣는다. 

9. 다음으로 ret 변수를 선언한다. 그리고 x를 찾기위해 로컬 실행 문백을 뒤진다. 3을 찾으면  x자리에 갖다 놓는다. 그리고 2를 더한다. 5가된다. 이 5를 변수 ret에 할당한다.

10. 그 다음으로 변수 ret의 값을 리턴한다. 리턴 하려면 ret을 찾아야 한다. 로컬 실행 문맥에서 찾는다. 찾았다. 이게 5를 갖고 있다. 찾은 5를 리턴한다. 그리고 함수가 종료된다.

11. 함수가 종료되고 로컬 실행 문맥 역시 박살난다. 사라졌다. 변수 x와 ret도 없어졌다. 콜 스텍에서 빠져나오고 리턴 값이 addTwo 실행 문맥을 호출한 실행 문맥으로 던져진다. 

12. 그래도 박살난 addTwo 실행 문맥이 남긴 유산은 있다. 바로 5. 이걸 변수 b에 넣는다. 

13. 콘솔 로그가 b를 콘솔에 찍어낸다.

 

여기까지가 저 간단한 코드를 JS가 실행하는 방법이다. 내가 참조한 블로그에서는 클로저를 알기 위해서 더 많은 예제를 봐야 한다고 했다.

오키 묻고 더블로 간다.

 

Lexical scope .. 이걸 도대체 한국말로 뭐라 해야 하는지 모르겠다. 그냥 렉시칼 스코프라고 한다.

아래 예제이다.

1: let val1 = 2

2: function multiplyThis(n) {
3:   let ret = n * val1
4:   return ret
5: }

6: let multiplied = multiplyThis(6)

7: console.log('example of scope:', multiplied)

JS의 복잡함은 이루 말할수 없다. this 때문인거 같다. 

로컬과 글로벌 실행 문맥에 변수가 있다. 그냥 이렇게 가정을 해보자. 만약에 로컬 실행 문맥 안에서 변수를 못 찾는다면( 없으니까 못찾는거다. 숨어있어서 못찾는거 그런거 아니다 ) JS는 이 실행 문맥을 호출한 실행 문맥으로 거슬러 올라간다. 찾을 때 까지 계속 올라간다. 만약 계속 못 찾았다 ? 그럼 undefined를 뱉어낸다.

이걸 알고 위의 예제를 보자

1. val1이라는 변수를 글로벌 실행 문맥에 선언한다. 그리고 숫자 2를 할당한다.

2. multiplyThis라는 새로운 변수를 선언한다. 그리고 정의한 함수를 변수에 할당한다.

3. multiplied라는 새로운 변수를 글로벌 실행 문맥에 선언한다.  

4. 그리고 글로벌 실행 문맥의 메모리에서 multitplyThis라는 변수를 꺼낸다. 꺼낸게 함수다. 파라미터로 6을 던져주면서 실행한다.

5. 새로운 실행 문맥이 만들어진다. 이 문맥의 이름은 multitplyThis이다.

6. 로컬 실행 문맥을 실행하면 처음으로 변수 n을 선언한다. 그리고 파라미터 값 숫자 6을 할당한다.

7. 다음으로 ret이 이름인 변수를 선언한다. 

8.9.  로컬 실행문맥의 메모리에서 변수 n과 val1을 찾는다. n은 6이라고  찾았는데 val1을 못 찾았다. 어찌해야되나? 문맥을 타고 올라간다. 올라가니 val1이 있다. 찾았다. 찾은 변수의 값을 곱한다. 12가 됬다. 12를 ret에 넣는다.

10. ret을 리턴한다. 로컬 실행 문맥이 박살난다. val1은 안 박살난다. 로컬 실행 문맥이 아니기 때문이다.

11. 리턴된 숫자 12가  mltipied 변수에 할당된다.

12. 12를 콘솔로 찍어낸다.

 

위의 과정중에서 변수를 찾기 위해 문맥을 타고 올라갔었다. 이 현상의 공식적인 이름이 바로 Lexical scope이다.

 

 

함수는 뭐든지 리턴 할 수 있다. 배열이든 함수든 변수들 뭐시기든 리턴할 수 있다.

아래 예제를 보자

 1: let val = 7
 
 2: function createAdder() {
 3:   function addNumbers(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return addNumbers
 8: }
 
 9: let adder = createAdder()
 
10: let sum = adder(val, 8)

11: console.log('example of function returning a function: ', sum)

1. 글로벌 실행 문맥에 val 변수를 선언하고 7을 할당한다.

2. 글로벌 실행 문맥에 변수 createAdder이름의 변수를 선언한다. 그리고 함수를 변수에 할당한다. 근데 여기서 { } 안에 있는 걸 건드지 않는다. 

3. 글로벌 실행 문맥에 adder 변수를 선언한다. 이 순간 일시적으로 undefined가 adder 변수에 할당된다.

4. 그리고 createAdder()를 호출 하기 위해 글로벌 실행 문맥 안의 메모리를 뒤진다. 찾았으면 꺼내온다.

5. 함수를 실행한다. 이 때 새로운 로컬 실행 문맥이 만들어진다. js엔진은 이 로컬 문맥을 콜 스텍에 집어 넣는다. 함수는 받은 파라미터가 없다. 그럼 바로 함수 내부로 들어간다.

6. 우린 또 addNumbers 변수를 로컬 실행 문맥에 생성하고 저장한다. addNumbers 변수는 바로 이 createAdder 실행 문맥에만 존재한다.

7. createAdder 함수는 addNumbers 변수를 리턴한다.그리고 로컬 실행 문맥이 없어진다.

8. 원래 addNumbers 변수는 없어진다. 그런데 정의된 함수는 여전히 존재한다. 이게 adder 변수에 할당된다. 

9. 새로운 변수 sum을 글로벌 실행 문맥에 선언한다. 그리고 일시적으로 undefined가 할당된다.

10. sum 변수에 adder 변수에 있는 함수를 실행해야 한다. 먼저 글로벌 실행 문맥의 메모리를 찾아본다. 정의된 함수가 있다. 가져온다.

11. 함수를 실행하기 전 2개의 파라미터를 보낸다. 7 과 8이다.

12. 이제 진짜 함수를 실행한다. 새로운 로컬 실행 문맥이 생성된다. 이 로컬 실행 문맥에 a, b 2개의 변수가 만들어진다. 각각 7과 8이 할당된다.

13. ret 이름의 새 변수가 선언된다. 

14. a와 b를 더한다. 결과는 15이고 이걸 ret변수에 할당한다.

15. 함수로부터 ret 변수가 리턴된다. 그리고 이 로컬 실행 문맥이 파괴된다. 그리고 콜 스텍에서 빠진다. 변수 a, b, ret은 이제 없어진다.

16. 리턴된 값은 sum 변수에 할당된다.

17. sum을 콘솔에 찍는다.

 

위의 과정에서 우리가 알 수 있는건 정의된 함수는 변수에 저장된다라는 점, 정의된 함수는 호출되기 전 까지는 프로그램에 보이지 않는다는 점이다.  또한 함수가 실행되는 매 순간 새로운 로컬 실행 문맥이 생성된다는 것이다. 이 로컬 실행 문맥은 함수가 종료되면 사라진다. 

 

 

이제 드디어 클로저다.

 1: function createCounter() {
 2:   let counter = 0
 3:   const myFunction = function() {
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 
 9: const increment = createCounter()
 
10: const c1 = increment()

11: const c2 = increment()

12: const c3 = increment()

13: console.log('example increment', c1, c2, c3)

1. 글로벌 실행 문맥에 createCounter 변수를 만든다. 그리고 함수 정의를 할당한다.

2. 새로운 변수 increment를 선언한다.

3. 글로벌 실행 문맥 메모리에서 createCounter 변수를 찾는다. 변수에 정의 된 함수가 있다.

4. createCounter 로컬 실행 문맥이 생성된다. 

5. 로컬 실행 문맥에 counter 변수를 선언하고 0을 할당한다.

6. 새로운 변수 myFunction을 선언한다. 변수의 내용은 또 다른 함수 정의이다. 지금 우리는 클로저를 만들었다. 클로저는 현재 실행문맥( 지금은 createCounter 실행 문맥 )의 변수들을 갖고있다. 지금의 경우 counter 변수를 갖고 있다.

7. myFunction 변수를 리턴하면서 createCounter 실행 문맥이 삭제된다. myFunction과 counter 변수는 존재하지 않는다. 

8. increment 변수에 createCounter의 리턴 값이 할당된다. increment는 함수 정의( myFunction이 갖고 있던 함수 정의, 이게 클로저가 됨 )를 갖고 있다. 

9. 새로운 변수 c1을 선언한다. 

10. 글로벌 실행 문맥에서 increment 변수를 찾는다. 찾았다. 이거 함수다. 

11. 새로운 실행 문맥을 만든다. 함수에 파라미터가 없다. 함수를 실행한다.

12. counter = counter + 1 //// 이라는게 있다. counter를 찾는다. 근데 글로벌이나 로컬 실행 문맥을 찾기 전에 먼저 클로저( myFunction였던 함수 정의를 포함한 그 스코프의 모든 내용) 내부를 체크한다. 잉? 클로저가 변수 counter를 갖고 있네 ? 값이 0 이네? 그럼 1을 더한다.  이제 클로저의 counter 변수는 1이된다. 

13. 다음으로 counter 값 즉 1을 리턴한다.

14. 리턴된 값 1 이 c1에 할당된다.

15. 나머지 c2, c3 반복 그리고 로그 찍힘

 

클로저 내부에 저장된 정보는 로컬 실행 문맥처럼 없어지지 않기 때문에 ( 뭔가는 그 정보를 계속 갖고 있으니까 ) counter에 1을 계속 추가하는게 가능하다. 

즉 클로저는 함수 생성당시 같은 스코프에 존재하는 모든 변수의 수집( collection )이다. 함수가 함수를 리턴하면서 실행 문맥이 생성되고 파괴되는  그 점을 이용하여 클로저를 사용한다. 

 

+ Recent posts