[JS] variableEnvironment과 LexicalEnvironment

로선생·2022년 5월 24일
1

variableEnvironment

variableEnvironment는 현재 컨텍스트 내의 식별자들에 대한 정보와 외부 환경의 정보를 포함한다.
LexicalEnvironment와 담기는 내용은 같지만, LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 즉시 반영되는 반면, variableEnvironment는 초기 상태를 유지한다.

실행 컨텍스트를 생성할 때, variableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 Lexical environment를 만들고, 이후에는 Lexical environment를 주로 활용한다.

variableEnvironment와 Lexical environment의 내부는 environment Record와 outer environment reference로 구성되어 있다.

LexicalEnvironment

어떤 용어를 대할 때, 사전적인 용어와 타인과의 커뮤니케이션을 위한 용어를 구분할 수 있다.
LexicalEnvironment은 사전적인 용어라고 생각하면 좋다.
즉, '현재 컨텍스트의 내부에는 어떤 식별자들이 있고, 그 외부 정보는 00을 참조하게 되어있다.' 라는 느낌으로 모아놓은 것이다.

(1)environment record와 호이스팅

environment record에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언자 함수가 있을 경우 그 함수 자체, var등으로 선언된 변수의 식별자 등이 식별자에 해당한다.
컨텍스트 내부 전체를 처음부터 끝까지 훑어나가며 순서대로 수집한다.

변수 정보를 수집하는 과정을 모두 마쳤더라도, 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태이다.
코드가 실행되기 전에, 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 알고 있게 되는 것이다.

흔히 호이스팅 개념을 '자바스크립트 엔진이 식별자들을 최상단으로 끌어올린 다음 코드를 실행한다' 로 설명하는데, 실제로 자바스크립트 엔진이 끌어올리지는 않지만, 편의상 끌어올린 것으로 간주한 가상의 개념이다.
(environment record의 수집 과정을 추상화한 개념이다.)

호이스팅 규칙

environment record에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다.

function a(x){
	console.log(x); 
    var x
    console.log(x);
    var x = 2 
    console.log(x);
}

a(1)

호이스팅이 되지 않았을 때, 어떤 값이 출력될지 예상해보자.
첫 번째 x는 함수 호출시 전달된 1이 출력되고, 두 번째에는 선언된 변수에 할당된 값이 없으므로 undefined가 출력되고, 마지막에는 2가 할당될 것 같다.

실제로는 어떨까?

이해하기 쉽게 코드를 바꿔보자.

function a(){
	var x = 1
	console.log(x); 
    var x
    console.log(x);
    var x = 2 
    console.log(x);
}

a()

이 상태에서 호이스팅을 처리해보자.
environment record는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있다.
각 식별자에 어떤 값이 할당되는지는 관심이 없다.
따라서 변수명만 끌어올리고 할당 과정은 원래 자리에 둔다.

function a(){
	var x;
    var x;
    var x;
    
	x = 1
	console.log(x); 
    console.log(x);
    x = 2 
    console.log(x);
}

a()

이후 실제 코드를 출력하면, 1,1,2 가 나온다.

또 하나의 예제를 보자.

function a(){
	console.log(b); 
    var b = 'bbb'
    console.log(b);
    function b(){}
    console.log(b);
}

a()

출력 결과를 예측해보자. 첫 번째 b는 값이 없으니 undefined, 2는 bbb, 3은 함수가 출력될 것 같다.
실제로는 어떨까?

a함수를 실행한 순간 a함수의 실행 컨텍스트가 생성된다.
호이스팅이 일어나고, 다음과 같은 형태로 변화한다.

호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것으로 여길 수 있다.

function a(){
	var b
    var b = function b(){}  // 함수명으로 선언한 변수에 함수를 할당한 것으로 이해할 수 있다. 
	console.log(b); 
    b = 'bbb'
    console.log(b);
    console.log(b);
}

a()

그렇게 되면, 출력은 처음 b함수, bbb,bbb 순으로 출력되는 것을 확인할 수 있다.

함수 선언문과 함수 표현식

둘 다 함수를 새롭게 정의할 때 쓰이는데, 그 중 함수 선언문은 function정의부만 존재하고, 별도의 할당 명령이 없는 것을 의미한다.
반대로 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다.
또한 함수 선언문은 반드시 함수명이 정의되어 있어야 하는 반면, 함수 표현식은 없어도 된다.
함수명을 정의한 함수 표현식을 기명 함수 표현식, 정의하지 않은 것을 익명 함수 표현식이라고 한다.

// 함수선언문
function a(){ }
a(); 

// 함수표현식
var b = function(){ }
b();

var c = function d(){ }
c()
d() // error

예제를 통해 실질적인 차이를 살펴보자.

console.log(sum(1,2))
console.log(multiply(3,4))

function sum(a, b){
	return a + b
}

var multiply = function (a, b){
	return a + b
}

실행 컨텍스트의 LexicalEnvironment는 두 가지 정보를 수집하는데, 여기서는 그 중에서 environment record의 정보 수집 과정에서 발생하는 호이스팅을 살펴보는 중이다.

