실행 컨텍스트(Execution Context)는 실행 가능한 코드가 실행되기 위해 필요한 환경을 뜻합니다
(실행할 코드에 제공할 환경정보를 모아놓은 객체)
실행 가능한 코드는 크게 3개의 종류가 있습니다.
- 전역 코드
- 함수 코드
- eval 코드(eval은 사용하지 않아야 합니다.)
일반적으로 실행이 가능한 코드는 전역코드와 함수코드 입니다.
자바스크립트 엔진은 코드를 실행하기 위하여 여러가지 정보를 알고 있어야 합니다.
- 변수: 전역변수, 지역변수, 매개변수, 객체의 프로퍼티
- 함수 선언
- 변수의 유효범위(scope)
- this
위의 정보들을 물리적 객체의 형태로서 자바스크립트 엔진에서 관리합니다.
이벤트 루프를 언급할때 javascript engine에는 heap과 call stack이 존재한다고 하였습니다.
heap은 메모리 할당이 일어나는 영역(변수, 객체 등등)
call stack은 코드의 실행에 따라 호출 스택이 쌓이는 영역
2022.03.31 - [Frontend/Javascript] - event loop
함수가 호출되면 call stack에 실행 컨텍스트가 하나씩 쌓이게 됩니다.
코드의 실행과 함께 콜스택에 실행 컨텍스트가 쌓이는 모습을 살펴보겠습니다.
var a = 'a';
console.log(a)
위의 코드를 실행하면 아래와 같이 콜스택에 올려져 실행되게 됩니다.
전역 범위에서 실행되었으므로 전역 실행컨텍스트 하나만 스택에 존재하는 것을 확인할 수 있습니다.
아래는 좀 더 복잡한 코드의 실행입니다.
var a = 'a'
function outerFunc() {
var b = 'b'
function innerFunc() {
console.log(a + b)
}
innerFunc()
}
outerFunc()
1. 전역 코드가 실행 (global EC 추가)
2. outerFunc가 실행됨(outerFunc EC 추가)
3. innerFunc가 실행됨(innerFunc EC 추가)
총 3개의 스택이 추가되는 것을 볼 수 있고
실행 후에는 해당 함수의 실행 컨텍스트를 파기하고 컨트롤을 다음 스택으로 반환하게 됩니다!
실행컨텍스트는 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념이지만
물리적으로는 객체의 형태를 가지며 아래의 3가지 프로퍼티를 가지고 있습니다.
Variable Object
위에서 언급한 여러가지 정보중 변수, 매개변수, 함수선언에 대한 정보가 담길 객체 입니다.
- 변수
- 매개변수, 인수정보
- 함수 선언
Variable Object는 실행 컨텍스트의 property이기 때문에 상황에 따라 다른 값을 가지게 됩니다.
하나의 예로 전역 컨텍스트에는 매개변수가 없지만 함수 컨텍스트에는 매개변수가 있습니다.
Scope Chain
프로그래밍에는 동적 스코프, 정적 스코프(lexical scope)가 존재합니다.
동적 스코프는 함수가 호출될때 스코프가 정해지는 것이고
정적 스코프는 함수가 선언된 위치를 기준으로 스코프가 정해지는 것입니다.
javascript에서는 정적 스코프를 체택하고 있습니다.(대부분의 언어에서는 정적 스코프를 체택하고 있습니다.)
선언된 위치를 기준으로 스코프를 가지게 된다는 것입니다.
(var는 함수 스코프, let, const는 블록 스코프를 가집니다. 이것 또한 정적 스코프 입니다.)
실행컨텍스트에서 Scope Chain은 자신이 선언된 위치를 기준으로 생성됩니다.
innerFunc은 outerFunc내부에서 선언되었습니다.
outerFunc는 전역 스코프에서 선언되었습니다.
그러므로 innerFunc의 스코프 체인은 [innerFunc, outerFunc, 전역컨텍스트]의 값을 가지게 됩니다.
이 스코프 체인은 변수의 값을 탐색할때 사용됩니다. 자세한 예시는 아래에서 언급하겠습니다.
위에서 등장한 전역컨텍스트, outerFunc, innerFunc의 실행컨텍스트를 살펴보겠습니다.
전역컨텍스트: {
vo: {
a: 'a',
outerFunc: function Object
},
sc: ['전역 변수객체'],
this: window
}
outerFunc: {
vo: {
b: 'b',
innerFunc: function Object
arguments: {}
},
sc: ['outerFunc 변수객체', '전역 변수객체'],
this: window
}
innerFunc: {
vo: {
arguments: {}
},
sc: ['innerFunc 변수객체', 'outerFunc 변수객체', '전역 변수객체'],
this: window
}
크롬의 개발자 도구에서는 Scope, CallStack을 확인할 수 있습니다.
중간에 debugger를 찍으면 아래의 사진과 같이 확인할 수 있습니다.
함수의 감춰진 프로퍼티 중 [[scope]]는 스코프 체인이 아니라 현재 참조하고 있는 스코프를 의미합니다.
function test1() {
function test2() {
function test3() {
var x = 1;
function test4() {
console.log("test4", x)
}
console.dir(test4)
test4();
}
console.dir(test3)
test3();
}
console.dir(test2)
test2();
}
console.dir(test1)
test1();
위의 예시를 보면 더 확실히 알 수 있습니다.
아래의 코드는 각 실행 컨텍스트를 구체화 시킨 것입니다.
전역컨텍스트: {
vo: {
test1: function Object
},
sc: ['전역 변수객체']
this: window
},
test1: {
vo: {
test2: function Object
},
sc: ['test1 변수객체','전역 변수객체']
this: window
}
test2: {
vo: {
test3: function Object
},
sc: ['test2 변수객체', 'test1 변수객체','전역 변수객체']
this: window
}
test3: {
vo: {
x: 1
test4: function Object
},
sc: ['test3 변수객체', 'test2 변수객체', 'test1 변수객체','전역 변수객체']
this: window
}
test4: {
vo: {},
sc: ['test4 변수객체', 'test3 변수객체', 'test2 변수객체', 'test1 변수객체','전역 변수객체']
this: window
}
개발자도구 console 에서 출력한 [[Scopes]]는 함수 실행컨텍스트가 가지는 스코프체인과 다릅니다.
[[Scopes]]는 스코프 체인이 아니라 현재 참조하고 있는 스코프를 의미하기 때문입니다.
test4가 실행되었을때 test4 내부에는 x 변수객체가 존재하지 않습니다.
그래서 스코프체인에 나와있는대로 다음 스코프인 test3의 변수 객체를 가져와서 출력해 줍니다.
이렇게 최상위 스코프 까지 가도 출력하고자 하는 변수가 없다면 ReferenceError를 출력해줍니다.
// 생략
function test3() {
var x = 1;
function test4() {
console.log("test4", x)
}
console.dir(test4)
test4();
}
console.dir(test3)
test3();
// 생략
// 실행 컨텍스트
test3: {
vo: {
x: 1
test4: function Object
},
sc: ['test3 변수객체', 'test2 변수객체', 'test1 변수객체','전역 변수객체']
this: window
}
test4: {
vo: {},
sc: ['test4 변수객체', 'test3 변수객체', 'test2 변수객체', 'test1 변수객체','전역 변수객체']
this: window
}
참조하는 값에 따라 달라진 [[Scopes]] 모습은 아래의 이미지 입니다.
var name = 'name'
function hi() {
var name = 'same'
console.log(name) // 'same'
sayWord('hello') // 'hello name'
}
function sayWord(word) {
console.log(word, name)
}
hi()
위 코드에서는 전역에서 hi를 호출하고 hi 내부에서 sayWord를 호출하였습니다.
콜스택에 쌓이는 모습은 아래와 같습니다.
실행 컨텍스트는 아래와 같이 생성됩니다.
// 실행 컨텍스트
전역컨텍스트: {
vo: {
name: 'name',
sayWord: function Object,
hi: function Object,
},
sc: ["전역 변수객체"]
this: window,
},
hi: {
vo: {
name: 'same'
},
sc: ['hi 변수객체', '전역 변수객체'],
this: window,
}
sayWord: {
vo: {
arguments: {
word: 'hello'
}
},
sc: ["sayWord 변수객체", "전역 변수객체"]
this: window,
},
아래는 위에서 사용한 함수를 실행컨텍스트의 스코프 체인을 이용하는 상황을 같이 서술한 것입니다.
var name = 'name'
function hi() {
var name = 'same'
console.log(name) // 'same' // name은 hi 컨텍스트의 변수객체 내에 존재하므로 가져와 사용합니다.
sayWord('hello') // 'hello name'
// sayWord는 hi컨텍스트의 변수 객체에 존재하지 않으므로 상위 스코프인 전역 컨텍스트의 변수객체를 참조합니다.
}
function sayWord(word) {
console.log(word, name)
// name은 sayWord의 변수 객체에 존재하지 않으므로 상위 스코프인 전역 컨텍스트의 변수객체를 참조합니다.
}
hi()
javascript에서 실행 컨텍스트 내부에 사용하고자 하는 변수가 존재하지 않는다면 상위 스코프의 변수객체를 참조합니다.
최상위 스코프까지 변수를 탐색하고 있으면 return, 찾지 못하면 referenceError를 반환합니다.
객체를 찾는것은 프로토타입 체인 입니다.
2022.02.28 - [Frontend/Javascript] - 🔥Javascript questions
this Value
this는 동적으로 결정됩니다. 이는 예측이 어려운 상황을 종종 만들어냅니다.
this는 함수와 다르게 선언되는 위치가 아니라 호출되는 상황에 따라 동적으로 값이 할당 됩니다.
this가 결정되는 상황은 크게 5가지 상황이 있습니다.
1. 일반 함수로 실행
2. 메소드로서 실행
3. 생성자 함수
4. bind, apply, call 을 사용한 명시적 바인딩
5. 콜백함수에서의 실행
2022.02.28 - [Frontend/Javascript] - 🔥Javascript questions
실행 컨텍스트가 어떤 과정으로 생성되는지 알아보겠습니다.
함수를 실행하면 함수의 실행 컨텍스트가 생성되고 콜스택에 함수 실행 컨텍스트가 추가됩니다.
추가된후에 아래의 1,2,3 단계가 실행됩니다.
이후 함수의 실행 컨텍스트 내용을 기반으로 함수 내의 코드가 실행됩니다.
1. 스코프 체인 생성 및 초기화
2. 변수 객체화(Variable Instantiation) 실행
3. this값 동적 할당
아래의 코드 실행과정을 따라가 보면서 위의 단계를 살펴보겠습니다.
var a = 'a'
function outerFunc() {
var b = 'b'
function innerFunc() {
console.log(a, b)
}
innerFunc()
}
outerFunc()
전역 코드 실행
위의 코드가 실행되면 제일 먼저 전역 컨텍스트가 생성된 후 콜스택에 추가됩니다.
전역컨텍스트 : {
vo: {},
sc: [],
this: window
}
1. 스코프 체인 생성 및 초기화
스코프 체인은 위에서 언급한 대로 선언된 위치의 영향을 받아 생성됩니다.
전역컨텍스트 : {
vo: {},
sc: ['전역 변수객체'],
this: window
}
// 전역 객체에는 DOM, BOM, Built-in Object등이 설정되어 있습니다!
전역 컨텍스트에서는 전역 객체에 접근할 수 있고
전역 객체에는 DOM, BOM, Built-in Object등이 설정되어 있습니다!!
2. 변수 객체화(Variable Instantiation) 실행
변수 객체화에서는 아래의 3단계 순서대로 처리를 수행합니다.
순서는 변경되지 않습니다!!!
2-1 함수 컨텍스트의 생성에서는 매개변수를 먼저 처리합니다.
2-2 함수 선언식을 끌어올려 변수 객체화를 수행합니다. (함수 호이스팅, 함수 표현식은 변수만 호이스팅)
2-3 변수들을 끌어올려 변수 객체화를 수행합니다.(변수 호이스팅, undefined로 초기화)
전역컨텍스트 : {
vo: {
a: 'a',
outerFunc: function Object
},
sc: ['전역 변수객체'],
this: window
}
// 전역 객체에는 DOM, BOM, Built-in Object등이 설정되어 있습니다!
이 단계에서 호이스팅이 발생합니다.
호이스팅이란 변수나 함수의 선언이 스코프 상단으로 끌어 올려 지는 것을 의미합니다.
호이스팅으로 인해 코드 상의 선언 전에 사용이 가능해 집니다.
console.log(name) // undefined
var name = 'lab'
console.log(name) // lab
hihi() // hihi
function hihi() {
console.log('hihi')
}
위의 코드는 실행 컨텍스트가 생성될때 변수 객체화 과정으로 인해 아래 코드처럼 변모합니다.
function hihi() {
console.log('hihi')
}
var name = 'undefined'
console.log(name) // undefined
name = 'lab'
console.log(name) // lab
hihi() // hihi
함수 호이스팅이 먼저 되고 변수 호이스팅이 됩니다.
변수 객체화는 전역 컨텍스트의 생성과 함수 컨텍스트의 생성에서 과정이 약간 다릅니다.
함수 컨텍스트의 생성에서는 매개변수를 먼저 처리합니다.
3. this값 동적 할당
this는 기본값을 window로 가지고 있습니다.
호출되는 상황에 따라 this의 값은 바뀌게 됩니다.
outerFunc 실행
위의 1,2,3 과정이 동일하게 실행됩니다.
실행컨텍스트 생성후 콜스택 추가
"outerFunc" : {
vo: {},
sc: [],
this: window,
}
1. 스코프 체인 생성 및 초기화
"outerFunc" : {
vo: {},
sc: ["outerFunc 변수객체", "전역 변수객체"],
this: window,
}
함수는 lexical scope(정적스코프)를 참조합니다.
outerFunc가 선언된 위치는 전역 컨텍스트 이므로 스코프 체인에는 전역컨텍스트가 있고 자기 자신의 스코프가 추가됩니다.
스코프 체인은 변수객체를 참조할때 이용됩니다.
변수를 먼저 자기자신의 스코프에서 찾고 존재하지 않으면 다음 스코프체인의 스코프에서 찾는 흐름 입니다.
2. 변수 객체화 실행
2-1. 함수면 매개변수 arguments 객체에 추가
2-2. 함수 호이스팅
2-3. 변수 호이스팅
"outerFunc" : {
vo: {
arguments: null,
b: 'b'
innerFunc: function Object,
},
sc: ["outerFunc 변수객체", "전역 변수객체"],
this: window,
}
만약 outerFunc의 코드에서 a를 출력한다면
1. 먼저 자기 자신의 변수 객체에서 찾아보고 없으므로
2. 다음 스코프 체인인 전역컨텍스트의 변수객체에서 a를 가져와 출력하게 됩니다.
전역컨텍스트 : {
vo: {
a: 'a',
outerFunc: function Object,
},
sc: ['전역 변수객체'],
this: window
}
3. this값 할당
innerFunc 실행
실행컨텍스트 생성 후 콜스택에 추가
"innerFunc": {
vo: {},
sc: [],
this: window,
}
1. 스코프 체인 생성 및 초기화
"innerFunc": {
vo: {},
sc: ["innerFunc 변수객체", "outerFunc 변수객체", "전역 변수객체"],
this: window,
}
2. 변수 객체화 실행
2-1. 함수면 매개변수 arguments 객체에 추가
2-2. 함수 호이스팅
2-3. 변수 호이스팅
innerFunc내부에는 변수가 존재하지 않으므로 객체화할 변수는 없습니다.
3. this값 할당
var a = 'a'
function outerFunc() {
var b = 'b'
function innerFunc() {
console.log(a, b)
}
innerFunc()
}
outerFunc()
자 이제 innerFunc가 실행되었을때
a 변수를 출력하기 위해 innerFunc 실행컨텍스트의 스코프 체인을 타고 이동하여 전역 컨텍스트의 변수 객체에서 a의 값을 가져옵니다.
b 변수를 출력하기 위해 innerFunc 실행컨텍스트의 스코프 체인을 타고 이동하여 outerFunc 컨텍스트의 변수 객체에서 b의 값을 가져옵니다.
이제 클로저를 실행컨텍스트를 이용하여 설명할 수 있습니다.
대표적인 클로저의 예시 하나를 가져왔습니다.
function counter(){
var count = 0
return {
getCount: function() {
return count;
},
addCount: function() {
count += 1
},
subCount: function() {
count -= 1
},
}
}
var counter1 = counter()
counter1.addCount()
console.log(counter1.getCount()) // 1
counter1.addCount()
console.log(counter1.getCount()) // 2
counter1.subCount()
console.log(counter1.getCount()) // 1
counter1.subCount()
console.log(counter1.getCount()) // 0
getCount, addCount, subCount의 실행 컨텍스트를 살펴보겠습니다.
getCount: {
vo: {},
sc: ["getCount 변수객체", "counter 변수객체", "전역 변수객체"],
this: window,
}
addCount: {
vo: {},
sc: ["addCount 변수객체", "counter 변수객체", "전역 변수객체"],
this: window,
}
subCount: {
vo: {},
sc: ["subCount 변수객체", "counter 변수객체", "전역 변수객체"],
this: window,
}
counter: {
vo: {
count: 0,
{
getCount: function Object,
addCount: function Object,
subCount: function Object,
}
},
sc: ["counter 변수객체", "전역 변수객체"],
this: window,
}
"전역 컨텍스트": {
vo: {
counter: function Object,
},
sc: ["전역 변수객체"],
this: window
}
getCount를 호출하게 되면 getCount내의 변수 객체에는 존재하지 않으므로 counter 변수객체에서 count를 가져와 출력해줍니다!
counter 실행컨텍스트는 종료되었지만 scope chaining에 의하여 counter의 변수 객체가 참조 되는 것입니다.
이러한 경우 자바스크립트 엔진에서는 해당 변수의 메모리를 계속 남겨둡니다.(garbage collection이 되지 않습니다.)
그래서 클로저를 많이 사용하게 되면 메모리가 부족해 질수도 있습니다.
출처 : https://poiemaweb.com/js-execution-context
http://dmitrysoshnikov.com/ecmascript/chapter-1-execution-contexts/
https://www.zerocho.com/category/JavaScript/post/5741d96d094da4986bc950a0
'Frontend > Javascript' 카테고리의 다른 글
Javascript Prototype 🤢 (0) | 2022.04.15 |
---|---|
React-query 🌱 (1) | 2022.04.09 |
npm, yarn (0) | 2022.04.03 |
Javascript custom array 🌹 (0) | 2022.04.02 |
Javascript createObjectURL, revokeObjectURL 🤥 (0) | 2022.04.01 |