변수의 유효범위(Scope)와 클로저, 가비지 컬렉션
JS : 함수지향 언어로 자유도가 높음. (높은 자유도가 단점이 되기도 함...ㅠ)
그때문에 JS의 함수는 동적 생성, 인자로 넘기기, 생성된 곳이 아닌 곳에서 호출이 가능
함수 내부에서 외부에 있는 변수에도 접근이 가능함
사용하다 보면 의문점이 생기는데
1. 함수 생성 이후 외부 변수가 바뀌면 어찌되는가??
참조하고 있는 값이 바뀌는거니 같이 바뀜
2. 매개변수로 함수를 넘겨 먼곳에서 호출하면 어떻게 됨? 호출된 곳을 기준으로 외부변수에 접근?
아뇨, 생성된 곳을 기준으로 외부변수에 접근하는 scope 가 결정됨. 호출된곳과는 무관함.
이렇게 답을할 수 있겠다.
아래 내용을 읽기 이전에 대략적으로 필요한 단어들을 정리하고 넘어가겠다.
외부 변수 : 함수 밖의 변수
내부 변수 : 함수 안의 변수
스코프 : 변수의 유효 범위
초기화 : 변수 선언 + 값 할당
프로그래밍 언어의 스코프 결정 방식은 두가지가 있음 [동적 스코프, 정적(렉시컬) 스코프]
동적 스코프 : 함수 호출 환경에 따라 상위 스코프를 결정
렉시컬 스코프 : 함수 생성(선언) 환경에 따라 상위 스코프를 결정, 대부분의 언어에서 채택함
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
위의 실행 결과는 JS 가 렉시컬 스코프를 가진다는것을 확인할 수 있다.
스코프라는 개념이 생소해서 일반적이지 않은것 같지만 함수가 호출 될 때마다 해당하는 함수에 렉시컬 스코프가 지정되는 아주 일반적인 과정임.
( * 렉시컬 스코프는 스코프를 결정하는 방식이고, 렉시컬 환경은 스코프에 의해 결정된 값들이 저장된 것이다.)
이제 함수의 유효 범위를 하나씩 찾아보자.
1. 코드 블록
{ ... } 안에서 선언된 변수는 블록 안에서만 사용 가능
2. 중첩 함수 (Nested functions) : 함수 내부에서 선언된 함수
흔히 사용됨
중첩함수는 새로운 객체의 프로퍼티나 중첩함수 그 자체로 반환 가능
이때도 외부 변수에 접근하는것은 동일함
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}; // 난수 발생기 만들기 좋겠네...
return count++; 구문을 실행하는 익명 함수에서 외부 변수는 count 임
3. 렉시컬 환경
렉시컬 환경은 명세서에만 존재하는 이론적인 내용일 뿐임
중요하니 하니씩 알아봅시다... 조금 복잡해요
변수
실행중인 함수, 코드블럭 { ... }, 스크립트 전체는 렉시컬 환경이라 불리는 내부 숨김 연관 객체를 가짐
렉시컬 환경 : 환경 레코드 (모든 지역변수를 프로퍼티로 저장, e.g. this) + 외부 렉시컬 환경
코드에서 사용하는 변수는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐임
변수를 조작하는 것도 환경레코드의 프로퍼티를 조작하는것임
let phrase = "hello";
위 코드의 렉시컬 환경은 환경 레코드 인 phrase : 'hello" 하나만 존재
외부 렉시컬 환경 에 대한 참조는 없음,
(* 스크립트 전체와 관련된 렉시컬 환경은 글로벌 렉시컬 환경)
과정을 한번 봅시다
1. 스크립트 실행 시 선언한 변수 전체 (phrase, ... )가 렉시컬 환경에 올라감 (uninitialized)
2. phrase = undefined 상태
3. phrase = "hello" 값 할당
4. phrase = "anything else..."; 할당 이후로 마음대로 사용 가능
함수 선언문
함수 또한 변수와 마찬가지로 값임
다만 함수 선언문으로 만든 함수는 일반 변수와 달리 바로 초기화 되어 렉시컬 환경이 만들어지는 즉시 사용 가능
선언 이전에 함수가 사용 가능한 이유가 이 때문임
let, const 변수에 할당된 함수 표현식은 할당 될 때까지 사용이 불가함... (초기화 X, 선언은 되었지만 할당이 안됨)
( * 변수들은 호이스팅되어 선언은 되지만 할당되지 않아 사용 불가능)
함수 선언문에만 해당하지 함수 표현식은 해당하지 않음
내부와 외부 렉시컬 환경
함수를 호출해서 실행하면 새로운 렉시컬 환경이 생성됨 (렉시컬 환경은 선언된 환경을 따름)
렉시컬 환경에는 전달된 파라미터와 함수의 지역변수가 저장됨
let phrase = "hello";
function say(name) {
alert (`${phrase}, ${name});
};
함수 호출 중일 때 내부 렉시컬 환경, 내부 렉시컬 환경이 가리키는 외부 렉시컬 환경을 가짐
2가지로 나눠서 볼 수 있다는거임
위의 예시에서
내부 렉시컬 환경 : 현재실행 중인 say 함수에 상응, 함수 인자인 name : "John" 값 가지고 있음
또한 외부 렉시컬 환경에 대한 참조를 가짐
외부 렉시컬 환경 : 전역 렉시컬 환경임. 전역 렉시컬 환경인 phrase, say 를 프로퍼티로 가짐
변수 접근 시 내부 렉시컬 환경을 검색 범위로 잡고 없으면 외부 렉시컬 환경으로 확장, 전역 환경까지 반복함
그래도 못찾으면 에러가 발생한다. 웹브라우저에서 캡처링 과정이랑 비슷함
변수의 검색 순서를 봅시다!
1. say 함수의 alert에서 name 에 접근 할 떄, 내부 렉시컬 환경을 찾아봄, name 이 있음
2. phrase에 접근하려는데 함수의 내부 렉시컬 환경에서는 그 값이없다
3. 외부로 확장해서 찾아봄. 외부 렉시컬에서 찾음
함수를 반환하는 함수
function makeCounter() {
let count = 0;
return function() {
return count++;
};
};
let counter = makeCounter(); // 이전에 만들어놓은 값을 재활용하는거네??
makeCounter() 호출 시 새로운 렉시컬 환경 객체가 만들어지고, 함수실행에 필요한 변수들이 저장됨
여기서는 두개의 렉시컬 환경이 만들어짐 ( 함수 내부 렉시컬 환경(counter), 외부 렉시컬 환경(global))
여기서 return 에 한줄짜리 중첩 함수가 만들어짐 ( 생성되기만 하고 실행은 안된 상태라 가정 )
모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억함 ( 생성 = 선언 )
[[Environment]]라 불리는 숨김 프로퍼티에 만들어진 곳의 렉시컬 환경에 대한 참조가 저장됨
그래서 counter.[[Environment]]에 { count:0 }이 있는거임
자신이 생성된 곳을 기억하는 이유가 [[Environment]] 때문임. 이 프로퍼티에는 함수가 생성될 때 단 한번 값이 세팅되고 변하지 않음
(호출 장소와 상관없음, 자신이 태어난곳! => 렉시컬 스코프)
코드로 다시 돌아가보자면
counter()를 호출하면 호출할 때 마다 새로운 렉시컬 환경이 생성됨
생성된 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로 참조함
다음줄로 넘어가서 실행 흐름이 중첩함수의 본문 return count++ 에 도달하였으면 count 변수가 필요한 상황임.
과정을 하나씩 따라가봅시다.
위 코드의 중첩된 익명함수에는 지역변수가 없어 렉시컬 환경이 비어있음. 다음으로 넘어감
counter() 의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count 값 찾음
count++ 의 변수값 갱신은 변수가 저장된 렉시컬 환경에서 이뤄짐
클로저 ( Closure )
외부 변수를 기억하고 외부 변수에 접근할 수 있는 함수
모던 자바스크립트를 참고해서 정리하고 있는데 모든 함수가 자연스럽게 클로저가 된다고 한다.
즉, 글로벌 변수를 조작하는 일반적인 함수라면 그 또한 클로저라고 할 수 있다. ( 예외사항 : new Funciton() )
다만, 이리저리 돌아본 결과 사람들이 흔히 생각하는 클로저는 함수를 반환하는 함수를 호출하여 함수 내부의 값에 접근하는 케이스를 말하는 듯 하다.
// 일반적인 함수가 클로저인가?
let alpha = 1;
function up() { // up 함수에서 외부 변수는 alpha
alpha++;
}
// 함수를 반환하는 함수가 클로저인가?
function makeCounter() {
let count = 0;
return function() {
return count++; // 익명함수에서 외부 변수는 count
};
};
무튼 클로저의 요점은 다음과 같다.
JS의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서, 어떤 환경에서 생성되었는지 기억한다.
함수 본문에서는 [[Environment]] 를 사용해 외부 변수에 접근하여 조작 가능.
( * 근데 굉장히 헷갈리는 점은 클로저라는 정의가 명확하게 쓰여진곳이 잘 없어서 클로저가 위와같은 함수를 말하는것인지 위와 동일하게 나타나는 현상을 말하는것인지 굉장히 애매모호하다. )
가비지 컬렉션 ( Garbage collection )
일반적으로 함수 호출 끝나면 렉시컬 환경이 메모리에서 제거되기 때문에 관련 변수를 참조 할 수 없다.
다만, 여전히 도달가능한 경우가 있다.
함수를 반환하는 중첩함수의 반환된 함수(내부)에 의해 [[Environment]] 프로퍼티에 외부 함수 렉시컬 환경에 대한 정보가 저장됨
function f() {
let value = 'alpha';
return function() {
alert(value);
}
}
let g = f();
// g.[[Environment]]에 f() 호출 시 만들어지는 렉시컬 환경 정보가 저장됨
중첩 함수 사용시 주의할점은, 호출 시 만들어지는 각 렉시컬 환경은 모두 메모리에 유지된다는점임
g = null;
그렇기 때문에 명시적으로 null 을 할당하여 가비지 컬렉터가 렉시컬 환경을 제거하도록 함.
추가적으로 조심해야 할 부분은 아래와 같다.
let arr = [f(), f(), f()];
위의 코드를 작성하면 서로 다른 3개의 렉시컬 환경이 만들어지는거임. 가비지 컬렉터가 삭제하기 위해서는 개별적으로 null 을 할당해야 한다.
함수는 독립적인 렉시컬 환경을 가진다! 이게 가장 중요한듯??
변수의 유효범위와 클로저 (javascript.info)
변수의 유효범위와 클로저
ko.javascript.info
위의 링크를 보고 정리하였습니다. 보다 자세한 설명은 링크를 참조하세요.
'Javascript' 카테고리의 다른 글
String 값은 어디에 저장이 될까? (0) | 2022.12.01 |
---|---|
자바스크립트의 Array (0) | 2022.11.30 |
함수 정리 (0) | 2022.11.29 |
Closer (0) | 2021.11.14 |
버블링과 캡처링 (0) | 2021.11.12 |
댓글