코어 자바스크립트 -2 실행 컨텍스트(feat.프링글스 제조과정)

뿌링클 치즈맛·2023년 6월 1일
0

전역컨텍스트맛 프링글스!

1. 실행 컨텍스트

실행할 코드에 제공할 환경 정보들을 모아놓은 객체.
자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.

스택과 큐

스택: 프링글스통 (a,b,c,d를 저장하면 꺼낼때는 d,c,b,a) , FILO
StackOverFlow의 그 스택이다. 많은 프로그래밍 언어는 스택이 넘칠 때 에러를 던진다.

큐: 놀이공원 입장 (a,b,c,d로 저장하면 꺼낼때도 a,b,c,d) , FIFO

실행컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체다. 실행 컨텍스트를 구성하는 가장 흔한 방법은 함수를 실행하는 것이다.
동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 구성된 컨텍스트들을 순서대로 콜스택에 쌓아올렸다가 가장 위에 쌓인 컨텍스트(가장 나중에 만들어진)와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.

var a=1;
function Outer(){
  function Inner(){
    console.log(a); //undefined
    var a=3;
  }
  inner(); -->4번 그림
  console.log(a)//1;
}
Outer(); -->3번 그림
console.log(a); //1 -->1번 그림

  1. 자바스크립트 코드 실행 전
  2. 자바스크립트 코드 실행시 전역컨텍스트가 콜 스택에 담김(활성화됨)
  3. Outer함수가 호출되면 JS엔진이 Outer에 대한 환경 정보를 수집해 Outer 컨텍스트 생성 후, 콜스택에 담음, 콜스택의 맨 위에 Outer 실행 컨텍스트가 놓였으니 전역 컨텍스트와 관련된 코드의 실행을 일시중단하고 Outer 실행컨텍스트와 관련된 Outer 함수 내부의 코드들을 순차적으로 실행
  4. Outer 함수 내에 있는 Inner 함수의 실행컨텍스트를 가장 위에 올리고 실행.Outer 중단.
  5. Inner 함수에 a변수에 3을 할당하고 나면 Inner 함수가 종료되고 Inner실행 컨텍스트가 콜스택에서 제거됨. 중단했던 inner();의 다음 줄부터 이어서 실행.(Outer함수 실행)
  6. a변수를 출력하고 Outer 함수도 종료됨
  7. a변수의 값을 출력하고 나면 전역공간에 더는 실행할 코드가 남아 있지 않아 전역 컨텍스트가 제거됨

2. VariableEnvironment

VariableEnvironment에 담기는 내용은 Lexical Environment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다.
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 Lexical Environment을 만들고 이후에는 Lexical Environment을 주로 활용한다.

3. Lexical Environment

현재 컨텍스트의 내부에 있는 식별자들과 그 식별자들이 참조하는 정보 등, 컨텍스트를 구성하는 환경 정보들을 사전처럼 모아두었다.

3-1.EnvironmentRecord와 호이스팅

//맨 위에 var a;가 선언되어있다고 생각해도 무방하다!
console.log(a) //undefined
var a= 10;
console.log(a)//10

environment에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 순서대로 저장됨
코드가 아직 실행되지는 않았지만 JS엔진이 이미 코드의 변수명을 모두 알고 있는 상태다.
JS엔진이 식별자를 최상단으로 끌어올려놓은 다음 실제 코드를 해석하는 것이라고 봐도 무방하다.

호이스팅 규칙

environmentRecord에는 파라미터의 이름, 함수 선언, 변수명 등이 담긴다.

1번 코드
function a(x){
  console.log(x); //1번째 콘솔: 1
  var x;
  console.log(x); //2번째 콘솔: 1
  var x=2;
  console.log(x); //3번째 콘솔: 2
}
a(1)

위와 아래의 코드는 a에 인자가 전달되었는지 아닌지와 내부에 var x=1;이 할당이 된 것 외에는 다른 점이 없다. 실행 결과도 같다.
즉, a에 매개변수로 전달된 1이 함수 내부의 다른 코드보다 먼저 선언과 할당이 이루어진 것으로 볼 수 있다.

