Lexical Environment

김상연·2022년 12월 4일

JavaScript

목록 보기
10/19

시작하기에 앞서 아래의 내용은 const 또는 let 으로 선언된 변수를 상정한다. 고전적인 방법 var 로 선언된 변수의 경우는 다르다.

1. 배경

{ ... } 코드블럭 내에서 선언된 변수는 그 안에서만 존재한다. 해당 코드블럭 밖에서는 참조 할 수 없는 것이다. 함수 또한 마찬가지다.

for, if, while 등의 경우도 마찬가지로 코드블럭 내부에서 선언된 변수는 내부에서만 참조 할 수 있다.

그런데 함수 코드블럭에서는 바깥의 변수도 참조 할 수 있다. 이 때, 함수 내부에 nested 함수를 선언 할 수 있고, 심지어 이것을 값으로 가진 객체 또는 nested 함수 자체를 리턴 할 수도 있다. 이렇게 리턴 된 nested 함수는 어떤 실행 컨텍스트에서든 동일한 외부 변수(nested 함수를 감싸고있는 부모함수의 코드블럭 내에 선언된)에 접근 할 수 있다.

예를들면 아래와 같은 결과를 얻을 수 있다. (당장 이해되지 않더라도 아래에서 자세히 풀이 해 준다.)

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

counter() // 0
counter() // 1
counter() // 2
		  // 독립접인 변수 count를 갖는게 아니라 모두 동일한 변수 count를 참조한다.

2. 변수

함수의 실행 환경, 코드 블럭, 또는 스크립트 그 자체는 모두 내부에(숨겨진) 객체를 하나씩 가지고 있는데, 이를 Lexical environment 라 한다. 이것은 두 부분으로 구성된다.

  • Environment Record : this 또는 지역 변수들을 속성으로 가진 객체
  • outer Lexical Environment 의 참조 (부모 코드 블럭의 그것)

즉, 변수란 이 내부(숨겨진) 객체의 속성을 말한다. 우리가 변수를 읽고, 조작하는 것은 사실 이 객체의 속성에 접근하고, 수정하는 것이다. 물론 이것 자체에 직접 접근하거나 조작 할 수는 없다.

스크립트 자체도 당연히 (global) lexical environment를 생성한다. 코드는 여러 줄이고, 이것들이 순차적으로 실행되면서 새로운 변수가 선언되거나 할당되고, 이후 각각이 다시 다른 값으로 덮어 씌워지기도 한다. 즉 코드를 한 줄 한 줄 실행 해 감에 따라 lexical environment는 수정되어간다.

한 가지 알아 둘 것은 스크립트의 실행 즉시 현 실행 컨텍스트에 선언된 모든 변수를 pre-populate한다는 것이다. 이 때 이 속성들은 기술적으로(technically) uninitialized state 인데 이 말의 의미는 js엔진이 해당 변수들의 존재를 알고는 있지만 let 또는 const 를 만나 선언 되기 전 까지는 참조 할 수 없는 상태라는 뜻이다.

3. 함수 선언

함수는 또한 값이다. 변수처럼. 차이점이라면 함수 선언은 즉시 initialized 된다는 것이다. 즉시 사용 할 수 있는 상태가 된다. 이것이 우리가 함수를 선언부를 만나기 전부터 함수를 사용 할 수 있는 이유다.

물론 함수 선언 의 경우에 한하고, let 이나 const 에 할당하는 함수 표현식 의 경우는 그렇지 않다.

4. 내부 / 외부 Lexical Environment

함수가 실행되는 순간, 새로운 lexical environment 가 생성된다. 당연히 이것이 내부, 기존의 것이 외부 환경이 된다.

앞서 설명했듯 inner lexical environmentenvironment record 뿐만 아니라 outer lexical environment 에 대한 참조도 가지고 있다.

우리가 어떤 변수에 접근하면 JS엔진은 일단 inner lexical environment 에서 찾아보고, 없다면 외부로, 그래도 없으면 외부로... 결국 global lexical environment 까지 가서 찾아낸다.

추가로 use strict 모드에서는 global lexical environment 에서도 찾지 못한 변수(어디에서도 선언되지 않은 변수)에 값을 할당하려 하면 오류를 이르킨다. 해당 모드가 아니라면 (예전에 쓰여진 코드들과으 호환성을 위해) global lexical environment 에 해당 변수를 속성으로 생성한다.

