Lexical environment는 주로 "어휘적 환경"이나 "정적 환경"으로 번역되지만, 이러한 표현들은 개념을 충분히 반영하지 못합니다. 대신 "사전적 환경"이라는 번역이 제안되었으며 이는 컨텍스트 내부의 환경 정보를 사전처럼 구성한 것을 의미합니다. 그러나 타인과의 소통을 위해서는 번역어보다 원어를 그대로 사용하는 것이 더 적합하며 variable environment역시 같은 이유로 원어를 사용하는 편이 권장됩니다.
Environment Record에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.
함수 매개변수, 선언된 함수, var로 선언된 변수의 식별자가 포함됩니다.
실행 컨텍스트 내부를 처음부터 끝까지 순서대로 훑으며 식별자 정보를 수집합니다.
변수 정보를 모두 수집한 후에도 코드는 아직 실행되지 않은 상태입니다.
실행 전에도 자바스크립트 엔진은 해당 환경의 모든 변수명을 알고 있습니다.
이를 바탕으로 호이스팅(Hoisting) 개념이 등장합니다.
호이스팅은 변수 정보를 "최상단으로 끌어올린다" 고 가정하는 가상의 개념입니다.
실제로 엔진이 끌어올리지는 않지만 이해를 돕기 위해 그렇게 간주할 수 있습니다.
전역 실행 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는
객체, 즉 전역객체(global object)를 활용합니다. 전역 객체에는 브라우저의 window, Node.js
의 global 객체 등이 있습니다. 이들은 자바스크립트 내장 객체(native object)로 분류됩니다.
매개변수와 변수에 대한 호이스팅(1)-원본 코드
function a (x) { // 수집 대상 1(매개변수)
console.log(x); // (1)
var x; // 수집 대상 2(변수 선언)
console.log(x); // (2)
var x = 2; // 수집 대상 3(변수 선언)
console.log(x); // (3)
}
a(1)
자바스크립트에서는 변수 선언(var)이 코드의 최상단으로 "끌어올려진다"고 가정합니다.
function a(x) {
var x; // (호이스팅: 변수 선언이 최상단으로 이동)
console.log(x); // (1)
console.log(x); // (2)
x = 2; // 값 할당
console.log(x); // (3)
}
호출: a(1)
함수 a가 호출될 때 매개변수 x는 값 1로 초기화됩니다.
(1): console.log(x)
x는 함수의 매개변수로 1로 초기화되어 있으므로 1이 출력됩니다.
var x;
var x는 이미 매개변수로 선언된 x와 동일한 스코프에 있으므로 무시됩니다. (중복 선언 무시)
(2): console.log(x)
변수의 값은 여전히 1이므로 1이 출력됩니다.
x = 2;
변수 x에 값 2가 할당됩니다.
(3): console.log(x)
x의 값이 2로 변경되었으므로 2가 출력됩니다.
함수 선언의 호이스팅(1) - 원본코드
funtion a () {
console.log(b); // (1)
var b = 'bbb'; // 수집 대상 1(변수 선언)
console.log(b); // (2)
function b () {} // 수집 대상 2(함수 선언)
console.log(b); // (3)
}
a();
자바스크립트는 변수 선언(var)과 함수 선언을 모두 스코프의 최상단으로 끌어올립니다.
function a() {
var b; // 변수 선언(호이스팅)
function b() {} // 함수 선언(호이스팅, 우선순위 높음)
console.log(b); // (1)
b = 'bbb'; // 변수에 값 할당
console.log(b); // (2)
console.log(b); // (3)
}
함수 선언과 변수 선언의 호이스팅
함수 b가 우선적으로 식별자 b에 할당됩니다.
변수 b는 선언만 되고 값이 할당되지 않은 상태로 남아있습니다.
(1): console.log(b)
변수 b는 함수 선언에 의해 함수 b를 참조하고 있습니다.
따라서, b는 함수 자체를 출력합니다: function b() {}.
b = 'bbb';
변수 b에 값 'bbb'가 할당됩니다.
이제 b는 문자열 'bbb'를 참조합니다.
(2): console.log(b)
변수 b의 값이 'bbb'로 변경되었으므로, bbb가 출력됩니다.
(3): console.log(b)
변수 b는 여전히 문자열 'bbb'를 참조하므로, bbb가 출력됩니다.
함수 선언문(Function Declaration) 과 함수 표현식(Function Expression) 은 함수를 정의하는 두 가지 방식입니다. 함수 선언문은 function 정의부만 존재하며 별도의 할당 명령이 없고, 반드시 함수명이 정의되어야 합니다. 반면, 함수 표현식은 정의한 function을 변수에 할당하는 방식으로, 함수명을 정의할 수도 있고 생략할 수도 있습니다. 함수명을 정의한 경우 기명 함수 표현식이라 부르며, 생략한 경우 익명 함수 표현식이라고 합니다. 일반적으로 함수 표현식은 익명 함수 표현식을 의미합니다.
함수를 정의하는 세 가지 방식
function a () {/* ... */} // 함수 선언문. 함수명 a가 곧 변수명.
a(); // 실행 ok.
var b = function () {/* ... */} // (익명) 함수 표현식. 변수명 b가 곧 함수명.
b(); // 실행 ok.
var c = function d () {/* ... */} // 기명 함수 표현식. 변수명은 c, 함수명은 d.
c(); // 실행 ok.
d(); // 에러!
기명 함수 표현식은 주의할 점이 하나 있습니다. 바로 외부에서는 함수명으로 함수를 호출할 수 없다는 점입니다. 함수명은 오직 함수 내부에서만 접근할 수 있습니다. 그렇다면 기명 함수 표현식에서 함수명은 어떤 용도로 쓰일까요? 과거에는 기명 함수 표현식은 함수명이 잘 출력됐던 반면 익명 함수 표현식은 undefined 또는 unnamed라는 값이 나왔었습니다. 이 때문에 기명 함수 표현식이 디버깅 시 어떤 함수인지를 추적하기에 익명 함수 표현식보다 유리한 측면이 있었습니다. 그러나 이제는 모든 브라우저들이 익명 함수 표현식의 변수명을 함수의 name 프로퍼티에 할당하고 있습니다.한편 с 함수 내부에서는 c()로 호출하든 d()로 호출하든 잘 실행됩니다. 따라서 함수 내부에서 재귀함수를 호출하는 용도로 함수명을 쓸 수 있습니다. 다만 c()로 호출해도 되는 상황에서 굳이 d()로 호출해야 할 필요가 있을지는 의문입니다.
함수 선언문과 함수 표현식(1) - 원본 코드
console.log(sum(1, 2));
console.log(multiply(3, 4));
function sum (a, b) { //함수 선언문 sum
return a + b;
}
var multiply = function (a, b) { //함수 표현식 multiply
return a * b;
}
console.log(sum(1, 2));
sum(1, 2)는 먼저 호출됩니다. 이 호출은 a + b를 반환하는 sum 함수를 실행하려는 것입니다.
console.log(multiply(3, 4));
multiply(3, 4)는 multiply 함수가 호출된 것입니다. 그러나 multiply 함수는 함수 표현식으로 선언되었기 때문에, 코드가 해석되는 시점에서 multiply 함수는 아직 정의되지 않았습니다. 그 결과, multiply 함수 호출은 에러를 발생시킵니다.
function sum(a, b) {
return a + b;
}
호이스팅: 함수 선언문은 자바스크립트에서 "호이스팅"에 의해 코드의 최상단으로 끌어올려집니다. 즉, sum 함수는 코드가 실행되기 전에 이미 정의되어 있어, console.log(sum(1, 2)); 호출이 정상적으로 실행됩니다.
var multiply = function(a, b) {
return a * b;
};
호이스팅되지 않음: 함수 표현식은 변수에 함수가 할당되는 방식이므로, multiply 변수에 함수가 할당되기 전에 multiply를 호출하면 undefined로 평가되어 에러가 발생합니다. 함수 표현식은 정의된 이후에만 호출할 수 있습니다.
스코프(Scope) 는 식별자의 유효범위를 의미합니다. 경계 A의 외부에서 선언한 변수는 A 내부와 외부에서 모두 접근할 수 있지만 A 내부에서 선언한 변수는 A 내부에서만 접근 가능합니다. 이러한 스코프의 개념은 대부분의 언어에 존재하며 자바스크립트도 예외는 아니지만 ES5까지의 자바스크립트는 특이하게도 함수만이 스코프를 생성합니다. 결국, 스코프 체인(Scope Chain) 은 식별자의 유효범위를 안에서 바깥으로 차례로 검색해 나가는 과정을 의미하며 이를 가능하게 하는 것은 Lexical Environment의 두 번째 수집 자료인 outerEnvironmentReference입니다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 Lexical Environment를 참조합니다.
"선언 시점"에 주목해야 하며 함수 선언은 실행 컨텍스트가 활성화된 상태에서만 가능하므로 함수가 선언되는 시점은 해당 실행 컨텍스트가 활성화될 때입니다.
예를 들어, 함수 A 내부에서 함수 B를 선언하고 함수 B 내부에서 또 다른 함수를 선언하면 함수 B의 outerEnvironmentReference는 함수 A의 Lexical Environment를 참조합니다.
outerEnvironmentReference는 연결리스트 형태로 구성되며 "선언 시점의 Lexical Environment"를 계속 찾아 올라가면 전역 컨텍스트의 Lexical Environment에 도달합니다.
각 outerEnvironmentReference는 자신이 선언된 시점의 Lexical Environment만 참조하므로 스코프 체인에서는 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근할 수 없습니다.
var a = 1;
var outer = function () {
var inner = function () {
console.log(a);
var a = 3;
};
inner();
console.log(a);
};
outer();
console.log(a);
전역 컨텍스트:
var a = 1;
전역에서 a라는 변수가 선언되고 값 1이 할당됩니다.
outer 함수 호출:
outer();
outer() 함수가 호출됩니다.
outer 함수 실행:
var outer = function () {
var inner = function () {
console.log(a);
var a = 3;
};
inner();
console.log(a);
};
outer 함수가 실행되면 내부에 inner 함수가 정의됩니다.
그 후 inner() 함수가 호출됩니다.
inner 함수 실행:
var a = 3;
console.log(a);
inner 함수에서 console.log(a)를 호출하기 전에, var a = 3이 선언됩니다. 호이스팅에 의해 a는 undefined로 초기화되지만, 할당은 나중에 이루어집니다.
따라서 console.log(a)는 undefined를 출력합니다 왜냐하면 inner 함수의 스코프 내에서 a가 존재하고 그 값이 아직 할당되지 않았기 때문입니다.
inner 함수 종료 후 outer 함수 실행:
inner() 함수가 실행된 후 outer 함수 내에서 console.log(a)가 호출됩니다.
이때 outer 함수의 스코프 내에서 a는 전역 변수 a를 참조하고, 값은 1입니다. 따라서 console.log(a)는 1을 출력합니다.
outer 함수 종료 후 전역 실행:
outer() 함수가 종료된 후, 전역에서 console.log(a)가 호출됩니다.
전역에서의 a 값은 여전히 1입니다. 따라서 마지막 console.log(a)도 1을 출력합니다.
스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능한 것은 아닙니다.
예를 들어 a 식별자가 전역 공간과 inner 함수 내부에서 모두 선언된 경우 inner 함수 내부에서 a에 접근하려 하면 스코프 체인에서 첫 번째로 검색되는 inner 스코프의 Lexical Environment에서 a를 찾게 됩니다.
inner 함수에서 a를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없습니다.
이는 변수 은닉화(variable shadowing) 라고 하며 내부 스코프에서 외부의 동일한 이름을 덮어쓰는 현상을 의미합니다.