Closure
April 10, 2023
ES
Closure
자바스크립트 함수는 함수 외부 변수에 접근하여 수정 변경이 가능한데 이 때, 수정가능한 변수가 담긴 렉시컬 환경(외부)의 범위를 클로저라고 한다. 클로저는 또한 외부 변수를 기억하고 그 변수에 접근할 수 있는 함수를 일컫기도 한다.
Lexical Environment
자바스크립트에서 실행중인 함수, 코드블록, 스크립트 전체는 렉시컬 환경(Lexical Environment)을 갖는다. 렉시컬 환경은 두 부분으로 구성되는데 하나는 코드 블록 내부를 표현하는 환경 레코드(Environment Record)로, 모든 지역변수의 프로퍼티를 갖고있고, 외부 렉시컬 환경(Outer Lexical Environment)는 외부 렉시컬 환경에 대한 참조를 담고있다. 따라서 변수는 환경 레코드의 프로퍼티라고 볼 수 있다. 이 때문에 코드 블럭 내부의 변수를 외부 렉시컬 환경에서 불러올 수 없는 것이다. (지역 변수는 전역 환경 레코드에 프로퍼티가 아니며 외부 렉시컬 환경에도 포함되지 않기 때문에 전역 환경에서 지역변수를 불러 올 수 없다.) 다만, 블럭안에서 외부환경의 변수는 불러오는 것은 가능한데, 그 이유는 해당 렉시컬 레코드에서 변수를 찾지 못하면 외부 렉시컬 환경(Outer Lexical Environment)에서 그 변수를 찾으려 하고, 더이상의 외부 렉시컬 환경이 없을 때까지 반복되기 때문이다.
렉시컬 환경과 함수
그런데 이 특성은 함수의 불러올 수 있다는 특징과 만나 흥미로운 결과를 만든다. 함수는 선언될 자신이 선언된 위치의 렉시컬 환경을 [[Environment]]
객체에 저장하게 된다. 그 뒤 함수가 실행되면 자신의 렉시컬 환경을 갖게 되는데, 특이한 점은 함수가 어디에서 실행 됐던, 자신이 선언된 위치의 렉시컬 환경을 [[Environment]]
에 저장하고 있기 때문에 함수가 실행중인 위치와 전혀 상관없는 다른 위치의 렉시컬 환경에 영향을 받게 되는 것이다.
function makeCounter() let count = 0; return function(){ return ++count; } } let counter = makeCounter();
위 상황에서 return ++count;
의 한줄짜리 함수는 선언 될 때, [[Environment]]
객체에 그 위체 대한 렉시컬 환경을 담게 된다. 이 후, makeCounter
에 의해 만들어진 counter
가 만들어 질 때, 함수의 렉시컬 환경을 갖게 된다. 따라서 원칙적으로 전역 환경에서 만들어진 counter
함수는 함수 내의 count
변수(makeCounter
의 환결 레코드의 프로퍼티)에 접근할 수 없지만 makeCounter
가 반환한 함수로 만들어진 counter
함수는 [[Environment]]
객체에 makeCounter
의 렉시컬 환경을 담고 있으므로 makeCounter
의 환경 레코드의 속성인 변수 count
에 접근하며 변경할 수 있게 된다.
화살표함수
화살표 함수가 일반 함수랑 다른점은 environment record에서 온다. 화살표함수의 환경레코드에는 일반 함수의 환경레코드와는 다르게 this, arguments object, super, new.target이 없다.
arguments
객체
일반 함수는 전달인자를 받을때 자신만의 arguments 객체를 만들어 환경 레코드에 저장하는데 반해 화살표함수는 가진만의 arguments 객체를 가지지 못하고 외부 arguments 객체에 접근한다. 이때문에 아래와 같은 일이 생긴다.
function myFunction() { let arrowFunction = () => { console.log(arguments); }; arrowFunction(1, 2, 3); } myFunction(4, 5, 6); // Output: [object Arguments] { 0: 1, 1: 2, 2: 3 }
화살표 함수의 logs가 가르키는 arguments
객체는 자신의 환경 레코드의 객체가 아닌 부모 렉시컬 환경에서 가지고 와야한다. 여기서 부모 렉시컬 환경은 myFunction
의 렉시컬 환경이고, myFunction
은 arrowFunction
과 달리 환경 레코드에 자신만의 arguments
객체를 가지므로 arrowFunction
은 myFunction
의 arguments
객체에 접근하게 되는 것이다.
this
선언식으로 만들어진 매소드의 this
는 함수가 불려진 객체를 나타낸다.
let myObject = { prop: 'Hello', myFunction: function() { console.log(this.prop); } }; myObject.myFunction(); // Output: 'Hello'
위 예제에서 myObject
객체 안에 선언된 myFunction
의 this
는 myFunction
매소드가 호출될 때의 객체를 가르키게 된다. 하지만 화살표 함수로 선언된 매소드는 자신만의 this
가 없다. 대신 화살표 함수를 감싸고 있는 함수의 this
를 차용한다.
let user = { firstName: "보라", sayHi() { let arrow = () => alert(this.firstName); arrow(); } }; user.sayHi(); // 보라
위에서 arrow
함수 안의 this
는 sayHi
함수의 this
가 가르키는 객체를 사용한다.
객체 안의 화살표 함수.
let myObject = { prop: 'Hello', regularFunction: function() { console.log(this.prop); }, arrowFunction: () => { console.log(this.prop); } }; myObject.regularFunction(); // Output: 'Hello' myObject.arrowFunction(); // Output: undefined
객체의 환경 레코드에는 this
가 없다.
함수안의 객체안의 화살표 함수.
let obj = { name: 'kim', myFunction() { let myObject = { prop: 'Hello', arrowFunction: () => { console.log(this.name); } }; myObject.arrowFunction(); } } obj.myFunction(); // Output: kim
화살표 함수를 감싸는 myFunction
함수의 this
를 참조함.
Scope
함수의 스코프란 함수가 접근가능한 환경레코드의 집합이라고 할 수 있다. 따라서 스코프는 렉시컬환경과는 약간 다른 개념이다. 위에서 설명 했듯이 함수가 선언될 때, 렉시컬환경의 범위는 내부 환경을 대변하는 환경레코드와 외부렉시컬환경의 참조로 이뤄지지만 만약 함수가 외부 렉시컬환경에서도 변수를 찾기 못하면 함수는 외부렉시컬환경의 외부렉시컬환경으로 변수를 찾아 나서기 때문이다. 이와같은 이유 때문에 스코프의 범위는 함수가 갖는 렉시컬환경 그 이상으로 확대된다.