5. closure 의 원리 : 함수는 자신이 만들어진 위치를 기억한다.

closure 란 함수의 일종을 말하는데, 외부 변수에 접근 할 수 있는 함수를 말한다. 이것은 프로그래밍 언어에 따라 생성할 수 없거나, 특별한 방법을 통해 구현 될 수도 있다. 하지만 JS에서는 기본적으로 모든 함수가 클로져다.

new Function() 문법이 유일한 예외다. 이것은 클로져가 아니다.

위의 소제목에 썼듯이 함수는 자신이 만들어진 위치를 기억한다. 이것은 변수(함수도 변수에 할당된 값이다.)의 (역시나)숨겨진 속성 [[environment]] 에 저장되어있다. 이를 통해 함수 자신이 만들어진 위치(코드블럭)의 lexical environment 에 접근할 수 있는 것이다.

6. 배경의 예시 코드 자세히 보기

이해를 돕기 위해 비교되는 케이스를 조금 추가했다.

let globalVariable = 0;

function mother() {
	console.log('globalVariable is : ', globalVariable++)

	let innerVariable = 0;

	return function() {
    	console.log('nested: ', globalVariable++)
		return innerVariable++
	}
}

const counter = mother();	//	(1)

console.log( mother()() );	//	(2)
console.log( mother()() );	//	(3)
console.log( mother()() );	//	(4)

console.log( counter() );	//	(5)
console.log( counter() );	//	(6)
console.log( counter() );	//	(7)

위와 같은 스크립트를 실행하면 아래와 같은 결과가 나온다.

globalVariable is : 0	//	(1)

globalVariable is : 1	//	(2)
nested : 2				// 	(2)
0						//	(2)

globalVariable is : 3	//	(3)
nested : 4				// 	(3)
0						//	(3)

globalVariable is : 5	//	(4)
nested : 6				// 	(4)
0						//	(4)

nested : 7				// 	(5)
0						//	(5)

nested : 8				// 	(6)
1						//	(6)

nested : 9				// 	(7)
2						//	(7)

//	보기 편하라고 줄바꿈 했다.

(1)의 경우는 제쳐두고, (2) ~ (4)과 (5) ~ (7) 의 두 가지 경우로 나뉜다.

  • (2) ~ (4)
    globalVariable이 계속 늘어난다. 그런데 innerVariable은 그대로다.

  • (5) ~ (7)
    globalVariable이 계속 늘어난다. 동시에 innerVariable도 계속 늘어난다.

계속 늘어나는 것은 당연히 ++단항연산자 때문인데, 보다 중요한 점을 시사한다. 같은 변수, 즉 같은 lexical environment 의 속성을 참조한다는 것이다. 이와 반대로 계속늘어나지 않는 것은 서로 다른 lexical environment 의 속성을 참조한다는 뜻이다.

(2) ~ (4)는 매번 mother 함수를 호출하고 있다. mother 함수는 호출되는 순간 그 자신의 lexical environment 를 생성한다. (2) 이후에 (3), (4)에서 보듯이 mother 함수가 또다시 호출되면 어떨까? 또 하나의 lexical environment 를 생성한다. (2) ~ (4)는 mother 함수가 리턴한 함수들을 곧바로 실행하고 있는데, 최종적으로 실행되는 함수들은 그래서 각각의 mother 함수에 생성된 lexical evironment 를 참조하고 있는 것이다.

이와 달리 (5) ~ (7)은 어떤가? mother 함수는 한번만 실행된다. 실행의 리턴인 최종 함수를 변couter변수에 담아 (5) ~ (7)에 걸쳐 세번 실행하는 것이다. 즉 하나의 mother 함수에 생성된 lexical environment 를 참조하고 있으니 ++단항연산자 의 결과를 공유하는 것이다.

한편 glovalVariable은 global lexical environment 의 속성이므로, mother가 한번 호출되던, 여러번 호출되던 상관없이 무조건global lexical environment 를 참조하게 된다. 그래서 어느곳에서 수정하던 어디에서나 같은 결과를 공유하는 것이다.

profile
리눅스와 컴퓨터 프로그래밍

0개의 댓글