2번 코드
fuction a(x) {
  var x=1;
  console.log(x); //1번째 콘솔: 1
  var x;
  console.log(x); //2번째 콘솔: 1
  var x=2;
  console.log(x); //3번째 콘솔: 2
}
a()

그래서 매개변수 1을 변수 선언과 할당(1)으로 바꿨다.(2번코드)

3번 코드
function a(){
  var x;
  var x;
  var x;
  
  x=1;
  console.log(x); //1번째 콘솔: 1
  console.log(x); //2번째 콘솔: 1
  
  x=2;
  console.log(x); //3번째 콘솔: 2
}

위에 1,2번 코드에서 함수 호이스팅을 모두 마친 상태를 표현한 것이 3번 코드다.
변수 x가 끌어올려지고, 값으로는 undefined가 할당된다. 아래 두 개의 변수 x는 이미 x가 선언된 x가 있으니 무시한다. 그리고 x에 1을 할당하고, console.log가 두 번 실행된다. 둘 다 1이 출력되고 x에 2를 할당한 뒤 3번째 콘솔의 console.log를 실행하면 2가 출력된다.
함수 a 내부의 모든 코드가 실행되었으므로 함수 a의 실행 컨텍스트가 콜스택에서 제거된다.

함수 선연문과 함수 표현식

함수 선언문과 함수 표현식은 모두 함수를 새롭게 정의할 때 쓰이는 방식인데, 그 중 함수 선언문은 function 정의부function a(){}만 존재하고 별도의 할당 명령이 없는 반면, 함수 표현식은 정의한 함수를 별도의 변수에 할당하는 것을 의미한다.

함수 선언문은 반드시 함수명이 정의되어있어야 하지만, function add(x,y){return x+y} add();로 호출
함수 표현식은 없어도 된다. var multiply=function(x,y){return x*y} multiply()로 호출
함수 표현식에 함수명을 정의한 함수 표현식을 '기명 함수 표현식' 이라고 하고, 정의하지 않은 것은 '익명 함수 표현식' 이라고 한다.
var subtract=function sub(x,y){return x-y} subtract()로 호출한다. sub()은 에러가 발생한다.

함수 선언문과 함수 표현식의 차이는 호이스팅에서 확인할 수 있다.

원본 코드
console.log(sum(1,2));
console.log(multiply(3,4));

function sum(a,b){return a+b;} //함수 선언문
var multiply = function(x,y){return x*y} //함수 표현식

호이스팅이 끝난 상태

var sum= function sum(a,b){return a+b;} -1
var multiply; -2
console.log(sum(1,2)); -3
console.log(multiply(3,4)); -4

 multiply = function(x,y){return x*y} -5

함수 선언문은 전체를 호이스팅하고, 함수 표현식은 var multiply만 호이스팅된다.

실행 순서
1. 변수 sum을 확보된 메모리 공간에 이름으로 준다.(1)
2. 또 다른 메모리 공간을 확보한 후, 변수 multiply에 연결한다.(2)
3. 함수 sum을 또또 다른 메모리 공간에 저장하고, 그 주솟값을 변수 sum의 값에 연결한다. (1)
4. sum함수를 실행해 콘솔에 출력한다.(3)
5. 변수 multiply에는 아무 값이 할당되어있지 않으므로 에러가 발생한다.
TA-DA!

함수 선언문은 혼란스러운 개념이다. 만약 A개발자가 sum함수(x+y를 리턴함)를 선언하고 잘 사용하고 있는데 신입 개발자B가 같은 파일의 500번째 줄에서 sum함수(x+'+'+y+'='+(x+y))를 선언했다. B는 500번 이후에만 sum함수가 영향을 미칠 것이라고 생각해 배포를 해버린다.
하지만 전역 컨텍스트가 활성활 될 때 코드 내에 선언된 모든 함수가 호이스팅되고, 동일한 변수명에 다른 값이 할당되는 경우에는 나중에 할당된 값이 먼저 할당된 값을 덮어씌운다. A가 선언한 sum함수는 온데간데 없어지고 B가 선언한 sum함수만 남는 것이다.
만약 A와 B가 모두 함수 표현식으로 정의했다면 A와 B가 의도한대로 sum함수가 제대로 실행되었을 것이다.

