직역하면 닫힘/폐쇠/완결성. MDN에는 '둘러쌓인 lexical environment의 참조와 에워싸여 함께 묶여진 함수의 조합' 이라고 설명되어 있음. 이를 외부함수 입장에서 생각하면 '내부함수와 lexical environment의 조합'이라고 할 수 있음.
MDN에서는 클로저가 함수가 생성될 때면 매번 발생한다고 설명함. 즉, 클로저는 특별한 개념이 아니고 함수의 생성과 함께 발생하는 당연한 개념. 하지만 클로저를 보편적인 상황에서 모두 적용하지는 않음. JS에 관련된 문서들에서 using clousre, with closure와 같은 표현을 사용함으로써 클로저 환경에서 발생하는 특별한 현상을 표현하기 위해 사용되는 개념. 따라서 어떤 특별한 현상이 일어나는지 알아야 함.
외부함수와 내부함수에서 조합이라고 할 수 있는 부분은 한가지임. 내부함수의 outerEnvironmentReference와 외부함수의 environmentRecord. 내부함수의 outerEnvironmentReference를 통해서 외부함수의 environmentRecord에 접근할 수 있음. 즉, 그림에서 컨텍스트 A에서 선언한 변수를 내부함수 B에서 참조할 경우에 발생하는 특별한 현상을 말함.
위 코드에서 생성되는 실행컨텍스트는 3개임. 전역, outer, inner.
outer함수가 생성되는 순간 전역컨텍스트와 outer내부에서 선언한 변수 사이에 클로저가 생김. 하지만 전역컨텍스트에서 선언한 변수를 outer에서 참조하는 상황은 아니기 때문에 특별한 현상은 일어나지 않고 당연한 개념에 속하는 클로저임.
음영처리된 부분의 경우 outer함수에서 선언한 변수 a의 값을 inner함수 안에서 변경하고 있음. outer실행컨텍스트에서 선언한 변수를 내부함수 inner에서 참조하고 있음.
전체의 흐름을 보면 아래와 같음.
전역 컨텍스트가 열리고 outer 함수 실행.
outer실행컨텍스트가 쌓임. outer실행컨텍스트의 lexicalEnvironment를 관찰. environmentRecord에는 outer에서 선언된 a:1과 inner:f이 들어감. outerEnvironmentReference에는 전역에서 선언한 outer:f가 들어감. inner가 호출됨.
inner실행컨텍스트가 쌓임. inner실행컨텍스트의 lexicalEnvironment를 관찰. environmentRecord는 선언한 것이 없어 비어있음. outerEnvironmentReference에는 outer컨텍스트의 lexicalEnvironment를 참조해 a와 inner의 값이 들어감.
inner이 종료되면 inner컨텍스트가 사라지고, outer이 종료되면 outer컨텍스트가 사라지고, 전역컨텍스트도 사라지며 종료됨.
코드를 바꿔 a를 출력하는 대신 반환하고, inner을 실행시키는 대신 반환하고, outer()의 결과를 outer2에 저장함. 실행과정을 관찰.
전역컨텍스트는 outer변수와 outer2변수를 수집. outer2에는 outer가 호출되기 전에는 undefined가 저장됨. outer의 실행이 종료된 시점이 되어야 값이 담김.
outer이 호출되면 outer컨텍스트가 쌓이면서 outer의 environmentRecord에는 a:1, inner:f가 들어감. outerEnvironmentReference에는 outer:f가 담김.
outer가 종료되면서 inner를 반환하고, 결과가 outer2에 담김.
outer실행컨텍스트는 종료되었지만 점선처리되어있음. outer2에 의해 inner가 호출될 수 있는데, inner에서 참조하고 있는 outerEnvironmentRecord상의 a변수는 참조카운트가 0이 아닌 상태기 때문. outer실행컨텍스트는 종료됐지만, 내부에서 선언한 변수 a는 살아남은 것.
이대로 outer2를 호출하면 inner함수의 실행컨텍스트가 쌓임.
inner컨텍스트에서 a에 접근하려고하면 environmentRecord에 없음. outerEnvironmentReference(outer)에서 a를 찾아 ++연산을 함. 참조를 하고 있기 때문에 inner컨텍스트의 값도 같이 바뀜. 2를 반환하면서 inner컨텍스트가 제거됨.
콘솔에는 2가 찍힘.
outer2를 다시 호출하면 inner컨텍스트가 쌓이면서 2->3 연산을 함.
inner컨텍스트가 종료되고 3이 출력, 전역컨텍스트 종료.
여기서 a:3은 전역컨텍스트가 종료되기 전까지 살아남음. outer2변수가 inner함수를 참조하고, inner함수가 a를 참조하기 때문. 참조카운트가 0이 되지 않음.
참조카운트를 0으로 만들기 위해서는 outer2에 다른 것을 대입해 참조를 그만하도록 해야함. 이렇게 하면 inner에 대한 참조카운트가 0이 되며 GC대상이 되고, inner내부에서 참조하고 있는 대상 역시 참조카운트가 0이 되며 GC대상이 됨.
"컨텍스트A에서 선언한 변수를 내부함수 B에서 참조할 경우에 발생하는 특별한 현상"은 outer에 있는 변수 a가 사라지지 않는 현상을 말함. 더 자세히 설명하면 "컨텍스트 A에서 선언한 변수 a를 참조하는 내부함수 B를 A의 외부로 전달할 경우, A가 종료된 이후에도 a가 사라지지 않는 현상"이라고 할 수 있음.
지역변수가 함수 종료 후에도 사라지지 않게 할 수 있음. == 함수 종료 후에도 사라지지 않는 지역변수를 만들 수 있음.
user함수는 _logged변수에 true를 할당하고 객체를 반환.
roy.name을 출력하면, roy객체에 name프로퍼티는 없고 name에 대한 getter가 있기 때문에 getter이 호출되고 getter이 _name을 반환. 이 _name은 user를 호출할 때 매개변수로 넘겨받은 값을 가지는 user함수에서 선언된 변수임. 원래는 user함수의 실행컨텍스트가 종료됨과 동시에 함께 사라져야하는 변수였던 것. 그러나 함수 실행을 종료할 때 return해줄 내용 안에서 해당 변수가 사용되고 있는 함수가 있어(참조카운트가 0이 아닌 상태라) 나중에 쓸 변수라고 판단해 살려둠. 함수는 죽었어도 변수는 죽지않음.
roy.name 프로퍼티의 값을 '제이'로 바꾸려고 하면 set name이 발동돼서 _name의 값이 '제이'로 바뀜.
roy._name에 '로이'를 할당하면 _name프로퍼티가 없기 떄문에 새로 만들어서 할당함. 이 명령에 의해서는 user함수에서 선언한 _name변수에는 어떠한 영향도 주지 못함.
getter인 status를 호출하면 user에서 선언된 _logged 변수의 값에 따라 'login' 또는 'logout'을 반환. 현재는 true이기 때문에 'login'이 출력됨.(클로저에 의해 _logged가 살아있음)
logout메서드를 호출하면 좀비변수 _logged가 false가 됨. 다시 roy의 status getter를 호출하면 _logged가 false라 'logout'이 출력.
한편 roy에는 status라는 프로퍼티의 getter는 있지만 setter는 없음. 이때 roy.status=true;
명령을 하면 무시됨. roy.status getter를 호출하면 여전히 'logout'임.(캡슐화)
_name, _logged라는 두 변수는 함수종료 후에도 사라지지 않음.(클로저의 핵심)
이 변수들을 외부로부터 보호할 수 있는 방법도 있음.(외부에 노출된 status프로퍼티는 getter로서만 역할을 하며, 실제 _logged의 값과는 별개의 문자열 만을 반환. 직접 _logged에 영향을 주는 것은 login/logout메서드에 의해서만 가능. 이를 객체지향언어의 중요한 개념인 캡슐화라고 함.)
중요한 것은 함수종료 후에도 사라지지 않는 지역변수를 만들 수 있다는 것이고, 메모리누수 관리등 다양한 프로그래밍 기법에 사용됨.