- 일할 때 잘하려면 배우는 지금 나의 무식함이 드러나고 지적받아야 한다. 창피해하지 말자
- 선배에게 잘 보이려고 괜히 빨리 끝내겠다고 하지 말고, 여유로운 시간 마감을 가지고 잘 해서 결과물로 잘 보여라.
- 데드라인에 대해서 코멘트할때는 근거를 가지고 설명해라(얼마나 걸릴지 수치로 얘기하고, 거의 다 됐습니다 라고 하지 말 것)
- 표현식을 평가하면 값이 만들어지듯, 소스코드를 평가하면 실행 컨텍스트가 만들어진다.
- 실행 컨텍스트는 4가지 소스코드가 평가될 때 만들어진다.
- 전역 코드
- 함수 코드: 함수 몸체 안의 코드를 의미하며, 호출하기 전까지는 평가도, 실행도 되지 않는다. 객체로만 존재하다 호출되어야 평가 시작
- eval 코드: 문자열로 된 소스코드를 인수로 전달
- 모듈 코드: 자신의 실행컨텍스트와 스코프를 만든다.
- 소스코드 평가 단계
- 평가되면 선언문들만 실행(호이스팅)되어 식별자로 등록된다.
- 선언문의 특징은 식별자를 꼭 갖는다는 것이다. 그래야 스코프에 key가 등록되니까.
- 어디에 등록되는가? 등록되려면 자료구조가 있어야 한다. 이 자료구조가 바로 실행 컨텍스트
- 소스코드 실행 단계
- 선언문 제외한 코드가 한줄씩 실행된다.
- 할당문이나 참조하는 순간 식별자를 갖는 객체에 들어가 값을 변경하거나 가져온다.
- 즉 실행 컨텍스트는 소스코드가 평가되고 실행되는 중 식별자를 관리하며, 다른 언어에도 이름만 다를 뿐(symbol table) 존재한다.
var x;
x = 1;
- 위와 같은 전역컨텍스트가 있다고 할 때, 소스코드가 로드되면 전역코드의 평가가 시작된다.
- 전역실행컨텍스트를 생성하고, 실행컨텍스트 스택에 이를 push한다.
- 선언문을 실행하여
var
키워드로 선언된 x라는 식별자를 실행컨텍스트에 등록하되, 값은 undefined로.
- 선언문을 실행하여
- 전역코드의 실행 단계(런타임)에서는 선언문은 건너뛰고 할당문만 실행한다.
- 현재 실행중인(최상단) 실행컨텍스트에서 선언되었는지 식별자를 찾고, 만약 선언문이 종점스코프에까지 없다면 암묵적 전역으로 전역객체에 프로퍼티를 추가한다.
- x에 할당될 숫자 리터럴 1은 식별자 x를 찾은 후 값으로 만들어져 할당된다.
const x = 1;
const y = 2;
function foo(a) {
const x = 10;
const y = 20;
console.log(a + x + y); // 130
}
foo(100);
console.log(x + y); // 3
- 소스코드 로드하며 전역코드 평가 시작, 전역 실행 컨텍스트 생성 및 실행컨텍스트에 push
- AST 트리자료구조로 소스코드를 parsing한 후 자료구조를 싹 한 번 돌면서(traverse) 선언문만 발라내 선두에 있는 선언문부터 실행하여 아까 만들어진 전역실행컨텍스트의 렉시컬환경에 식별자를 넣는다.
const
로 선언된 식별자는 우리가 알 수 없는 값(ECMAScript에 명시되지 않음)으로 초기화가 된 후, 할당 이전에 참조하면 에러를 일으키도록 TDZ 처리된다.- function 선언문이 실행될 때, 함수이름을 식별자로 하는 키에 함수객체가 만들어져 binding된다. 이 때 함수객체가 갖는 프로퍼티와 내부슬롯
[[Environment]]
이 세팅된다. 런타임 이전에 함수객체가 생성되기 때문에 함수 호이스팅이 일어나는 것이다.
- 전역코드의 실행단계에 들어가 할당문을 실행하고, 함수를 호출한다.
- 함수를 호출할 때 인수로 들어가는 숫자 리터럴 100은 값으로 평가되어 메모리 어딘가에 저장된 후 전달된다.
- 함수코드의 평가 시작, 함수실행컨텍스트를 생성 후 실행컨텍스트 스택에 push하며 현재 실행중인 실행컨텍스트가 된다.
- 매개변수와 지역 변수, 함수선언문이 실행되어 함수 실행컨텍스트의 렉시컬환경에 식별자가 등록된다
- foo 함수 코드가 실행되며 할당문이 실행되고,
console.log
를 만나 console이라는 식별자를 현재 실행중인 실행컨텍스트에서 찾는다. - 없으면 상위 스코프인 전역 실행컨텍스트로 옮겨가 찾는데, console은 전역 실행컨텍스트의 렉시컬환경의 객체환경레코드에서 찾을 수 있다.
- 객체환경레코드는
bindingObject
라는 하나의 프로퍼티만 갖고 있는 객체로, 이 프로퍼티의 값은 전역객체를 가리킨다.
- 객체환경레코드는
- console 객체의 메서드인 log를 찾아 인수로 넘어온 표현식을 평가한 값을 전달하며 호출한다.
- 이 때,
console.log
도 함수이니 당연히 실행컨텍스트를 만들어 실행한 후 함수 종료와 함께 실행컨텍스트 스택에서 pop된다.
- 이 때,
- foo 함수의 모든 실행문이 끝났으므로 종료되면서 실행컨텍스트 스택에서 pop된다.
- 전역 실행 컨텍스트로 돌아와
console.log
에서 전역변수를 넘기며 함수를 호출한 뒤 전역 실행 컨텍스트도 실행 컨텍스트 스택에서 pop
- 실행 컨텍스트에는 기본적으로 두개(Lexical Environment, Variable Environment)의 component가 있다.
- 생성 초기에는 둘 다 같은 것을 가리키지만 with문 등에 의해 나중엔 다른 것을 가리킨다. 그러나 우리가 거기까지 알 필요는 없으니 하나라고 생각하자.
- 전역 실행 컨텍스트는 전역 렉시컬 환경 객체를 가리킨다. 이 객체는 전역 환경레코드와 외부 렉시컬 환경에 대한 참조를 프로퍼티로 갖는다.
- 전역환경레코드는 특별히 객체환경레코드와 선언적 환경레코드, GlobalThis를 프로퍼티로 갖는다.
- 객체환경레코드의 유일한 프로퍼티인 BindingObject에는 전역객체가 바인딩되어있다.
- 선언적 환경레코드에는 let, const로 선언된 식별자가 등록된다.
- GlobalThis 또한 전역객체를 가리킨다.
- 외부 렉시컬 환경에 대한 참조는 전역의 경우 null 값을 갖는다.
- 전역환경레코드는 특별히 객체환경레코드와 선언적 환경레코드, GlobalThis를 프로퍼티로 갖는다.
- 함수 실행 컨텍스트도 렉시컬 환경이라는 객체를 가리킨다. 이 객체 또한 환경레코드와 외부 렉시컬 환경에 대한 참조, this를 프로퍼티로 갖는다.
- 환경레코드에는 함수의 매개변수, arguments, 함수 몸체에 선언된 지역변수와 함수가 식별자로 등록된다.
- 함수객체가 생성될 때 세팅된
[[Environment]]
내부슬롯에 바인딩된 렉시컬 환경이 함수의 외부 렉시컬환경에 대한 참조 값으로 들어간다. - this는 호출시 동적으로 결정된다.
- 전역객체는 가장 먼저, 전역 코드 평가 이전에 생성된다.
- 전역코드가 실행되기 이전의 태초와, 전역코드 실행 종료시 전역 렉시컬 환경의 객체환경레코드(BindingObject)도, GlobalThis가 사라졌을 때에는 아무도 전역객체를 가리키지 않는데, 그럼 garbage collector의 대상이 되는 것인가?
- 전역객체는 예외이다. 태초부터 끝까지 살아있어야만 한다.
- 전역코드가 종료되고 전역객체가 살아있지 않다면, 브라우저에 걸어둔 이벤트 핸들러는 다 전역객체에 있는데 못쓰게 된다.
- 소스코드 4가지(전역, 함수, 모듈, eval)는 왜 구분되어 있을까?
- 이들이 실행컨텍스트를 만드는 특별한 케이스들이기 때문에.
- var로 선언된 변수는 함수레벨 스코프를 갖지만, let, const로 선언되는 변수는 블록레벨 스코프를 갖는다.
- if문 안에 let이나 const로 변수를 선언하면, 블록 렉시컬 환경이 생성되고 현재 실행중인 실행컨텍스트의 Lexical Environment는 이 블록 렉시컬 환경을 가리킨다.
- 블록 렉시컬 환경의 외부 렉시컬 환경 참조는 현재 실행중인 실행컨텍스트가 가리키던 Lexical Environment 객체를 가리키고 있으며, 블록문이 종료되면 이를 통해 현재 실행중인 실행컨텍스트의 Lexical Environment가 원래대로 돌아온다.
- 블록문은 실행컨텍스트를 만들지 않는다. 렉시컬 환경만을 만들 뿐이다.
- for문을 돌리면 렉시컬 환경이 여러개 생기므로 나누어 돌려야 한다.
- 함수형 프로그래밍 언어라면 클로저를 다 지원한다.
- 함수형 언어에서, 함수는 객체이다. 그렇기 때문에 프로퍼티를 가질 수 있으며, 이를 통해 상위 스코프 저장이 가능하므로 클로저가 기본으로 제공된다.
- 클로저는 함수와 그 함수가 선언된 렉시컬 환경(=상위 스코프)의 조합이다.
- 전역에서 함수 객체가 만들어질 때는, 전역 실행컨텍스트가 현재 실행중인 실행 컨텍스트일 때이다.
- 그 때의 렉시컬환경은 즉 함수가 선언된 위치의 렉시컬환경이며 함수는 이를 상위스코프로 삼는다.
- 클로저는 이렇듯 자신의 스코프와 함께 상위 스코프를 갖는 함수이다.
- 그러나 이 의미라면 자바스크립트의 모든 함수는 클로저이다. 왜냐면 모든 함수는 내부슬롯
[[ Environment ]]
를 가지기 때문이다.
- 일반적인 의미의 클로저는 그러나 그보다 더 좁은 의미를 갖는다.
- 외부함수의 반환값으로 내부함수가 리턴된다는 전제 (내부 함수가 외부 함수보다 오래 살아남는 경우) 아래, 내부함수가 외부함수에서 선언된 식별자를 바라보는 경우
- 내부함수가 외부함수가 갖는 식별자를 바라보지 않는 경우, 외부함수의 렉시컬 환경을 참조하고 있다고 하더라도 자바스크립트 엔진이 최적화를 위해 다 날려버리기 때문에 클로저라고 하지 않는다.
- 클로저인 경우에도 모던 브라우저에서는 내부함수가 참조하지 않는 식별자를 날려버린다.
- 디버거에서 말하는 특정 함수의 Closure는 그 함수가 바라보는 외부함수의 렉시컬환경, 즉 상위스코프를 말하며 이 때 참조하고 있는 식별자를 보여준다.
- 상태를 안전하게 변경하고 유지하기 위해 사용한다.
- 전역에 선언된 num 변수를 increase라는 함수로 조작하는 경우, 전역변수라는 것 외에도 외부 환경에 의존하며 부수효과를 낳는다는 점에서 바람직하지 않다.
- 그렇다고 객체에 넣어 increase 메서드로 조작한다고 하더라도 객체는 public이기 때문에 외부에서 조작이 가능하다.
- increase라는 함수를 통해서만 num을 변경할 수 있도록 할 때, num을 조작하는 내부함수를 선언하며 동시에 지역변수로 num을 선언 및 할당하는 외부함수를 즉시실행함수로 만든다. 내부함수를 리턴하는 즉시실행함수 실행의 표현식을 increase 변수에 할당한다.
- 두개 이상의 작업을 하게 하고 싶으면, 함수를 두개 이상 선언하고 해당 함수를 객체에 담아 전달하며 메서드로 호출한다.
- increase의 상위스코프는 즉시실행함수이며, 즉시실행함수의 몸체에 선언된 num은 increase 외에는 접근하지 못하므로 안전하게 유지된다.
- 몇 번의 시도 끝에 실행컨텍스트와 클로저 드디어 완벽히 이해했다. 다시 수업듣길 잘했다.