3-2. 스코프,스코프 체인,outerEnvironmentReference

스코프: 식별자에 대한 유효 범위 스코프 체인: 식별자의 유효범위를 안에서 바깥으로 차례로 검색해 나가는 것. 경계A의 외부에서 선언한 변수는 A의 외부 뿐만 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 A의 내부에서만 접근할 수 있다.

스코프 체인
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 Lexical Environment를 참조한다. 선언된 시점은 콜 스택 상에서 실행 컨텍스트가 활성화된 상태일 때 뿐이다. 모든 코드는 실행컨텍스트가 활성화 상태일 때 실행되고, 어떤 함수를 선언하는 행위는 하나의 코드이기 때문이다.

보기 편하게 대괄호를 이동시켰다.
var a=1; ->1
var outer=function out()->2
{
  var inner=function in() ->3
  {
    console.log('1',a); ->4//undefined 
    var a=3;->5
  }->6
  inner(); ->7 //inner함수 호출
  console.log('2',a) ->8//3의 값을 가진 a는 inner함수 안에서 선언되고 초기화되었으므로 같은 스코프에 있는 1의 값을 가진 a를 출력한다.
};->9
outer(); ->10 //
console.log('3',a); ->11 //1이 출력된다.

실행 순서
1. var a와 var outer가 레코드에 저장된다.outer의 함수 out은 저장되지 않고 outer라는 이름만 레코드에 저장된다.(1,2)
-전역 컨텍스트의 Lexical Environment: { a, outer} =>[GLOBAL(실행 컨텍스트의 이름), {a,outer}(레코드에 저장된 객체)]
2. 전역 스코프에 위치한 변수 a에 1이 할당되고 똑같이 전역 스코프에 있는 변수 outer에 함수 out을 할당한다. (2)
3. outer함수를 호출한다.(10) 11번으로 이동하지 않고 outer 실행 컨텍스트가 콜스택에 쌓이고 2번 코드로 이동해 실행한다.(10->2)
-out 함수의 Lexical Environment: [outer,{ inner }]
4. outer 실행 컨텍스트의 레코드에 var inner를 저장한다. (3)
5. outer 스코프에 있는 inner에 함수 in을 할당한다. (3)
6. inner함수를 호출한다.(7) 8번으로 이동하지 않고 3번으로 이동해 함수를 실행한다. (7->3)
7. var a를 inner 실행컨텍스트의 environmentRecord에 저장한다.(5)
-outerEnvironmentReference에는 inner함수가 선언될 당시의 Lexical Environment[outer,{inner}]가 담기고, inner함수는 outer함수의 내부에서 선언되었으므로 Lexical Environment를 참조복사한다. [inner,{a}] 이때 inner의 outerEnvironment는 outer Lexical Environment다.
8. console.log('1',a)에 존재하는 식별자 a에 접근하고자 한다. inner 컨텍스트의 레코드에서 a를 검색한다.7번에서 레코드에 저장한 a를 찾았다. 값은 없으니 1,undefined를 출력한다.(4)
9. inner 스코프에 있는 변수 a에 3을 할당한다.(9)
10. in함수의 실행이 종료된다.(6) 콜스택에 쌓여있던 inner컨텍스트가 제거되고 outer실행컨텍스트가 가장 최상위가 되면서 활성화된다. 8번 코드로 이동한다.(6->8)
11. 식별자 a를 찾아보자. outer 컨텍스트에는 [outer,{inner}]뿐이므로 outer의 outerEnvironmentReference에 있는 전역 Lexical Environment[GLOBAL,{a,outer}]에서 a를 찾아본다. 전역Lexical Environment에 a가 있으니 a에 할당된 1을 반환한다.('2',1)(8)
12. out함수 실행이 종료된다.(9) outer 실행 컨텍스트가 콜스택에서 제거되고, 바로 아래에 있던 전역 컨텍스트가 다시 활성화되면서 11번째 줄로 이동한다.(9>11)
13. 식별자 a를 또! 찾아보자. 현재 활성화 상태인 전역 컨텍스트의 레코드에 a가 있으니 3,1을 출력해주자.(11)
14. 모든 코드의 실행이 완료되었다. 전역 컨텍스트가 콜 스택에서 제거된다. 마지막 프링글스까지 다 먹었으니 통을 버려주자!

