-
사전적 정의는 function(함수)과 Lexical Environment(어휘적 환경)의 조합으로, 함수가 생성될 당시의 외부 변수를 기억하고, 생성 이후에도 계속 접근 가능한 코드를 의미한다.
...글로 적으니 무슨 소리인지 모르겠다. 따라서 예시와 함께 알아볼건데, 그 전에 Lexical Env.에 대해 알아보자.
Lexical Environment
##################################################### [1] const name = "Woong"; ##################################################### [2] function isAdult(age) { if (age > 19) { console.log(`${name} is adult (${age})`); } else { console.log("Not adult"); } } ##################################################### [3] isAdult(21); ##################################### [4]
[1] 위치에서 다음과 같은 전역 렉시컬 환경이 만들어진다.
name: not initialized isAdult: function
[2] 위치에서 다음과 같은 전역 렉시컬 환경으로 갱신된다.
name: "Woong" isAdult: function
[3] 위치에선 전에 [1] 위치에서 함수에 대한 초기화가 이루어 지기에 별 다른 변화는 없다.
[4] 위치에선 다음과 같은 내부 렉시컬 환경이 만들어진다.
[전역 렉시컬 환경] name: "Woong" isAdult: function
[내부 렉시컬 환경] age: 21 (전역 렉시컬 환경 참조)
그 다음은 isAdult로 할당된 함수 내부가 실행되는데, age 변수는 내부 렉시컬 환경에 있기에 사용하는데 지장이 없다. 하지만, name 변수의 경우에는 문제의 여지가 있다. 실제로, VSC에선
가로줄이 그어진다. 하지만 전혀 문제가 없이 실행이 가능하다. 왜 일까?그 이유는 바로 내부 렉시컬 환경은 전역 렉시컬 환경을 참조할 수 있기 때문이다.
내부 렉시컬 환경에서 변수를 찾고, 없으면 전역 렉시컬 환경으로 확장해 변수를 찾는다. 이 때문에 함수 내부에 name이 선언되지 않아도 사용할 수 있고, 동일명의 지역변수와 전역변수가 있을 때, 함수 내부에서 실행되는 코드는 우선적으로 지역변수의 값을 가져다 쓰는 것이다.
중첩 함수의 경우는 어떨까?
function gretting(say) { return function (name) { return console.log(say, name); }; } const sayHello = gretting("Hello"); sayHello("Woong");
이 경우 실행이 마지막 줄까지 도달했을 때의 렉시컬 환경은 다음과 같이 만들어진다.
[전역 렉시컬 환경] gritting: function sayHello: function
[gretting 렉시컬 환경] say: "Hello" (전역 렉시컬 환경 참조)
[익명함수 렉시컬 환경] name: "Woong" (gretting 렉시컬 환경 참조)
익명함수를 전달받은 sayHello는 익명함수의 렉시컬 환경을 전달받고, 따라서 익명함수가 접근할 수 있었던 변수에도 모두 접근이 가능하다.
심지어 접근 가능했던 상위 함수 gretting이 소멸하고 동시에 익명함수도 소멸된다 해도, sayHello에는 소멸 전 익명함수의 렉시컬 환경이 저장되어 있으므로 계속해서 상위 변수에 접근이 가능하다.
Closure
중요한 점은 gretting, 익명함수가 소멸함에 따라 그 안에 지역변수 say, name에는 더이상 접근할 방법이 없지만, sayHello만은 접근이 가능하다는 점이다.
이 sayHello함수를 Closure라 부르고, say, name변수를 은닉화에 성공했다고 부른다.
Closure의 응용을 살펴보자.
const counter = (function (num) { console.log(`init num === ${num}`); let constVar = 0; return { inc: function () { return ++num; }, dec: function () { return --num; }, catNum: function () { return num; }, catConstNum: function () { return constVar; }, }; })(1); counter.inc(); console.log(counter.catNum()); counter.dec(); console.log(counter.catNum()); console.log(counter.catConstNum());
이번에는 Clusure을 자기호출 익명함수로 만들었다. 초기화 값은 1.
그리고 반환되는 값은 객체로 객체의 value값은 함수를 갖고있다. 각각의 함수는 다른 역할을 수행한다. 마치 메서드처럼 사용할 수 있다.
실제로도 메서드가 맞다. counter가 return 받는 대상은 객체이고, 객체 내에 함수가 존재하니까.
하지만 기존 메서드와의 차이점이라면
기존 객체는 메서드가 참조하는 객체 내부의 변수를 외부에서도 참조할 수 있지만,
Closure는 메서드가 참조하는 변수를 외부에서는 참조할 수 없다. 오직 Closure을 통해서만 참조 가능하다.만약 catNum 함수를 생략한다면?
num의 증감은 가능해도 볼 방법이 전혀 없다.
이렇듯 Closure을 사용하면 개발자의 능력껏 변수의 공개 범위를 정할 수 있고, 변수의 수정 여부와 수정 방법도 제한할 수 있다.
여담이지만 Closure로 렉시컬 환경에서 참조했던 변수의 접근하는 방법은 위 방법처럼 읽기 함수로 접근하는 방법 뿐 만이 아니다.
return { catNumByKey: num, catNumByFunction: function () { return num; }, };
그냥 객체 쓰듯이 키-값으로도 접근 할 수 있다. (당연하지...)
...마지막으로 생성자 함수에서도 사용할 수 있다.
const MyColor = function (color) { let c = color; this.getColor = function () { return c; }; this.changeColor = function (newColor) { c = newColor; }; }; const myColor = new MyColor("red");
여기서 this.changeColor를 지운다면 c를 변경할 방법은 존재하지 않게 된다.
요약
Closure를 이용하면 은닉화된 함수, 객체를 만들 수 있다.
Closure의 원리는 Lexical Environment에 있다.
Closure을 사용하면 변수의 공개, 수정 여부와 수정 방법도 제한할 수 있다.'언어 공부 > JS' 카테고리의 다른 글
prototype (0) 2022.04.10 일반 함수로 생성자 함수처럼 구현 (0) 2022.04.07 console.log() 실시간 적용 (0) 2022.04.06