호이스팅을 마치면 다음과 같이 변한다.

var sum = function sum(a, b){
	return a + b
}

var multiply;

console.log(sum(1,2))
console.log(multiply(3,4))

multiply = function (a, b){
	return a + b
}

함수 선언문은 전체를 호이스팅 한 반면 함수 표현식은 변수 선언부만 호이스팅했다.
함수를 다른 변수에 값으로써 할당한 것이 곧 함수 표현식이다.

따라서, sum함수는 선언 전에 할당해도 아무 문제가 없다. 어떻게 작성해도 오류가 나지 않기 때문에, 도움이 되기도 반대로 큰 혼란을 주는 원인이 되기도 한다. (오류를 찾지 못할 수도 있기 때문)

(2) 스코프, 스코프 체인, outer environment reference

스코프는 식별자에 대한 유효범위이다. 경계 밖에서 선언한 변수는 a함수 내부에서도 접근이 가능하지만, a함수의 내부에서 선언한 변수는 a함수 내부에서만 접근할 수 있다.
이러한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 한다.
이를 가능하게 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outer environment reference 이다.

스코프 체인

outer environment reference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.

예를 들어, a함수 내부에 b함수를 선언하고, b함수 내부에 c함수를 선언한 경우,
함수 c의 outer environment reference는 함수 b의 LexicalEnvironment를 참조한다.
함수 b의 LexicalEnvironment에 있는 outer environment reference는 다시 함수b가 선언되던 때, a의 LexicalEnvironment를 참조하는 것이다.

이처럼 outer environment reference는 연결리스트 형태를 띈다.
선언 시점의 LexicalEnvironment를 계속 찾아 올라가면 마지막에는 전역 컨텍스트의 LexicalEnvironment가 있을 것이다.
또한 outer environment reference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능하다.

이와 같은 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는, 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.

var a = 1;

var outer = function (){
	var inner = function (){
    	console.log(a);
        var a = 3
    }
    inner()
    console.log(a);

} 
outer()
console.log(a);

1)전역 컨텍스트 활성화:

  • environment record에 a, outer 식별자가 담긴다.
  • 전역 컨텍스트는 선언 시점이 없으므로, outer environment reference에는 아무것도 담기지 않는다.
  • a에 1을, outer에 함수를 할당한다.
  • outer함수가 호출되면, 전역컨텍스트의 코드는 일시중단되고 outer실행 컨텍스트가 활성화된다.

2)outer 컨텍스트 활성화:

  • environment record에 inner식별자를 저장한다.
  • outer environment reference에는 outer가 실행될 당시의, 즉 전역 컨텍스트 LexicalEnvironment를 참조복사한다. 이를 [global, {a, outer} ]이라고 보기 쉽게 표기하자.
  • inner함수가 호출되면 outer 실행 컨텍스트의 코드는 임시중단되고 inner컨텍스트가 활성화된다.

3)inner 컨텍스트 활성화:

  • inner 실행 컨텍스트의 environment record에 a 식별자를 저장한다.
  • outer environment reference에는 inner함수가 실행될 당시의 LexicalEnvironment를 참조복사한다.
    [outer, {inner} ]
  • 식별자 a에 접근하고자 한다. 우선 현재 활성화 상태인 inner컨텍스트의 environment record에서 a를 검색한다. a가 발견되었는데, 여기에는 아직 할당된 값이 없다. 따라서 undefined가 출력된다.
  • inner 스코프에 있는 변수 a에 3을 할당한다.
  • 함수가 종료되고 inner실행 컨텍스트가 콜스택에서 제거되고 outer가 이어서 시작된다.

4) outer 컨텍스트 활성화:

  • a에 접근하고자 한다. 활성화된 실행 컨텍스트의 LexicalEnvironment에 접근하여 environment record에 a가 있는지 찾아보고, 없으면 outer environment reference의 environment record에 접근하여 넘어가는 식으로 계속 검사한다. 여기서는 전역 LexicalEnvironment에 a가 있으니 해당 값 1을 반환한다.
  • outer 실행 컨텍스트가 콜스텍에서 제거된다.

5)전역 컨텍스트 활성화:

  • 식별자 a에 접근하고자 한다. 현재 활성화 상태인 전역 컨텍스트의 environment record에서 a를 검색한다. 1이 출력된다.
  • 전역 컨텍스트가 콜스텍에서 제거되고 종료된다.

변수 은닉화

스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능하지는 않다.
코드 상의 식별자 a는 전역공간, inner함수 모두에서 선언되었다.
inner 함수 내부에서 a에 접근하려고 하면 무조건 스코프 체인 상의 첫 번째 인자, inner 스코프의 내부에서 검색할 수밖에 없다.
함수 내부에서 선언한 변수가 있기 때문에 함수 외부에서 선언한 동일한 이름의 a변수에는 접근할 수 없다. 이를 변수 은닉화라고 한다.

코드의 안전성을 위해 가급적 전역변수 사용을 최소화하고자 노력하는 것이 좋다.

profile
이제는 이것저것 먹어요

0개의 댓글

관련 채용 정보