JavaScript - 자바스크립트의 실행 컨텍스트, 스코프, 스코프 체인
🚀 자바스크립트의 실행 컨텍스트(Execution Context)
자바스크립트에서 말하는 실행 컨텍스트란, 실행 가능한 코드가 실행되는 환경, 실행되기 위해 필요한 환경으로 이해할 수 있습니다.
C언어에서는 함수 호출 때마다 기존 함수의 호출 정보 위에 스택처럼 쌓이는데, 자바스크립트도 이처럼 실행중인 코드가 실행 컨텍스트에 스택처럼 쌓이면서 실행 환경을 가지게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
console.log("전역 컨텍스트입니다.");
function func1(){
console.log("func1 입니다.");
}
function func2(){
func1();
console.log("func2 입니다.");
}
func2();
- 결과
1
2
3
전역 컨텍스트입니다.
fun1 입니다.
fun2 입니다.
- 실행 컨텍스트 스택
위와 같이 실행 컨텍스트 스택에 컨텍스트들이 차곡 차곡 쌓이는 모습이 됩니다. 함수 종료 시 직전의(호출된) 실행 컨텍스트에 반환됩니다.
🚀 실행 컨텍스트 - 변수 객체, 스코프 체인, this
실행 컨텍스트는 물리적으로는 생성할 때 변수 객체, 스코프 체인, this 프로퍼티 등의 속성을 가지게 됩니다.
변수 객체
실행 컨텍스트가 생성될 때는, 자바스크립트 엔진이 해당 컨텍스트에 필요한 정보들(객체에서 사용할 매개변수, 사용자가 정의한 변수, 객체)을 저장할
변수 객체
를 만듭니다.아까 실행 컨텍스트는 현재 실행 가능한 코드가 실행되는 환경이라고 했었습니다. 즉, 그 코드들이 실행될 때 필요한 각종 정보들을 담아 두는 곳이
변수 객체
라고 생각하시면 되겠습니다.먼저, 변수 객체의
argument
프로퍼티는 함수 호출시 암묵적으로 전달된arguments
객체를 참조합니다.arguments
객체는 함수 호출 시 넘긴 인자들이 배열로 저장된 객체입니다.
arguments 객체 : 함수 호출 시 암묵적으로 내부에 생성되는 객체. 실제로 함수 윗부분에서 console.dir(arguments); 를 작성해 보면, 넘겨진 인자들에 대한 정보와 length 등을 확인할 수 있다. 여기서 arguments 객체는 일반 객체이지만 length 프로퍼티가 존재하는 유사 배열 객체 이다.
스코프 체인
변수 객체에는
[[scope]]
프로퍼티에 추가되는 연결리스트 형태의 스코프 정보가 만들어집니다. 이것을 스코프 체인이라고 합니다. 스코프는 현재 실행 중인 컨텍스트의 유효한 범위를 나타내는 것입니다.현재 실행 컨텍스트 내부에서 사용하는 지역 변수 생성
함수 안에 정의된 변수들이나, 내부 함수들이 변수 객체에 생성됩니다. 그런데 이 때는 변수나 함수 표현식으로 만들어진 함수들은 생성만 되고 초기화는 이루어지지 않습니다. 각각의 것들이 실행되기 전까지는 초기화가 이루어지 않습니다. 그래서 아직 이들의 값은
undefined
인 채로 변수 객체에 생성됩니다.this 프로퍼티에 this 가리키는 객체 저장하기
이 변수 객체의 this에 바인딩할 객체를 저장합니다.
정리해서 보도록 하겠습니다.
아래와 같은 코드를 작성했을 때, foo() 함수 실행 컨텍스트 내의 변수 객체는 어떻게 생성될까요?
1
2
3
4
5
6
7
8
9
function foo(p1, p2, p3){
var a;
function bar(){
return a*a;
}
return p1 + p2 + p3 + a + bar();
}
foo(3, 4, 5);
이처럼 foo() 실행 컨텍스트의 변수 객체에는 arguments 부터 시작하여 스코프 체인, 함수 인자에 대한 값, 지역 변수, 내부 함수, this 바인딩까지 모든 정보들이 들어가게 됩니다. 여기서 foo() 함수의 this는 전역 객체에 바인딩됩니다. 함수 호출 방식에 따라 달라지는 this 바인딩의 경우를 제외하면 자바스크립트의 this는 기본적으로 전역 객체에 바인딩이 됩니다.
이제 변수 객체가 만들어진 이후, 코드 내부의 여러 표현식이 실행되고, 이 때 변수의 초기화나 내부 함수의 실행, 연산 등이 이루어집니다. 이 때 undefined로 되어 있던 지역변수 a도 값이 초기화가 됩니다.
🚀 스코프 체인(Scope chain)
스코프 체인에는 현재 컨텍스트가 참조 가능한 변수 객체들이 연결리스트 형식으로 담겨 있습니다. 그러나 아무렇게나 담기는 것이 아닙니다. 현재 실행 컨텍스트의 변수 객체와, 상위 컨텍스트의 스코프 체인(리스트)이 합쳐져서 해당 실행 컨텍스트의 스코프 체인이 됩니다.
맨 처음 실행 컨텍스트에 대해 설명할 때, 그림에서 함수가 실행되는 순간 각각의 실행 컨텍스트들이 만들어지는 모습을 볼 수 있었습니다. 새롭게 만들어지는 실행 컨텍스트는 자신을 실행한 함수의 [[scope]]
프로퍼티에 더하여 새로운 스코프 체인을 만듭니다.
아래와 같은 코드가 있다고 가정해 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
var a = 1;
var b = 2;
function foo(){
var a = 10;
var b = 20;
console.log(a + b); // 30
}
foo();
console.log(a + b); // 3
각각의 변수 a, b 더하기 값 결과가 달랐습니다. 그 이유가 무엇일까요?
위에서 말했던 실행 컨텍스트, 스코프 체인 등의 개념을 모두 생각해 보시면 될 것 같습니다.
처음에 최초 코드를 실행하면 전역 실행 컨텍스트
가 생성됩니다. 여기에는 (맨 바깥의) 변수 a, b, 그리고 함수 foo() 가 있었습니다. 맨 처음 실행 시에는 스코프 체인이 전역 변수 객체를 가집니다.
또한, foo() 함수를 foo(); 로 실행하기 전, foo() 함수 객체가 생성됩니다. 객체가 생성될 때 [[scope]]
값은 현재 실행되는 컨텍스트(지금은 전역 실행 컨텍스트)의 [[scope]]
값을 그대로 가지게 됩니다. 그러므로 foo 함수 객체의 [[scope]]
에는 전역 변수 객체가 담깁니다.
이후 foo() 함수를 실행했습니다. 그러면 foo() 실행 컨텍스트가 새로 만들어집니다. 그리고 새로 만들어진 실행 컨텍스트는 아래와 같은 방법으로 스코프 체인을 가집니다.
foo() 함수를 실행함으로써 새롭게 만들어진 foo 실행 컨텍스트는 자신의 스코프 체인을 다음과 같은 규칙으로 가지게 됩니다.
- 현재 실행중인 함수 객체(foo 함수 객체)의
[[scope]]
프로퍼티를 복사- 거기에는 전역 변수 객체가 담겨 있었습니다.
- 새롭게 생성된 foo 실행 컨텍스트의 변수 객체에 있는
[[scope]]
프로퍼티에 방금 복사한[[scope]]
값을 둔 다음, - 그 리스트 제일 앞에, 새롭게 생성된 변수 객체를 추가 합니다. 여기서 새롭게 생성된 변수 객체란 foo 함수를 실행함으로써 새롭게 만들어진 foo 실행 컨텍스트의 변수 객체가 될 것입니다.
이제 함수 바깥과 함수 안에 똑같은 이름의 변수 a, b가 있는데도 참조한 값이 다른 이유가 조금씩 느껴지실 것입니다.
함수에서 변수를 참조할 때, 이렇게 만들어진 스코프 체인
을 순차적으로 탐색하면서 검색하게 됩니다. 즉, foo 함수는 변수를 검색할 때 스코프 체인의 가장 앞에 있는 foo 변수 객체
를 먼저 살펴볼 것입니다. 따라서, foo 함수 내부에 있던 변수 a, b가 가장 먼저 참조될 수 있던 것입니다.
만약 foo 함수 내부에 변수 a, b가 없었다면 어떻게 될까요? 변수 a, b가 있는 변수 객체를 찾을 때까지, 스코프 체인에서의 다음 객체로 이동하면서 계속 찾아나갈 것입니다. 전역 객체까지 모두 찾았는데 없으면, undefined
를 반환하게 됩니다.
이처럼 스코프를 타고 가면서 찾아가는 방식이라 스코프 체인이라는 이름이 붙여졌다고 볼 수 있습니다.
이와 비슷한 예시로 프로토타입 체인(체이닝) 도 있습니다. 프로토타입 체이닝의 경우, 특정 객체의 프로퍼티나 메소드에 접근하려고 했는데 해당 객체에 없었으면 [[Prototype]]
링크(크롬 브라우저에서는 __proto__
)를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색해 나갑니다.
이런 코드는 변수 참조가 어떻게 될까요?
1
2
3
4
5
6
7
8
9
10
11
12
var value = "value1";
function foo(){
console.log(value);
}
function bar(){
var value = "value2";
console.log(value); // value2
foo(); // value1
}
bar();
먼저 최초 코드 실행 시 전역 실행 컨텍스트
가 만들어집니다.
그리고 foo(), bar() 함수 객체들도 차례로 객체 생성이 되는데, 이 때 foo, bar 함수 객체 각각의 [[scope]]
는 현재 실행되는 컨텍스트(지금은 전역 실행 컨텍스트)의 [[scope]]
값을 가진다고 했으므로, 전역 변수 객체를 가리킵니다.
이후 bar(); 를 통해 bar 함수를 실행했습니다.
그러면 bar 실행 컨텍스트
가 만들어 지면서 bar 함수가 실행될 텐데요,
우선 현재 실행 중인 함수 객체(bar 함수 객체)의 [[scope]]
프로퍼티를 복사(전역 변수 객체가 담겨 있음)합니다. 그리고 새롭게 생성된 bar 변수 객체를 bar 실행 컨텍스트 [[scope]]
프로퍼티의 가장 맨 앞에 추가합니다. 그러면 결과적으로 bar 실행 컨텍스트의 스코프 체인은 bar 변수 객체 -> 전역 변수 객체
가 됩니다.
이후에 foo() 함수가 호출되어 실행됩니다.
foo 실행 컨텍스트
가 만들어 지면서 foo 함수가 실행됩니다.
foo 실행 컨텍스트 변수 객체의 [[scope]]
는 우선 현재 실행 중인 함수 객체(foo 함수 객체)의 [[scope]]
프로퍼티를 복사합니다. 여기에도 초기에 전역 변수 객체가 담겨 있었습니다. 이후 새롭게 생성된 foo 변수 객체를 [[scope]]
프로퍼티의 가장 맨 앞에 추가합니다. 그러면 결과적으로 foo 실행 컨텍스트의 스코프 체인은 foo 변수 객체 -> 전역 변수 객체
가 됩니다.
이처럼 각 함수 객체가 처음 생성될 당시의 실행 컨텍스트가 무엇인지도 중요함을 알 수 있습니다.
본 포스팅은 인사이드 자바스크립트(송형주, 고현준 저)
의 내용을 참고하여 작성하였습니다.