이번 포스팅은 Javascript의 Execution Context(실행컨텍스트)
와 `Hoisting(호이스팅)
그리고 Scope Chaining(스코프체이닝)
에 대해 알아보도록 하겠습니다. 실행 컨텍스를 이해해야지만, 이 포스팅에 담기는 내용들인 Scope Chaining
과 hoisting
에 대한 이해가 수월합니다.
이번 포스팅역시 코어자바스크립트
라는 책을 기반으로 작성되었습니다.
1. 실행 컨텍스트(Execution Context)란?
javascript의 Execution Context(이하 실행컨텍스트)
는 code 실행시 필요한 정보들(필요한 환경들)을 모아놓은 객체 입니다.
javascript는 코드를 실행하며, 필요한 환경정보들(ex. 변수, 함수 등..)들을 모아 이를 이용해 실행컨텍스트
를 만들고, 이를 콜스택
에 쌓습니다. 그후, 코드를 실행하며 필요한 실행컨텍스트를 사용하고, 이를 스택에서 제거하는 식으로 코드의 환경과 순서를 보장합니다. 아직 잘 이해가 안가셔도 괜찮습니다. 아래에서 조금더 자세히 살펴보도록 하겠습니다.
1.1 실행컨텍스트가 생성되는 경우
실행 컨텍스트는 크게 3가지 상황에서 생성이 됩니다.
- javascript 실행 시작시 (전역컨텍스트)
- eval() 함수
- 함수 실행시
전역컨텍스트는 자동으로 생성이 되고, eval()은 여러 관점이 존재하지만, 많은 곳에서 악마로 취급을 받습니다. 때문에 우리가 실행컨텍스트를 구성하는 방법은 함수를 실행하는 것
이 유일합니다. 다시 한번 강조드리자면, 함수가 선언되는 시점이 아닌, 함수가 실행(호출)
되는 시점입니다.
1.2 실행컨텍스트와 코드의 실행과정
코드가 실행되며 실행 컨텍스트
가 어떤식으로 적용되는지는 예제를 살펴보며 알아보도록 하겠습니다. 실행컨텍스트
에 어떤 정보가 들어가는지 등은, 잠시뒤 설명드릴 예정입니다. 지금은 실행컨텍스트
가 언제 생성되고 콜스택에 쌓이고 제거되는지만을 집중해서 봐주세요.
01 // ------------------- (1)
02 var a = 1;
03 function outer() {
04 function inner() {
05 console.log(a); //undefined
06 var a = 3;
07 }
08 inner(); // ---------------- (2)
09 console.log(a); // 1
10 }
11 outer(); // ----------------- (3)
12 console.log(a); // 1
(1)
에서 전역컨텍스트가 생성되고, 이를 콜스택에 쌓습니다. (전역컨텍스트는 일반 컨텍스트와 별다를 바가 없습니다. 단지 코드 최상단의 전역적인 코드의 환경이기 때문에 붙혀진 이름입니다.)- 다시 코드를 실행하며
(3)
에서 outer()가 호출되고, outer() 함수 실행을 위한 실행컨텍스트가 생성되고, 콜스택에 쌓입니다. - outer() 함수가 실행되고 코드가 실행되다가,
(2)
에서 inner() 함수가 호출되어, inner() 함수 실행을 위한 실행컨텍스트가 생성되고, 콜스택에 쌓입니다. - inner() 함수안에서 a를 콘솔에 출력하고 나면 실행할 코드가 더이상 없기 때문에, inner()가 콜스택에서 제거됩니다.
- 그 다음 콜스택의 최상단에 위치한 outer() 함수가 다시 실행되면서, 9번째 줄을 실행하고 a를 콘솔에 출력하고, outer() 함수도 콜스택에서 제거됩니다.
- 콜스택 최상단에 위치한 전역컨텍스트가 다시 실행되며, 12번째줄에서 a를 다시 콘솔에 출력하고 종료됩니다.
1.3 실행컨텍스트의 정보
실행컨텍스트에 담기는 내용은 위 그림과 같습니다. 아래에서 조금더 자세히 설명드리겠습니다.
1.3.1 VariableEnvironment
최초 컨텍스트 생성시 VariableEnvironment에 모든 정보가 담기고 이를, LexicalEnvironment에서 복사하여 사용합니다. 즉, LexicalEnvironment의 스냅샷으로, 실행중에도 변경사항이 반영되지 않습니다. 지금은 중요하지 않으므로, LexicalEnvironment
를 살펴보겠습니다.
1.3.2 LexicalEnvironment
LexicalEnvironment에는 다시 크게 environmentRecord
와 outerEnvironmentReference
가 있습니다. LexicalEnvironment에 담기는 내용은 아래 2.
에서 더 자세히 다루도록 하겠습니다.
1.3.3 ThisBinding
ThisBinding에는 실행컨텍스트가 생성될 당시 this
로 지정된 객체가 저장이 됩니다. 함수
와 같이 this에 아무런 객체가 저장되지 않는 경우에는, 전역객체가 저장이 됩니다.
2. LexicalEnvironment와 Hoisting 그리고 ScopeChaning
LexicalEnvironment
의 구성은 environmentRecord, outerEnvironmentReference
로 이루어져 있다고 앞서 말씀드렸습니다. 이 각각의 데이터가 어떻게 수집되고 이용되는지를 파악한다면, Hoisting
과 ScopeChaining
에 대한 개념 역시 수월하게 파악하실 수 있습니다.
2.1 environmentRecord와 Hoisting(호이스팅)
environmentRecord
에는 현재 실행컨텍스트와 관련된 코드의 식별자 정보, 함수정보
들이 저장됩니다. javascript 엔진은 컨텍스트 내부의 모든 식별자를 environmentRecord에 저장한뒤 코드를 실행하게 됩니다. 아래에는 저장되는 식별자 정보들의 예입니다.
- 함수의 매개변수 식별자
- 선언된 함수자체
- let, const등으로 선언된 변수의 식별자
Hoisting(호이스팅)
environmentRecord
에 저장될 정보들은 코드 실행전, 컨텍스트 내부를 쭉 훑으며 순서대로 수집을 합니다. 각각의 식별자 정보들을 모두 수집한 뒤에도, 코드들은 아직 실행전이기 때문에, javascript엔진은 실행할 컨텍스트의 내부에 있는 모든 식별자들을 코드의 최상단으로 끌어올려 놓은 다음 코드를 실행하는것과 마찬가지로 간주해도 코드를 해석하는데 잘못되는 부분이 발생하지 않습니다.
실제로는 변수들을 최상단으로 끌어올리는 등의 코드변화를 일으키지는 않지만 그렇게 간주를하자는 개념이 바로 Hoisting입니다. javascript 엔진이 변수 정보를 수집하고 코드를 실행하는 과정을 더욱 이해하기 쉽게 하기위해 발생된 개념이죠.
조금 더 이해를 도와드리기 위해 Hoisting 예제를 살펴보겠습니다.
xxxxxxxxxx
01 function a(x) {
02 console.log(x); // (1)
03 var x;
04 console.log(x); // (2)
05 var x = 2;
06 console.log(x); // (3)
07 }
08 a(1);
hoisting이 되지 않은 경우
hoisting이 되지 않은 경우의 위 예제의 출력을 예상해본다면 어떨까요? (1)
에서는 1이, (2)
에서는 undefined, (3)
에서는 2가 출력된다고 예상이 됩니다. 하지만 실제 javascript 실행시 이 예상과는 전혀다른 결과값이 나타나게 됩니다.
hoisting이 되는 경우
hoisting이 되는 경우에는 8번째 줄
에서 함수 a(1)이 호출되면서, 콜스택에 a() 함수의 실행컨텍스트가 쌓이게 됩니다. 실행컨텍스트는 코드 실행전 모든 식별자를 스캔하여 저장한다고 말씀드렸습니다. 때문에 매개변수 x
와 (1), (2), (3)
에서 선언된 변수 x는 모두 같은 이름의 식별자이기 때문에, 스캔 후 실행컨텍스트에는 단 하나의 x
식별자 정보가 담겨있게 됩니다. 한줄씩 자세히 살펴보겠습니다.
- 매개변수 x에 1을 담아 호출을 했기 때문에, 2번째 줄코드가 실행되기전의
environmentRecord
에는var x = 1
이라는 정보가 담겨있습니다. 따라서 (1)에서는1
이 출력됩니다. environmentRecord
에 이미var x
가 존재하기 때문에, 3번째 줄의var x
는 무시됩니다.(2)
에서는environmentRecord
에 존재하는 x식별자의 값인1
이 출력됩니다.- 5번째 줄에서 environmentRecord의 x값을 2로 변경합니다.
(3)
에서는 2가 출력됩니다.
xxxxxxxxxx
01 function a() {
02 var x;
03 x = 1;
04 console.log(x);
05 console.log(x);
06 x = 2;
07 console.log(x);
08 }
09 a();
위 예제코드는 앞선 예제코드가 hoisting이 진행되고난 후의 가상의 코드상황입니다.
2.2 outerEnvironmentReference와 scopeChaining
scopeChaning (스코프 체이닝)
x
01 function a() {
02 var a = 1;
03 function b() {
04 var b = 2;
05 function c() {
06 var c = 3;
07
08 console.log(c); // 3
09 console.log(b); // 2
10 console.log(a); // 1
11 }
12 c();
13 }
14 b();
15 }
16 a();
scope
는 어떤 식별자에 대한 유효범위를 의미합니다. 위와 같은 예제에서 javascript는 함수 c()
내부에 a와 b라는 변수가 존재하지 않더라도, 상위 scope (콜스택의 하위에 존재하는 실행컨텍스트들)에 존재하는 변수인 b, a
에 접근하여 출력이 가능합니다.
어떻게 이러한 접근이 가능할까요? 이는 바로 실행컨텍스트 내부의 LexicalEnvironment의 두번째 수집 자료인 outerEnvironmentReference
덕분입니다. 이 outerEnvironmentReference
가 어떠한 데이터를 담고있는지를 파악한다면, scopeChaining
의 과정을 쉽게 이해하실 수 있습니다.
outerEnvironmentReference
는 현재 함수가 호출될 당시의 LexicalEnvironment
를 참조합니다. 다시말해 콜스택에서 현재 실행컨텍스트 바로 하단에 위치한 LexicalEnvironment
를 참조한다는 것 입니다. outerEnvironmentReferenece
는 연결리스트
의 형태를 띄고 있습니다. 천천히 예제와 그림을보며 설명드리겠습니다.
- 16번째 줄에서 a()가 호출되며
function a
에 대한 실행컨텍스트가 생성되어 콜스택에 쌓이며, 이LexicalEnvrionment
의outerEnvironmentReference
는 호출될 당시의 활성 컨텍스트인전역컨텍스트의 LexicalEnvironment
를 참조합니다.
- a() 함수가 실행되다가, 14번째 줄에서 b()가 호출되며
function b
에 대한 실행컨텍스트가 생성되어 콜스택에 쌓이고, 이LexicalEnvrionment
의outerEnvironmentReference
는 호출될 당시의 활성 컨텍스트인function a의 LexicalEnvironment
를 참조합니다. - b() 함수가 실행되다가 12번째 줄에서 c()가 호출되며 같은 방식으로 c의 실행컨텍스트의
outerEnvironmentReference
는 c()가 호출될 당시의 활성컨텍스트인 b의 LexicalEnvironment를 참조하게 됩니다. - c() 함수가 실행되다가 8번째 줄에서 c를 콘솔에 출력하라는 명령을 받고, javascript엔진은 현재 활성화 실행컨텍스트(c)의
environmentRecord
에서식별자 c
가 존재하는지를 찾습니다. 존재 하기 때문에, 3을 출력합니다. - 이어 9번째 줄에서는 b를 콘솔에 출력하라는 명령입니다. javascript엔진은 현재 활성화 실행컨텍스트의 environmentRecord에서 식별자 b가 존재하는지를 찾지만 존재하지 않습니다. 때문에
outerEnvironmentReference
가 참조하는 상위컨텍스트(b)
의environmentRecord
에서식별자 b
가 존재하는지를 찾습니다. b를 찾았으니 2를 출력합니다. - 10번째 줄에서는 a를 콘솔에 출력하기 위해 javascript엔진은
environmentRecord
에서식별자 a
를 찾고 존재하지 않다면outerEnvironmentReference
를 참조하는 식으로 반복하며a
를 찾아 나갑니다. a 실행컨텍스트에서 식별자 a를 찾아 1을 출력하고 종료됩니다.
이처럼 현재 scope에서 식별자를 찾고 존재하지 않는다면 계속해서 상위 scope로 범위를 넓히며 식별자를 찾는 과정을 scopeChaning
이라고 합니다.
scopeChaning의 특징
xxxxxxxxxx
01 function a(){
02 let num = 1;
03 function b(){
04 let num = 2;
05 function c() {
06 let num = 3;
07
08 console.log(num); // 3
09 }
10 c();
11 }
12 b();
13 }
14 a();
scopeChaning은 상위로 점차 scope를 넓히며 식별자를 찾아 나아간다고 말씀드렸습니다. 이 때문에 코드 실행시 필요한 식별자가 현재 실행컨텍스트에 존재한다면 이를 반환하고 더이상 상위 scope로 검색을 진행하지 않습니다. 따라서 위의 예제에서 8번째 줄의 결과는 평생을 찍어보아도 3밖에 출력되지 않습니다.
즉 여러 scope에서 동일한 식별자가 선언되어 있는 경우, 무조건적으로 현재 scope에서 가장 먼저 발견되는 식별자에만 접근이 가능합니다.
'Web > javascript' 카테고리의 다른 글
javascript - [코어자바스크립트] 불변객체와 복사(얕은복사와 깊은복사) (0) | 2019.11.24 |
---|---|
javascript - [코어자바스크립트] javascript의 메모리와 데이터할당 (데이터 할당의 자유와 효율성) (0) | 2019.11.21 |
javascript - Redux란? (redux 예제) (0) | 2019.09.08 |
Javascript - ES6 (` : 템플릿 리터럴) 문자열에 변수 포함시키기 (0) | 2019.09.01 |
javascript - 동기, 비동기 처리과정과 event loop (0) | 2019.05.14 |