본문 바로가기
Archive

렉시컬 환경과 클로저

by livemehere 2022. 8. 31.

자바스크립트는 함수 지향언어라는 점에서 개발자가 자유도를 느낄 수 있다.

한번 작성한 함수는 어디서든 호출해서 사용할 수 있기 때문이다.

그래서 함수에대한 특성을 잘 알고있어야 그만큼 입맛대로 사용할 수 있다.

 

아래는  {} 코드블록으로 변수의 범위를 분리했다.

let,const로 선언한 변수는 블록이 변수의 범위이다.

{
  // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

  let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

  alert(message); // 안녕하세요.
}

alert(message); // ReferenceError: message is not defined

 

하지만 var를 사용하면 결과가다르다.

var의 유효범위는 함수단위이기 때문이다. 

var는 호이스팅된다는 점도 있기때문에, 예상치못한 사이드이펙트가 발생하기 쉬워서 근례 작성되는 코드에서는 보기드물다.

{
    // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

    var message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

    console.log(message); // 안녕하세요.
}

console.log(message); // 안녕하세요.

 

좀 더 확실하게 확인하면, 이렇게 동일한 변수를 두번 선언하는 것도 가능하다.

정확히는, 서로 범위가 다르기때문에, 그냥 각각 선언한 것이다.

{
  // 메시지 출력
  let message = "안녕하세요.";
  alert(message);
}

{
  // 또 다른 메시지 출력
  let message = "안녕히 가세요.";
  alert(message);
}

 

중첩 함수

아래처럼 함수안에서 함수를 작성하는 패턴은 자바스크립트에서 자주 볼 수 있는 패턴이다.

변수, 함수의 유효범위가 {} 블록이라는 점과 함수는 외부변수를 참조할 수 있고, 외부에서는 내부변수 참조가 불가능 하다는 점을 활용할 때 자주 쓰인다.

function sayHiBye(firstName, lastName) {

  // 헬퍼(helper) 중첩 함수
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

 

이런식으로 사용할 수 있다.

이런 방식으로 react의 useState()를 구현할 수 있다.

또 두 번 생성하면 각각의 환경을 가진다.

function counter(){
    let count = 0;
    return function(){
        return count++;
    }
}

const c1 = counter();
const c2 = counter();

console.log(c1()) // 0
console.log(c1()) // 1
console.log(c1()) // 2

console.log(c2()) // 0

렉시컬 환경

자바스크립트에서는 실행중인 함수, 코드블록 {} , 스크립트 전체는 렉시컬 환경(Lexical Environment)라 불리는 내부 숨김연관객체를 갖는다.

 

렉시컬 환경 객체는 두 부분으로 구성되는데

1. 환경 레코드(Environment Record) : 모든 지역변수를 프로퍼티로 저장한 객체 및 this를 저장

2. 외부 렉시컬 환경(Outer Lexical Environment) 에대한 참조 : 외부 코드와 연관

 

렉시컬 환경은 명세서에만 존재한다

자바스크립트가 어떻게 동작하는지에대한 설명일 뿐, 직접 조작하거나 객체를 얻을 수 없다.

 

여기서 재밌는 사실을 알아낼 수 있었다.

함수가 선언문이 오기도 전에 사용할 수 있는 내부동작이 변수와 함수와의 차이가 있었기 때문이다.

변수와 함수가 렉시컬 환경에 등록되는 과정

렉시컬 환경은 스크립트가 시작되면 다음과 같은 과정을 거친다

그림과 같이 시작과 동시에 변수와 함수를 모드 렉시컬 환경에 저장하는데

사실상 구문을 한줄도 읽지 않은 상태인데도 function 을 즉시 초기화한다.

반면, 변수는 한줄 한줄 읽어나가면서 let, const 구문을 만났을때, 초기화가 되면서 사용이 가능하다.

다만, 함수 표현식은 변수이기 때문에 해당하지 않는다.

 

내부와 내부 렉시컬 환경

함수를 호출하면 새로운 렉시컬 환경이 만들어지고, 함수 호출시 넘겨받은 매개변수는 실행된 함수의 지역변수로 저장이된다.

그리고 외부 렉시컬환경에대한 참조를 가지고있는다. 이는 함수의 [[Environment]]라는 숨김 프로퍼티에 자신이 생성된 곳을 기억함으로서 가능하다. 즉, [[Environment]]에는 외부 렉시컬 환경에대한 참조가 저장되어있다.

이원리를 통해서, 함수 내부는 외부의 값을 참조할 수 있고, 외부는 내부의 렉시컬 환경을 가지고있지 않기 때문에 참조할 수 없는 것이다.

이런 원리를 통해서 아래와 같이 렉시컬 환경을 활용해서, 독립적인 렉시컬 환경을 가지는 함수를 만들수 있다

makeCounter()를 실행할 때마다 새로운 렉시컬 환경이 만들어지고, 개별적인 카운터를 생성할 수 있다.

클로저(closure)

이제 클로저를 이해할 수 있다.

클로저는 외부 변수를 기억하고, 외부변수에 접근할 수 있는 함수를 의미한다.

 

자바스크립트에서는 모든 함수가 자연스럽게 클로저가 된다. (new Function을 이용해 함수를 만들면 [[Environment]]가 현제 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조하는 예외가 있다. 즉, 생성자 함수는 외부 변수에 접근할 수 없고, 전역 변수에만 접근 가능하다)

 

가비지 컬렉션

자바스크립트에서는 도달할 수 없는 상태의 객체는 메모리에서 제거된다.

일반함수는 실행이 끝나면 렉시컬 환경이 메모리에서 제거되는데, 이 때문에 함수호출이 끝나면 함수 내부의 변수를 참조할 수 없는 이유이다.

그런데 클로저 함수는 외부의 변수가 내부 함수에 의해서 참조되기 때문에 사라지지 않는다.

그래서 명시적으로 아래 코드처럼 null을 할당해서 참조를 제거해주어야 메모리에서 사라지게된다.

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g가 살아있는 동안엔 연관 렉시컬 환경도 메모리에 살아있습니다.

g = null; // 도달할 수 없는 상태가 되었으므로 메모리에서 삭제됩니다.

기괴한 모습

이제 이렇게 함수를 두번호출하는것과 같은 모습을 이해할 수 있다.

클로저함수를 실행하는 것이다.

function sum(a) {

  return function(b) {
    return a + b; // 'a'는 외부 렉시컬 환경에서 가져옵니다.
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4

 

렉시컬 환경에 대해서 조금더 이해하고나니 자바스크립트의 동작에대해서 더 납득이간다.

프로그래밍에서 의문점을 가질때 "왜?"라는 질문이아닌, "그렇다니까 그런거다" 라는 시점이있다.

예를들면 "1+1 왜 2인가요? -> 그게 규칙이니까"

처럼 함수는 어떻게 외부변수에 참조할수있고, 왜 내부변수에 참조할 수없나요? 라는질문에 이제는 대답할 수 있다.

그냥막연히 그런거니까, 그런규칙이니까 라고 알고있었는데, 뭔가 마법을 풀어낸거같다

반응형

'Archive' 카테고리의 다른 글

잘안알려진 함수특성  (0) 2022.08.31
전역 객체  (0) 2022.08.31
재귀와 실행컨텍스트  (0) 2022.08.31
JSON 과 메서드  (0) 2022.08.30
나를 괴롭히던 Date 정리  (2) 2022.08.30