[JS] 코어자바스크립트 - 실행 컨텍스트
코어 자바스크립트를 꼼꼼하게 정독하고
복습하며 정리한 내용을 토대로 F-lab 멘토링을 받고있다.
이 글은 멘토링으로 알게된 것들을 다시한번 정리해보는 글이다.
실행 컨텍스트 : 실행할 코드에 제공할 환경정보들을 모아놓은 객체
실행 컨텍스트란?
사전지식 - 스택과 큐
스택 Stack
출입구가 하나뿐인 깊은 우물같은 데이터 구조. Last in First out.
큐 Queue
양쪽이 모두 열려있는 파이프같은 데이터 구조. First in First out.
실행 컨텍스트
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
동일한 환경에 있는 코드를 실행할때 필요한 정보를 모아 컨텍스트를 구성하고
이것을 콜 스택에 쌓아 올려뒀다가, 가장 위에있는 컨텍스트 관련 코드부터 실행함으로서
전체 코드의 환경과 순서를 보장한다.
동일한 환경이란
동일한 환경이란 전역공간, eval()함수, 일반 함수 등이 있는데
전역 공간은 자바스크립트 엔진이 자동으로 생성하는 것이고, eval() 함수는 쓰지 않는 것이 좋으므로
일반적으로 함수를 실행하는 것을 통해 실행 컨텍스트를 구성하게 된다.
eval function is evil
콜스택에 실행컨텍스트가 쌓이는 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1; // 1. 전역컨텍스트가 콜스택에 담긴다.
function outer() {
// 3. 호출됨
function inner() {
// 5. 호출됨
console.log(a);
var a = 3;
}
inner(); // 4. inner 함수호출. inner실행컨텍스트를 콜스택에 담는다.
console.log(a);
}
outer(); // 2. outer 함수호출. outer실행컨텍스트를 콜스택에 담는다.
console.log(a);
자바스크립트 엔진은 실행해야할 코드와 관련된 환경정보를 수집해서
실행컨텍스트 객체에 저장하고, 그 컨텍스트는 콜스택에 쌓여 순서대로 처리된다.
실행컨텍스트에 담긴 것
실행컨텍스트 생성 시 담기는 내용들은 아래와 같다.
VariableEnvironment
컨텍스트 내 식별자들에 대한 정보, 외부 환경정보
LexicalEnvironment
처음에는 VariableEnvironment와 같지만 변경사항을 실시간 반영
ThisBinding
식별자가 바라봐야 할 대상 객체
실행컨텍스트가 수집하는 정보
VariableEnvironment
여기에 담기는 내용은 LexicalEnvironment와 동일하다.
실행 컨텍스트 생성 시 VariableEnvironment에 정보를 먼저 담고
이것을 그대로 복사해 LexicalEnvironment를 만든다. 이후에는 LexicalEnvironment를 주로 활용한다.
LexicalEnvironment
이것의 의미는 ‘사전적인 정보들’이라고 할 수 있다.
현재 컨텍스트 내부에는 어떤 식별자들이 있고,
그 외부 정보는 어떤 값을 참조하도록 구성되어있다와 같은 환경 정보들을 사전처럼 모아놓은 것이다.
VariableEnvironment
와 LexicalEnvironment
에는 각각
envitonmentRecord
와 outerEnvironmentReference
정보가 저장된다.
envitonmentRecord
여기에는 현재 컨텍스트와 관련된 식별자 정보들이 저장된다.
컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어 순서대로 정보를 수집한다.
이렇게 변수 정보가 먼저 수집되지만 코드는 실행되기 전인 셈이 된다.
자바스크립트 엔진은 코드 실행 전임에도 해당 환경에 속한 변수명들을 미리 알고있게 된다.
이런 개념을 호이스팅이라 부를수 있다.
호이스팅 hoisting
자바스크립트는 변수 정보를 수집 (호이스팅) 할때
각 식별자에 어떤 값이 할당될 것인지는 신경쓰지 않는다.
변수를 호이스팅할 때는 변수명만 먼저 끌어올리고, 할당 부분은 원래 자리에 남겨둔다.
변수를 선언해 식별자가 들어갈 메모리 공간만 확보하고 실제 데이터 주소까지는 연결하지 않는 것이다.
1
2
3
4
5
6
7
8
9
10
function a() {
// 함수 a 실행컨텍스트 생성. 변수 x 저장
var x = 1; // x의 값을 1로 할당
console.log(x); // 1출력
var x; // 이미 저장된 변수이므로 아무 동작 없음
console.log(x); // 저장되어있던 1 출력
var x = 2; // x의 값을 2로 대체
console.log(x); // 새로 저장된 2 출력
}
a();
1
2
3
4
5
6
7
8
9
10
11
function a() {
// 함수 a 실행컨텍스트 생성. 변수 b 저장
// 변수는 선언부만 끌어올리지만 함수는 함수 자체를 끌어올린다.
// 변수 b를 끌어올렸는데, 함수가 b에 다시 할당되었다.
console.log(b); // b 함수 출력
var b = "bbb"; // b에 'bbb'값을 재할당
console.log(b); // 'bbb' 출력
function b() {}
console.log(b); // 'bbb' 출력
}
a();
함수 선언문과 함수 표현식
함수 선언문과 함수 표현식의 구분
function a()
함수 선언문. 함수명 a가 곧 변수명.
var b = function()
익명함수 표현식. 변수명 b가 곧 함수명.
var c = function d()
기명함수 표현식. 변수명은 c, 함수명은 d.
c()로는 실행 가능하지만 d()로 실행하면 에러가 난다. d()는 c변수 안에서만 호출할 수 있다.
함수 선언문의 위험성
실행컨텍스트 생성 시 변수를 호이스팅하면서
함수 선언문은 함수 전체를 호이스팅하는 반면, 함수 표현식은 변수 선언부만 호이스팅한다.
1
2
3
4
5
6
7
8
9
10
11
12
// sum함수 통째로 호이스팅
// multiply는 변수명만 호이스팅 (할당아직 안된 상태)
console.log(sum(1, 2)); // sum함수는 통째로 호이스팅 되어있으므로 정상실행
console.log(multiply(3, 4)); // multiply함수는 내용이 선언되기 전이므로 에러
function sum(a, b) {
return a + b;
}
var multiply = function (a, b) {
return a * b;
};
이렇게되면 선언과 실행이 어떤 순서로 있어도 오류가 나지 않는다는 점에서 장점으로 보일 수 있지만
오히려 큰 혼란을 일으키는 원인이 되기도 한다.
1
2
3
4
5
6
7
8
9
10
11
12
// 앞에있는 sum함수 통째로 호이스팅
// but 뒤에있는 sum이 다시 호이스팅되어 덮어쓰임
console.log(sum(3, 4))
function sum(x, y) {
return x + y
}
function sum (x, y) {
return x + '+' + y '=' + (x + y)
}
var c = sum(1, 2)
console.log(c)
이렇게되면 모든 출력에서 결과값은 마지막에 선언된 함수의 실행 내용대로 나오게 된다.
오류없이 통과되는 것은 복잡한 코드에서 이런식으로 문제를 발생시킨다.
만약 모든 sum함수를 함수 표현식으로 정의했다면
두번째 sum함수 전까지는 첫번째 정의한 sum함수의 내용대로,
두번째 sum함수 이후에는 두번째 정의한 sum함수의 내용대로 출력되었을 것이고
함수 전에 결과를 도출하려는 코드 역시 에러를 발생시켜 빠르게 디버깅 할 수 있었을 것이다.
사실 전역공간에 함수를 선언하거나 동명의 함수를 중복 선언하는 경우는 없어야하지만
적어도 함수를 함수 표현식으로 정의하면 위와 같은 상황은 방지할 수 있다.
스코프 scope
스코프란
스코프란 식별자에 대한 유효 범위이다.
a의 외부에서 선언한 변수는 a 내부, 외부에서 모두 접근이 가능하지만
a의 내부에서 선언한 변수는 a 내부에서만 접근이 가능하다.
ES5전에는 함수에 의해서만 스코프가 생성되었다고 한다.
지금은 const, let, class 등을 통해 블록에 의해서도 스코프 생성이 가능해졌다.
스코프 체인
식별자의 유효범위를 안에서부터 바깥으로 차례대로 검색해나가는 것을 스코프체인이라고 한다.
그리고 이것을 가능하게 하는 것이 LexicalEnvironment의 두번째 수집 자료인
outerEnvironmentReference
이다.
outerEnvironmentReference는 현재 호출된 함수가 선언된 당시의 LexicalEnvironment를 참조한다.
outerEnvironmentReference는 자신이 선언된 시점의 LexicalEnvironment만 참조할 수 있으므로,
가장 가까운 요소부터 차례대로만 접근할 수 있다.
따라서 여러 스코프에서 같은 식별자를 선언한 경우, 스코프 체인 상에서 가장 먼저 발견된 식별자에 접근하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1; // 1. 전역공간 a, outer 호이스팅
var outer = function () {
// 3. outer 공간 inner 호이스팅
var inner = function () {
// 4. inner 공간 a 호이스팅
console.log(a); // 5. a발견 했으나 아직 값이 할당된 a가 없으므로 undefined
var a = 3; // 6. inner 공간 내에 있는 a에 3 값을 할당
};
inner(); // 4. inner 실행
console.log(a); // 7. 발견된 a가 없어 다음 공간의 a를 참조하여 1 출력
};
outer(); // 2. outer 실행
console.log(a); // 8. 자신이 있는 공간에 바로 a가 있어 1 출력
위 코드를 보면 inner 밖에있는 스코프에서는 inner안에 선언된 a값을 참조할 수 없다.
이것을 변수 은닉화라고 한다.
전역변수와 지역변수
전역변수
전역공간에서 선언한 변수
지역변수
함수 내부에서 선언한 변수
함수를 선언하면서 여러 스코프에 영향을 주지 않게 하여 코드의 안전성을 확보할 수 있도록
가급적 전역변수 사용을 최소화 하려는 노력이 필요하다.
Leave a comment