하지만 스코프 체인 위에 있는 변수라고 무조건 접근할 수 있는 것은 아니다. 위의 코드에서 a는 전역에서도 선언되었고, in함수 내에서도 선언된다. inner함수에서 a에 접근하려고 하면 무조건 inner스코프의 Lexical Environment부터 검색할 수밖에 없다. inner에 있으니 더 이상의 검색은 진행되지 않고 inner에 저장된 a를 반환한다. 이때 전역에 선언된 동일한 이름의 변수인 a에는 접근할 수 없고, 이를 변수 은닉화 라고 한다.

실행순서를 정리하면서 이해한 것

작업 환경: Lexical Environment [맛의 이름,{변수,변수(변수에 할당된 값/함수)}]
원래는 이런 형태인데 [ { } ] 보기 편하려고 ( )를 추가함
영양 정보: environmentRecord
다른 맛의 영양 정보 : outerEnvironmentReference

빈 조리대 위에서 레시피에 따라 프링글스를 만들어보자.

  1. 아무것도 없는 빈 조리대 위에 맨 아래에 기본 반죽인 전역맛 프링글스 반죽을 넣어줬다. 전역맛 프링글스 반죽에는 변수 a와 outer가 들어있다. 전역맛의 작업환경(Lexical Environment)에 영양정보로 a와 outer를 저장해준다[GLOBAL,{a,outer}]. 저장하고 레시피대로 만드는 과정에서 변수 a에는 1이,변수 outer에는 함수out이 할당되어있음을 확인하고 아까 저장한 outer에 연결해준다[GLOBAL,{a(1),outer(function out)}]. 전역맛 레시피대로 실행하다 보니 outer맛이 필요하다는 것을 알고 outer 맛을 우선 완성하기로 했다. outer맛이 전역맛 위에 얹어졌다. 반죽이지만 얹여져도 된다고 하자... 만드는 사람의 입장에서는 전역맛은 안 보이고 outer맛만 보이는 상태다. 그렇지만 아까 영양정보에 전역맛에 a와 outer=out함수 가 저장되었다는 것은 알고 있고, 필요할 때는 이 정보를 사용할 수도 있다(outer의 outerEnvironmentReference=> 전역맛의 영양정보).
  2. outer맛에는 변수 inner가 들어있다.inner를 outer의 영양정보에 저장한다.[outer,{inner}] inner에 함수 in이 할당되었음을 확인하고 저장한 inner에 연결해준다[outer,{inner(function in)}]. 이후 레시피를 확인하니 inner맛이 필요하다고 한다. inner 맛을 outer 맛 위에 얹어준다. outer맛은 보이지 않고 아까 저장한 outer의 영양정보와 전역맛의 영양정보를 확인할 수 있다.(inner의 outerEnvironmentReference => outer의 영양정보와 전역맛의 영양정보) 전역맛은 아까 저장해뒀으니 언제든지 꺼내 쓸 수 있다.
  3. inner맛에는 변수 a가 들어있다. a를 outer에 영양정보에 저장한다.[inner,{a}] ?앞서 전역맛에 있던 a와는 다른 영양성분이다.?
    레시피는 1과 a를 출력하라고 한다. inner의 영양정보에서 a를 찾아본다. 별다른 값이 할당되어있지 않으므로 undefined를 출력해준다. 출력한 이후에 a에 3을 할당하라는 지시에 따라 작업환경을 [inner,{a(3)}]로 만들어준다. inner맛을 완성했다.완성된 프링글스는 구워야하니 조리대에서 치워준다. inner맛의 영양정보도 필요없으니 접어두자. 이제 outer맛 프링글스 반죽이 제일 위에 보인다.
  4. outer맛 레시피에서 2와 변수 a를 출력하라고 한다. 아까 a를 어디서 본 것 같은데...(만드는 사람은 inner맛에 있던 영양정보를 까먹었다.) outer 영양정보에는 [outer,{inner(function in)}]밖에 없다. 그럼 그 전에 만들었던 전역맛 영양정보를 확인하자.[GLOBAL,{a(1),outer(function out)}] 다행히 전역맛에 a가 있음을 확인했다. a에는 1이 할당되어있으니 2,1을 출력해준다. 레시피에 따르면 이제 outer맛도 완성이다. outer맛 반죽과 outer맛의 영양정보를 조리대에서 치워버렸다. 이제 조리대 위에는 전역맛 반죽과 레시피,전역맛의 영양정보만 남아있다.
  5. 전역맛 레시피의 마지막 줄은 3과 a를 출력하라는 내용이다. a를 찾으면 프링글스 완성이다! 전역맛 영양정보에 a가 있으니 재빠르게 가져와 3,1을 출력한다. 전역맛 반죽도 완성했으니 오븐에 구워주고 레시피와 영양정보가 들어있는 작업환경 쪽지를 모두 버린다. 도비는 자유에요!

