자바스크립트는 싱글쓰레드 기반 언어이다. 하나의 콜 스택을 갖고 있고 하나의 프로그램은 동시에 하나의 코드만 실행할 수 있다. 이 기반이 되는 중요한 개념이 실행 컨텍스트이다.
실행 가능한 코드를 형상화하고 구분하는 추상적인 개념. 즉, 실행 컨텍스트는 실행 가능한 코드가 실행되기 위해 필요한 환경 이라고 말할 수 있다. 여기서 실행 가능한 코드는 아래 3가지가 있다.
eval()
로 실행되는 코드자바스크립트 엔진은 코드를 실행하기 위해 여러가지 정보가 필요하다. 실행에 필요한 정보는 아래 4가지가 있다.
이와같이 실행에 필요한 자료를 형상화하고 구분하기 위해 자바스크립트 엔진은 실행 컨텍스트를 물리적 객체의 형태로 관리한다. ( 3가지 프로퍼티를 소유. 밑에서 설명 )
var x = 'xxx';
function foo(){
var y = 'yyy';
function bar(){
var z = 'zzz';
console.log(x+y+z);
}
bar();
}
foo();
위 코드를 실행하면, 실행 컨텍스트 스택이 생성하고 소멸한다.
현재 실해중인 컨텍스트에서 이 컨텍스트와 관련없는 코드(예를들면 다른 함수)가 실행되면 새로운 컨텍스트가 생성된다. 이 컨텍스트는 스택에 쌓이게 되고 컨트롤(제어권)이 이동한다.
다음은 스택이 생성하고 소멸하는 과정이다.
global EC
)가 생성되고 실행 컨텍스트 스택에 쌓인다. 전역 실행 컨텍스트는 어플리케이션의 종료까지 유지된다.글로벌의 경우 실행 이전에 생성되지만 함수의 경우 호출할 때 생성된다.
실행 컨텍스트가 생성 => 자바스크립트 엔진은 실행에 필요한 여러 정보들을 담을 객체를 생성한다. 이를 Variable Object (VO) 라고 한다. 여기에는 아래의 정보가 담겨있다.
Variable Object
는 실행 컨텍스트의 프로퍼티이기 때문에 값을 갖는데 이 값은 다른 객체를 가리킨다. 여기서 전역 코드 실행시 실행되는 전역 컨텍스트 객체와 함수를 실행할 때 생성되는 함수 컨텍스트의 경우, 가리키는 객체가 다르다. ( 전역 코드와 함수의 내용이 다르기 때문에 )
Variable Object
가 가리키는 객체는 아래와 같다.
Variable Object
는 최상위에 위치하고 모든 전역변수, 전역 함수 등을 포함하는 전역 객체(Global Object/ GO) 를 카리킨다. 전역 객체는 전역에 선언된 전역 변수와 전역 함수를 프로퍼티로 소유한다.
Variable Object
는 Activation Object / AO 활성객체 를 가리키며 매개변수와 인수들의 정보를 배열의 형태로 담고 있는 객체인 argument object
가 추가된다
Scope Chain
은 일종의 리스트로서, 해당 전역 또는 함수가 참조할 수 있는 변수, 함수 선언 등의 정보를 담고있는 전역 객체 (GO) 또는 활성 객체(AO)의 리스트를 가리킨다.
현재 실행 컨텍스트의 활성 객체를 선두로 하여 순차적으로 상위 컨텍스트의 활성 객체를 가리키며 마지막으로 전역 객체를 가리킨다.
스코프 체인은 변수를 검색하는 메커니즘이다. 객체의 프로퍼티나 메소드를 검색하는 메커니즘은 프로토타입 체인이다.
스코프 체인을 통해 하위함수에서 상위함수의 스코프까지 참조가 가능하다. (함수가 중첩되어 있으면 부모함수의 Scope가 자식함수의 스코프 체인에 포함되기 때문이다.) 함수 실행 중 변수를 만나면 그 변수를 우선 현재 Scope, 즉 AO에서 검색해보고, 만약 검색에 실패하면 스코프 체인에 담겨진 순서대로 검색을 이어간다. (이것이 스코프 체인이라고 불리는 이유이다.)
스코프 체인은 함수의 프로퍼티인 [[Scope]]
로 참조할 수 있다.
this
프로퍼티는 this 값에 할당된다. 이는 함수 호출 패턴에 의해 결정된다.
var x = 'xxx';
function foo(){
var y = 'yyy';
function bar(){
var z = 'zzz';
console.log(x+y+z);
}
bar();
}
foo();
컨트롤이 실행 컨텍스트에 진입하기 이전에 유일한 전역 객체가 생성된다. 전역 객체는 다음과 같은 특징이 있다.
전역 객체가 생성된 이후, 전역 코드로 컨트롤이 진입하면 전역 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 쌓인다.
이후 실행 컨텍스트를 바탕으로 다음의 과정이 실행된다.
실행 컨텍스트 생성된 이후 가장 먼저 스코프 체인의 생성과 초기화가 일어난다. 이때 스코프 체인은 전역 객체의 레퍼런스를 포함하는 리스트가 된다.
Variable Instantiation
은 실행 컨텍스트 객체 프로퍼티 중 하나인 VO
에 프로퍼티와 값을 추가하는 것을 의미한다. 이 과정에서 변수, 매개변수와 인수 정보, 함수 선언을 VO
에 추가하여 객체화 한다.
Variable Instantiation
은 다음과 같은 순서로 Variable Object
에 값을 설정한다. (1->2->3)순서 유지)
argumetns
객체 초기화 )undefined
가 값으로 설정된다. (변수 호이스팅)VO에 프로퍼티와 값을 설정하는 과정에서 호이스팅이 일어난다.
함수 선언은 Variable Instantiation
실행순서 2와 같이 함수명 foo가 VO(전역 코드인 경우 GO)의 프로퍼티로, 생성된 함수가 값으로 설정된다.
생성된 함수 객체는 [[Scrope]]
프로퍼티를 가지게 된다. 이는 함수 객체만이 소유하는 내부 프로퍼티로서 함수 객체가 실행되는 환경을 가리킨다. 따라서 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다.
내부 함수의 [[Scope]]
의 프로퍼티는 자신의 실행 환경과 자신을 포함하는 외부함수의 실행 환경과 전역객체를 가리키는데 이때 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scope]]
프로퍼티가 가리키는 외부함수의 실행 환경(AO)는 소멸하지 않고 참조할 수 있다. 이것이 클로저 이다
즉 클로저는 스택에서 외부함수의 실행 컨텍스트가 소멸했지만, 내부 함수의 스코프 체인에서 가리키는 외부함수의 활성 객체가 남아있는 상태라고 말할 수 있겠네
클로저 ? 클로저는 내부함수가 외부함수의 Context에 접근할 수 있는 것을 가리킨다.(생활코딩)
여기까지 살펴본 실행 컨텍스트는 아직 코드가 실행되기 이전이다. 하지만, 스코프 체인이 가리키는 VO에 이미 함수가 등록 되어 있으므로 이후 코드를 실행 할 때 함수 선언식 이전에 함수를 호출할 수 있게 되었다. (함수 호이스팅)
함수선언식의 경우 VO에 함수 표현식과 동일하게 함수명을 프로퍼티로 함수객체를 값으로 할당 하지만,,,
잠깐! 함수 선언식 vs 함수 표현식
// 함수 선언식 (일반적인 프로그래밍 언어 방법)
function hello(){}
// 함수 표현식 (JS의 특징)
var funcExpression = function(){}
함수 선언식은 VO에 변수와 함수객체를 즉시 할당하고,
함수 표현식은 일반 변수의 방식을 따른다. (즉시 할당하지 않고 코드가 실행되는 시점에 값이 할당) 이렇게 함수 선언식의 코드가 실행되기 이전 현재 실행 컨텍스트의 Variable Instatiation
과정에서 함수가 VO에 먼저 할당된다. 이를 함수 호이스팅 이라 한다.
// 함수 호이스팅
hello() // 'function hoisting'
function hello(){
console.log('function hoisting')
}
var funcExpression
funcExpression() // Reference Error ㅜㅜ
funcExpression = function(){
console.log('maybe occured Reference Error....')
}
변수 선언은 Variable Instantiation
순서3과 같이 변수명(x)가 VO의 프로퍼티로, undefined
가 값으로 설정된다. 이 작업은 아래와 같이 세분화된다.
VO에 변수를 등록한다. 이 변수 객체는 스코프가 참조할 수 있는 대상이 된다.
VO에 등록된 변수를 메모리에 할당한다. 이 단계에서 undefined
로 초기화된다.
undefined
로 초기화된 변수에 실제값을 할당한다.
var
키워드로 선언된 변수는 선언과 초기화가 한번에 이루어진다. 따라서 선언문 이전에 변수에 접근하여도 VO에 변수가 존재하기 때문에 에러가 발생하지 않는다. 단 undefined
를 반환한다. 이러한 현상을 변수 호이스팅 이라 한다.
호이스팅 (좀 더 자세하게 말하면)
호이스팅이란 “끌어올린다”라는 뜻으로 변수 및 함수 선언문이 스코프내 상단으로 끌어올려진다고 이해할 수 있지만, 정확히는 변수 및 함수가 컴파일 과정에서 컨텍스트에 저장되는 것 이다.
컴파일 과정에서, 함수의 경우 ( 정확하게 함수 선언식의 경우 )
Variable Object에 함수를 등록하는 과정을 거친 뒤 선언문 이전에 함수를 실행할 수 있다.
func() // error 없음 function func() {}
이유는 해당 스코프의 컨텍스트에 컴파일 과정에서 이미 Variable Object에 함수가 등록 되어있기 때문이다. 따라서 런타임 환경 때 상단에서 함수를 실행해도 호출이 가능하다.
하지만 함수 표현식의 경우 변수와 같은 방식으로 컴파일 과정에는 선언이 이루어진 뒤
undefined
로 초기화 된 후 런타임 과정에서 값의 할당을 받기 때문에 값의 할당이 이루어지기 전에는 함수를 호출할 수 없다.다시, 변수의 경우 컴파일 과정에서 Variable Object에 변수의 이름이 Property로
undefined
가 값으로 선언과 동시에 초기화가 된다. 따라서 변수에 할당을 해주는 코드 이전에 변수를 참조하면undefined
가 리턴된다.console.log(a) // undefined var a = 5
하지만,
let
과const
의 경우var
와 다르게 선언과 동시에undefined
로 초기화 되지 않는다. 따라서 초기화 구문을 만나기 전까지 참조하게 되면 Reference Error를 발생시킨다. 이 구간을 TDZ라고 한다.마지막으로!
let a
위 코드는
var
와 마찬가지로let a = undefined
와 같다.
변수 x는 ‘xxx’로 아직 초기화되지 않았고, 이후 변수 할당문에 도달하면 값의 할당이 이루어진다.
변수 호이스팅 샘플
// var는 선언과 동시에 undefined로 초기화 후 VO에 등록 -> 변수 호이스팅
console.log(a) // undefined
var a
// let은 선언과 동시에 초기화 x
console.log(b) // Reference error
let b
변수 선언처리가 끝나면 다음은 this value가 결정된다. this value가 결정되기 이전에 this는 전역 객체를 가리키고 있다가 함수 호출 패턴에 의해 this
에 할당되는 값이 결정된다. 전역 코드의 경우 this는 전역 객체를 가리킨다.
전역 컨텍스트(전역 코드)의 경우 VO, SC, this의 값은 항상 GO이다.
예제의 코드에서 변수 x에 문자열 ‘xxx’ 할당과 함수 foo의 호출이 실행된다.
전역 변수 x에 문자열 ‘xxx’를 할당할 때, 현재 실행 컨텍스트의 스코프 체인을 0번 인덱스 부터 가리키고 있는 VO를 검색하여 변수명에 해당하는 프로퍼티가 있는지 찾는다. 발견하면 ‘xxx’를 할당한다.
전역코드의 함수 foo가 실행되면 새로운 함수 실행 컨텍스트가 생성된다. 함수 foo의 실행 컨텍스트로 컨트롤이 이동하면 전역 코드의 경우와 마찬가지로
이 순차적으로 실행된다. 단, 전역 코드와 다르게 함수 코드가 실행되기 때문에 전역 코드의 룰이 아닌 함수코드의 룰이 적용된다.
함수 코드의 스코프 체인의 생성과 초기화는 Activation Object를 생성하고 이 객체를 스코프 체인의 선두에 설정하는 것으로 시작한다.
Activation Object는 우선 arguments 프로퍼티의 초기화를 실행한다. 그 후 Variable Instantiation
가 실행된다. ( AO에는 프로그램이 직접 접근할 수 없다. 하지만 프로퍼티에는 접근 가능하다.)
잠깐.. arguments
객체란 함수에 전달된 인수에 해당하는 Array
형태의 객체이다.
function hello(a, b, c){
console.log(arguments[0]) // 1
console.log(arguments[1]) // 2
console.log(arguments[2]) // 3
}
hello(1,2,3)
그 후, Caller ( 이 시점에서는 전역 컨텍스트 )
의 SC이 참조하고 있는 객체(GO)가 현재 실행 컨텍스트의 스코프 체인에 push
된다.
따라서 함수 foo를 실행한 직후, 현재 실행 컨텍스트의 스코프 체인은 AO-1 과 GO 를 순차적으로 참조한다.
스코프 체인의 생성과 초기화에서 생성된 AO를 VO로서 Variable Instantiation
이 실행된다. 이 과정을 제외하고는 전역 코드와 같은 처리가 실행된다. ( VO에 함수 설정 -> VO에 변수 설정 ) 따라서 VO에 프로퍼티 bar 값은 새로 생성된 Function Object 로 설정되고, [[Scopes]
]는 현재 실행 컨테스트의 SC 리스트를 가리키게 된다.
다음으로 변수 y를 프로퍼티로 추가하고 값은 undefined
로 초기화 한다. ( var
로 선언 했으니까 동시에 함)
변수 처리가 끝나면 다음은 this value가 결정된다. (처음에는 다 GO 가리킴) this에 할당되는 값은 함수 호출 패턴에 의해 결정된다. 내부 함수의 경우 this의 value는 전역 객체이다.
foo함수 내부에서는 변수 y에 문자열 ‘yyy’의 할당과 함수 bar가 실행된다.
지역변수 ‘y’에 문자열 ‘yyy’를 할당할 때, 현재 실행 컨텍스트의 스코프 체인을 0번 인덱스 부터 순회하여 참조하고 있는 VO에 변수 y가 있다면 값 ‘yyy’를 할당한다.
함수 bar가 실행되기 시작하면 새로운 실행 컨텍스트가 생성되고 컨트롤이 함수 bar의 실행 컨테스트로 넘어가면 함수 foo의 코드와 같이
마지막 단계에서 console.log(x + y + z)
결과는 아래와 같은 과정을 거친다.
스코프 체인 변수 검색