전역컨텍스트맛 프링글스!
실행할 코드에 제공할 환경 정보들을 모아놓은 객체.
자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.
스택과 큐
스택: 프링글스통 (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번 그림
VariableEnvironment에 담기는 내용은 Lexical Environment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다.
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 Lexical Environment을 만들고 이후에는 Lexical Environment을 주로 활용한다.
현재 컨텍스트의 내부에 있는 식별자들과 그 식별자들이 참조하는 정보 등, 컨텍스트를 구성하는 환경 정보들을 사전처럼 모아두었다.
//맨 위에 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에는 아무 값이 할당되어있지 않으므로 에러가 발생한다.
함수 선언문은 혼란스러운 개념이다. 만약 A개발자가 sum함수(x+y를 리턴함)를 선언하고 잘 사용하고 있는데 신입 개발자B가 같은 파일의 500번째 줄에서 sum함수(x+'+'+y+'='+(x+y))를 선언했다. B는 500번 이후에만 sum함수가 영향을 미칠 것이라고 생각해 배포를 해버린다.
하지만 전역 컨텍스트가 활성활 될 때 코드 내에 선언된 모든 함수가 호이스팅되고, 동일한 변수명에 다른 값이 할당되는 경우에는 나중에 할당된 값이 먼저 할당된 값을 덮어씌운다. A가 선언한 sum함수는 온데간데 없어지고 B가 선언한 sum함수만 남는 것이다.
만약 A와 B가 모두 함수 표현식으로 정의했다면 A와 B가 의도한대로 sum함수가 제대로 실행되었을 것이다.
스코프 체인
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
빈 조리대 위에서 레시피에 따라 프링글스를 만들어보자.
이때 inner맛을 만드는 시점에서 outer맛과 전역맛의 영양정보에는 모두 접근할 수 있다. 하지만 inner맛을 완성하고 outer맛을 만들때는 이미 영양정보를 접어두었으니 접근할 수가 없다.
전역 변수와 지역변수
전역변수: 전역 스코프에서 선언한 변수.(a,outer)
지역 변수: 함수 내부에서 선언한 변수.(inner,inner 안의 a)
코드의 안전성을 위해서는 가급적이면 전역 변수의 사용은 최소화하는 것이 좋다!
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 전역 컨텍스트/함수 실행에 의한 컨텍스트 등이 있다.
실행 컨텍스트 객체는 활성화 되는 시점에 VariableEnvironment, LexicalEnvironment,ThisBinding의 세 가지 정보를 수집한다.
실행 컨텍스트 생성시에는 VariableEnvironment와 LexicalEnvironment가 같은 내용으로 구성되지만 LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 있으면 즉시 반영되고, VariableEnvironment는 초기 상태를 유지한다. VariableEnvironment와 LexicalEnvironment는 매개 변수 명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성되어 있다.
호이스팅: 코드 해석을 보다 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념.
실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 끌어올린다고 해석하는 것.
변수 선언과 값 할당이 동시에 이뤄진 경우에는 선언부만 호이스팅하고, 할당과정은 원래 자리에 남아있게 되고, 여기서 함수 선언문과 함수 표현식의 차이가 발생한다. 함수 선언문은 전체를 호이스팅하고, 함수 표현식은 함수가 저장된 변수 명만 호이스팅된다.
스코프: 변수의 유효 범위. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다.(자기보다 하나 위의 것을 참조한다고 생각함) 현재 LexicalEnvironment에 찾고자 하는 변수의 값이 없으면 outerEnvironmentReference에서 찾는다. 이런 과정을 거쳐 전역컨텍스트의 LexicalEnvironment에서도 찾지 못하면 undefined를 반환한다.
전역 변수:전역 컨텍스트의 LexicalEnvironment에 담긴 변수
지역 변수: 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들
this:실행 컨텍스트 활성화 당시에 지정된 this가 저장. 함수 호출 방법에 따라 그 값이 달라짐. 지정되지 않은 경우에는 전역 객체가 저장됨