Javascript 실행 컨텍스트와 렉시컬 스코프, 스코프 체인
실행 컨텍스트란?
Javascript 실행 컨텍스트란 우리가 작성한 자바스크립트 코드가 실행되는 환경을 의미한다.
여기서 실행 컨텍스트를 보기전에 자바스크립트 동작 원리에 대해 간단하게 봐보자.
자바스크립트는 한번에 한 코드밖에 실행시키지 못한다. 그래서 싱글 쓰레드라고 부른다.
아래의 예시를 보자.
결과를 보기전 우리가 예상할 수 있는 결과는 2, 4, 6 이지만 실제 결과 값은 2, 6, 4 이다.
이유는 setTimeout, 비동기통신 등 바로 실행시킬수 없는 코드는 대기실로 보내어 실행될 수 있는
코드가 될 때까지 대기하고 바로 실행될 수 있는 코드는 "스택" 이라는 공간에서 코드를 실행 시킨다.
바로 실행될 수 없는 코드들이 실행될 수 있는 코드가 되면 큐라는 공간으로 들어와
스택 공간이 완전히 비워지면 큐에서 스택으로 옮겨져 코드가 실행된다.
이때 스택 공간은 완전히 비워져 있어야 한다.
여기서 LIPO(Last In, First Out) 구조인 스택이라는 공간이 실행 컨텍스트이다.
이러한 실행 컨텍스트는 두가지의 실행 컨텍스트가 존재한다.
전역 실행 컨텍스트(Global Execution Context) 와 함수 실행 컨텍스트(Functional Execution Context) 이다.
전역 실행 컨텍스트(Global Execution Context)
전역 실행 컨텍스트는 함수 내에 존재하지 않는 변수, 코드 등이 존재하는 공간이다.
전역 실행 컨텍스트는 무조건 하나의 실행 컨텍스트만 존재하며, 자바스크립트 엔진이 일부 자바스크립트 코드가
실행될 때마다 전역 실행 컨텍스트를 작성한다.
또한 전역 실행 컨텍스트는 어플리케이션이 종료되거나 브라우저가 닫히기 전까지 유지된다.
함수 실행 컨텍스트(Functional Execution Context)
함수 실행 컨텍스트는 함수가 실행될 때마다 새로운 실행 컨텍스트가 작성된다.
실행 컨텍스트는 LIPO(Last In, First Out) 구조인 스택이라는 방식으로 모든 실행 컨텍스트를 저장하고
실행하는데 사용된다.
아래는 실행 컨텍스트에 대한 예시이다.
var global = '전역변수';
function childF(){
var local = '지역변수‘;
console.log(global);
console.log('자식함수');
};
function parentF(){
childF();
console.log('부모 함수');
}
parentF();
1. 자바스크립트 코드를 실행하는 순간 ① 처럼 전역 실행 컨텍스트가 실행 컨텍스트인 콜 스택에 담긴다.
브라우저의 경우 window, node 환경의 경우 global 같은 객체를 사용할 수 있는 이유다.
2. 전역 실행 컨텍스트를 실행하면서 parentF() 함수를 실행하였으므로 parentF() 함수의 실행 컨텍스트를
생성하고 콜 스택에 담는다.
콜 스택 실행 컨텍스트의 최상단에 parentF() 함수 실행 컨텍스트가 존재하기 때문에 전역 실행 컨텍스트는
실행을 일시적으로 중단하고 parentF() 함수 실행 컨텍스트를 실행한다.
3. parentF() 함수 내부에서 childF() 함수를 호출했으므로 childF() 함수 실행 컨텍스트를 생성하여
실행을 위해 콜 스택에 담는다.
콜 스택 실행 컨텍스트 최상단에 childF() 함수 실행 컨텍스트가 존재하므로 parentF() 함수 실행 컨텍스트
실행을 중단하고 childF() 함수 실행 컨텍스트를 실행한다.
4. childF() 함수가 종료된 후 childF() 함수 실행 컨텍스트는 콜 스택에서 제거된다.
childF() 함수 실행 컨텍스트가 제거된 후 콜 스택 최상단은 parentF() 함수 실행 컨텍스트이므로
중단됐던 코드 지점부터 재개된다.
5. parentF() 함수가 종료된 후 콜 스택에서 제거되고 전역 공간에 실행할 코드가 남아있지 않다면
콜 스택에서 전역 실행 컨텍스트 또한 제거된다.
렉시컬 스코프(Lexical Scope)
렉시컬 스코프란 함수를 어디서 호출했는지가 아니라 어디에 선언하였는지에 따라 결정되는걸 의미한다.
즉, 함수를 어디에 선언하였는지에 따라 상위 스코프를 결정한다는 뜻이며, 가장 중요한점은
위에서 언급한대로 함수의 호출 위치가 아닌 함수의 선언 위치이다.
렉시컬 스코프는 정적 스코프(Static Scope) 라고 부르기도 한다.
아래의 예시 코드를 보자
var lexical_value = 1;
function parentF(){
var lexical_value = 10;
childF();
}
function childF(){
console.log(lexical_value);
}
parentF();
childF();
- lexical_value 라는 전역 변수가 존재한다.
- parentF() 함수에도 lexical_value 지역 변수가 존재한다.
- parentF() 함수 내부에 childF() 함수를 호출하는 코드 존재한다.
- childF() 함수는 lexical_value 를 호출하는 코드가 존재한다.
눈으로 봤을때 예상하는 결과는 parentF() 함수 내부에 존재하는 lexical_value 가 10이므로
parentF() 함수의 결과는 10, childF() 함수는 전역변수인 lexical_value 에 의해 결과가 1 일줄 알았다.
하지만 실제 결과는 아래와 같이 1 두개가 나왔다.
이유는 자바스크립트는 렉시컬 스코프를 따르므로 함수의 호출 위치가 아닌 선언 위치에 따라
상위 스코프가 결정된다.
즉, 함수를 어디서 호출했는지가 중요한게 아니라 호출된 함수의 선언 위치에 따라 상위 스코프가 결정된다.
그렇기 때문에 childF 함수가 parentF() 에서 호출된 것과 상관없이 전역에 선언된 함수 위치에 따라
전역 변수인 lexical_value = 1 을 참조하므로 변수 1 두개가 출력된 것이다.
* 스코프(Scope)
모든 식별자(변수, 함수, 클래스 이름)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을
참조할 수 있는 유효 범위가 결정된다. 즉, 스코프는 식별자가 유효한 범위를 뜻한다.
제가 쉽게 이해할 수 있는 문장은 "다른 식별자들이 특정 식별자에 접근할 수 있는 범위" 를 뜻한다.
스코프 체인(Scope Chain)
스코프 체인은 전역 객체와 중첩된 여러가지의 함수의 스코프의 레퍼런스를 차례로 저장하고,
각각의 스코프가 어떻게 연결(Chain)되어 있는지 보여준다.
아래의 예시를 보자.
var lexical_value = 1;
function parentF(){
var lexical_value = 10;
function childF(){
console.log(lexical_value);
}
childF();
}
parentF();
1. 자바스크립트를 실행하게 되면 전역 변수 및 함수 등 전역 실행 컨텍스트를 생성하여 콜 스택 실행 컨텍스트에 담긴다.
2. 전역 실행 컨텍스트를 실행하는 과정에 parentF() 함수가 호출된다.
3. parentF() 함수가 호출되면 parentF() 함수 실행 컨텍스트가 생성되고 콜 스택에 담겨 실행된다.
4. parentF() 함수를 실행하는 과정에서 내부 함수인 childF() 함수를 호출한다.
5. childF() 함수 실행 컨텍스트를 생성하여 콜 스택에 담겨 childF() 함수가 실행된다.
childF() 함수는 lexical_value 를 childF() 함수에서 탐색하여 찾지만 변수가 존재하지 않아 childF()의
상위 스코프인 parentF() 함수를 탐색한다. 이때 parentF() 함수에 변수 lexical_value 가 존재하면
parentF() 함수의 lexical_value 를 참조하게 되고, 만약 없다면 마지막으로 전역 객체를 탐색한다.
상위 스코프인 parentF() 함수에서 lexical_value 값이 존재하기 때문에 지역 변수인 lexical_value = 10 값이
출력된다.
만약 parentF() 함수에 lexical_value 변수가 존재하지 않으면 전역 객체에 있는 lexical_value = 1 인
전역 변수가 출력된다.
이러한 과정들처럼 스코프에 담긴 순서대로 탐색하는 걸 스코프 체인이라고 한다.