이때 inner맛을 만드는 시점에서 outer맛과 전역맛의 영양정보에는 모두 접근할 수 있다. 하지만 inner맛을 완성하고 outer맛을 만들때는 이미 영양정보를 접어두었으니 접근할 수가 없다.

전역 변수와 지역변수
전역변수: 전역 스코프에서 선언한 변수.(a,outer)
지역 변수: 함수 내부에서 선언한 변수.(inner,inner 안의 a)

코드의 안전성을 위해서는 가급적이면 전역 변수의 사용은 최소화하는 것이 좋다!

4. this THAT PINK VENOM

실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트가 활성화되었을 때, this가 지정되지 않은 경우에 this는 전역 객체(window)를 가리킨다.

5. 정리

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 전역 컨텍스트/함수 실행에 의한 컨텍스트 등이 있다.
실행 컨텍스트 객체는 활성화 되는 시점에 VariableEnvironment, LexicalEnvironment,ThisBinding의 세 가지 정보를 수집한다.
실행 컨텍스트 생성시에는 VariableEnvironment와 LexicalEnvironment가 같은 내용으로 구성되지만 LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 있으면 즉시 반영되고, VariableEnvironment는 초기 상태를 유지한다. VariableEnvironment와 LexicalEnvironment는 매개 변수 명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성되어 있다.

호이스팅: 코드 해석을 보다 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념.
실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 끌어올린다고 해석하는 것.
변수 선언과 값 할당이 동시에 이뤄진 경우에는 선언부만 호이스팅하고, 할당과정은 원래 자리에 남아있게 되고, 여기서 함수 선언문과 함수 표현식의 차이가 발생한다. 함수 선언문은 전체를 호이스팅하고, 함수 표현식은 함수가 저장된 변수 명만 호이스팅된다.

스코프: 변수의 유효 범위. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다.(자기보다 하나 위의 것을 참조한다고 생각함) 현재 LexicalEnvironment에 찾고자 하는 변수의 값이 없으면 outerEnvironmentReference에서 찾는다. 이런 과정을 거쳐 전역컨텍스트의 LexicalEnvironment에서도 찾지 못하면 undefined를 반환한다.

전역 변수:전역 컨텍스트의 LexicalEnvironment에 담긴 변수
지역 변수: 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들

this:실행 컨텍스트 활성화 당시에 지정된 this가 저장. 함수 호출 방법에 따라 그 값이 달라짐. 지정되지 않은 경우에는 전역 객체가 저장됨

profile
뿌링클 치즈맛

0개의 댓글