2-3 LexicalEnviroment
"현재 컨텍스트 내부에는 a,b,c 식별자들이 있고 그 외부 정보는 d 를 참조하도록 구성되어 있다." 라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것
enviromentRecord
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장
다음은 enviromentRecord 에 저장되는 정보들로 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집
- 함수에 지정된 매개변수 식별자.
- 선언한 함수가 있을 경우 그 함수 자체.
- 선언된 변수의 식별자.
변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 이전 상태
- 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈
- 엔진의 실제 동작 방식 대신에 “자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다.” 라고 생각할 수 있다. → 호이스팅(Hosting)
호이스팅(Hositing) & 규칙
“끌어올리다” 의미로, 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념
자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주
변수에 대한 호이스팅
function a() {
var x = 1; // 수집 대상 1
console.log(x); // 1 ------ (1)
var x; // 수집 대상 2
console.log(x); // 1 ------ (2) << undefined 가 아님 ! 🔎
var x = 2; // 수집 대상 3
console.log(x); // 2 ------ (3)
}
a(1);
enviromentRecord 는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없음
따라서, 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남는다. (매개변수도 마찬가지.)
function a() {
var x; // 수집 대상 1의 변수 선언 부분
var x; // 수집 대상 2의 변수 선언 부분
var x; // 수집 대상 3의 변수 선언 부분
x = 1; // 수집 대상 1의 할당 부분
console.log(x); // 1 ------ (1)
console.log(x); // 1 ------ (2) << undefined 가 아님 ! 🔎
x = 2; // 수집 대상 3
console.log(x); // 2 ------ (3)
}
a(1);
함수 선언의 대한 호이스팅
원본 코드
function a() {
console.log(b); // [Function: b] ------- (1)
var b = "bbb"; // 수집 대상 1(변수 선언)
console.log(b); // bbb ----------------- (2)
function b() {} // 수집 대상 2(함수 선언)
console.log(b); // bbb ----------------- (3)
}
a();
호이스팅을 마친 상태
function a() {
var b; // 수집 대상 1. 변수는 선언부만 끌어올린다.
function b() {} // 수집 대상 2. 함수 선언은 전체를 끌어올린다.
console.log(b); // [Function: b] ------- (1)
b = "bbb"; // 변수의 할당부는 원래 자리에 남겨둔다.
console.log(b); // bbb ----------------- (2)
console.log(b); // bbb ----------------- (3)
}
a();
함수 선언문을 함수 표현식으로 바꾼 코드
function a() {
var b;
var b = function b() {};
console.log(b); // [Function: b] ------- (1)
b = "bbb"; // 수집 대상 1(변수 선언)
console.log(b); // bbb ----------------- (2)
console.log(b); // bbb ----------------- (3)
}
a();
함수 선언문과 함수 표현식
둘 다 함수를 정의할 때 쓰이는 방식
- 함수 선언문(function declaration)
** 반드시 함수명이 정의되어 있어야 한다. ( 기명 함수 표현식 )
- 함수 표현식(function expression)
** 함수명이 없어도 된다. (= 익명 함수 표현식 )
function a() { ... } // 함수 선언문 = 함수명 a 가 곧 변수명
a(); // 실행 O
var b = function() { ... } // (익명)함수 표현식 = 변수명 b가 곧 함수명
b(); // 실행 O
var c = function d() { ... } // 기명 함수 표현식 = 변수명은 c, 함수명은 d
c(); // 실행 O
d(); // 에러 !
함수 선언문과 함수 표현식에 대한 호이스팅 비교 예제
console.log(sum(1, 2)); // 3
console.log(multiply(3, 4)); // TypeError: multiply is not a function
// sum 함수 (by. 함수 선언문)
function sum(a, b) {
return a + b;
}
// multiply 함수 (by. 함수 표현식)
var multiply = function (a, b) {
return a * b;
};
// sum 함수 (by. 함수 선언문) -> 함수 전체가 호이스팅됨.
var sum = function sum(a, b) {
return a + b;
};
// multiply 함수 (by. 함수 표현식) -> 변수 선언부만 호이스팅됨.
var multiply;
console.log(sum(1, 2)); // 3
console.log(multiply(3, 4)); // TypeError: multiply is not a function
// multiply 함수 (by. 함수 표현식)
multiply = function (a, b) {
return a * b;
};
- 함수 선언문은 함수 전체가 호이스팅
- 반면, 함수 표현식은 변수 선언부만 호이스팅
함수도 하나의 값 으로 취급할 수 있다는 것이 바로 이것
함수를 다른 변수에 값 으로써 할당 한 것이 곧 함수 표현식
함수 선언문의 위험성
// ..생략..
console.log(sum(3, 4);
// ..생략..
function sum(x, y) {
return x + y;
}
// ..생략..
var a = sum(1, 2);
// ..생략..
function sum(x, y) {
return x + ' + ' + y + ' = ' + (x + y);
}
// ..생략..
var c = sum(1, 2);
console.log(c);
// ..생략..
위와 같이 상단에 선언한 함수와 이후 코드 어딘가에 동일한 이름으로 다시 선언한 경우
- 전역 컨택스트가 활성화될 때 전역공간에 선언된 함수들이 가장 위로 끌어올려진다.
- 동일한 변수명에 서로 다른 값을 할당한 경우 나중에 할당한 값이 먼저 할당한 값을 덮어씌우게 된다. (= 오버라이딩, override)
이것을 함수 표현식을 사용한다면 비교적 조기에 에러를 발견하거나 안전하게 사용 가능
// ..생략..
console.log(sum(3, 4); // Uncaught Type Error: sum is not a function
// ..생략..
var sum = function (x, y) { // 함수 선언문 -> 함수 표현식
return x + y;
}
// ..생략..
var a = sum(1, 2);
// ..생략..
var sum = function (x, y) { // // 함수 선언문 -> 함수 표현식
return x + ' + ' + y + ' = ' + (x + y);
}
// ..생략..
var c = sum(1, 2);
console.log(c);
// ..생략..
함수 선언문 대신 함수 표현식으로 함수를 선언한다면 함수 선언 이전에 함수를 호출하는 부분에서 에러가 검출 되므로 더 빠른 타이밍에 디버깅 가능
원활한 협업을 위해서는 전역공간에 함수를 선언하거나 동명의 함수를 중복 선언하는 경우는 없어야만 한다.
만약, 동명의 함수가 있더라도 모든 함수가 함수 표현식 으로 정의되어 있다면 함수 선언문으로 선언되었을 때와 비교적 빨리 디버깅이 가능
스코프, 스코프체인, outerEnviromentReference
스코프(Scope) : 식별자에 대한 유효범위
자바스크립트 ES5 까지는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 형성
ES6 부터는 블록 에서도 스코프 경계가 발생하게 함으로써 다른 언어와 비슷한 스코프 형성이 가능
단, var 변수가 아닌 let, const, class, strict mode 에서의 함수 선언 등에 대해서만 범위로서의 역할을 수행
ES6 에서는 둘을 구분하기 위해 함수 스코프(var), 블록 스코프(let, const ...) 라는 용어를 사용
스코프 체인(Scope Chanining) : 이러한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것
이것을 가능케 하는 것이 바로 LexicalEnviroment 의 두 번째 수집 자료인 outerEnviromentReference
outerEnviromentReference
현재 호출된 함수가 선언될 당시의 LexicalEnviroment 를 참조
“선언하다." 라는 행위가 실제로 일어날 수 있는 시점 = 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때
- 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문
outerEnviromentReference 는 연결리스트(Linked List) 형태를 띈다.
- 선언 시점의 LexicalEnviroment 를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnviroment 가 있을 것이다.
또한, 각 outerEnviromentReference 는 오직 자신이 선언된 시점의 LexicalEnviroment 만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능
- 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능
변수 은닉화(variable shadowing) : 내부 함수에 선언된 식별자와 내부 함수 외에 선언된 식별자(like. 전역변수) 이름이 같을 때, 스코프 체인 상 가장 가까운 위치 상의 식별자로 접근하므로, 외부에 존재하는 식별자에는 접근할 수 없는 셈인 것
전역변수와 지역변수
- 전역변수(global variable) = 전역 공간에서 선언한 변수는 전역 변수
- 지역변수(local variable) = 함수 내부에서 선언한 변수는 무조건 지역변수
정리
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
- 실행 컨텍스트 객체는 활성화되는 시점에 VariableEnviroment, LexicalEnviroment, ThisBinding 의 세 가지 정보를 수집
실행 컨텍스트를 생성할 때는 VariableEnviroment 과 LexicalEnviroment 가 동일한 내용으로 구성되지만 LexicalEnviroment 는 함수 실행 도중에 변경되는 사항이 즉시 반영되는 반면 VariableEnviroment 는 초기 상태를 유지
VariableEnviroment 와 LexicalEnviroment 는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 enviromentRecord 와 바로 직전 컨텍스트의 LexicalEnviroment 정보를 참조하는 outerEnviromentReference 로 구성
호이스팅(Hosting)은 코드 해석을 좀 더 수월하게 하기 위해 enviromentRecord 의 수집 과정을 추상화한 개념
- 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 “끌어올린다” 고 해석
- 변수 선언과 값 할당이 동시에 이뤄지는 문장은 선언부만 호이스팅 하고 할당 과정은 원래 자리에 남아있게 된다.
- 여기서 함수 선언문과 함수 표현식의 차이가 발생
스코프(Scope)는 변수의 유효범위를 의미한다.
- outerEnviromentReference 는 해당 함수가 선언된 위치의 LexicalEnviroment 를 참조
- 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnviroment 를 탐색해서 발견되면 그 값을 반환
- 발견하지 못하면 다시 outerEnviromentReference 에 담긴 LexicalEnviroment 를 탐색하는 과정을 반복
- 전역 컨텍스트의 LexicalEnviroment 까지 탐색해도 해당 변수를 찾지 못하면 undefined 를 반환
전역 컨텍스트의 LexicalEnviroment 에 담긴 변수를 전역변수, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들을 모두 지역변수
- 안전한 코드 구성을 위해 가급적 전역변수 사용은 지양하고 지역변수를 사용하는 것이 좋다.