실행 컨텍스트
소스코드의 타입
ECMAScript는 소스타입을 다음과 같이 구분. 각각의 소스코드는 실행 컨텍스트를 생성한다.
전역 코드 | 전역에 존재하는 소스코드 | 전역에 정의된 함수, 클래스 등 내부 코드 포함 X |
함수 코드 | 함수 내부에 존재하는 소스코드 | 함수 내부에 중첩된 함수, 클래스 등 내부 코드 포함 X |
eval 코드 | 빌트인 전역함수인 eval함수에 인수로 전달되어 실행되는 소스 코드 | |
모듈 코드 | 모듈 내부에 존재하는 소스코드 | 모듈 내부의 함수, 클래스 등 내부 코드 포함 X |
전역 코드
- 전역 변수를 관리하기 위해 존재
- var키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩하기 위해 전역 객체로 연결
함수 코드
- 지역 스코프를 생성하고 지역 변수, 매개변수, arguements 객체를 관리
- 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결
eval 코드
- strict mode(엄격 모드)에서 자신만의 독자적인 스코프 생성
모듈 코드
- 모듈별 독립적인 모듈 스코프 생성
소스코드의 평가와 실행
모든 소스코드는 실행에 앞서 평과 과정을 통해 실행하기 위한 준비를 한다.
- 소스 코드의 평가 과정
- 실행 컨텍스트를 생성
- 변수, 함수 등의 선언문만 먼저 실행
- 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프에 등록
- 선언문을 제외한 소스코드가 순차적으로 실행 (런타임)
- 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색해 취득
- 소스 코드의 실행 결과를 스코프에 다시 등록
❓ 실행 컨텍스트란 무엇인가? 어떻게 동작되고, 어떻게 생겼는지
실행 컨텍스트의 역할
코드가 실행되기 위해선 스코프, 식별자, 코드 실행 순서 등의 관리가 필요
👉 코드가 실행되기 위한 모든 것을 관리하는 실행 컨텍스트
식별자와 스코프 - 렉시컬 환경으로 관리
코드 실행 순서 - 실행 컨텍스트 스택으로 관리
⚡ 실행 과정
1. 전역 코드 평가
// 전역 변수 선언
const x = 1;
const y = 2;
// 함수 정의
function foo(a){
// 지역 변수 선언
const x = 10;
const y = 20;
// 메서드 호출
console.log(a + x + y);
}
// 함수 호출
foo(100);
// 메서드 호출
console.log(x + y);
전역 코드의 선언문만 먼저 실행
var 키워드로 선언된 전역변수와 함수 선언문으로 정의된 전역 함수는 전역 객체의 프로퍼티와 메서드가 됨.
// 📔 전역스코프
x // uninitialized
y // uninitialized
foo // undefined
2. 전역 코드 실행
런타임이 시작되어 전역코드가 순차적으로 실행
전역 코드 실행값이 할당되고 함수가 호출
함수 호출 시 순차적으로 실행되던 전역 코드의 실행을 일시 중단하고 함수 내부로 진입
// 📔 전역스코프
x // 1;
y // 2;
foo // function -> 함수 안으로 진입
3. 함수 코드 평가
함수 코드를 실행하기 위한 준비 : foo의 실행 컨텍스트 생성
선언문 실행
👉 매개변수와 지역 변수가 지역 스코프에 등록
👉 argument 객체가 생성
👉 this 바인딩 결정
// 📄 foo의 지역스코프
x // uninitialized
y // uninitialized
4. 함수 코드 실행
매개변수와 지역변수에 값이 할당
// 📄 foo의 지역스코프
x // 10
y // 20
console.log를 찾기 위해 스코프 체인을 타고 전역까지 올라간다.
실행 컨텍스트 스택 - 코드 실행 순서
const a = 1;
function x() {
const b = 2;
function y() {
const c = 3;
console.log(a + b + c);
}
function z() {
const d = 4;
console.log(a + b + d);
}
y();
console.log("check")
z();
}
x();
stack 자료구조형태로 관리되며,
최상위에 존재하는 실행컨텍스트는 언제나 현재 실행 중인 코드의 실행 컨텍스트.
렉시컬 환경 - 스코프, 식별자
스코프란?
자신이 선언된 위치에 따라 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위
👉 식별자가 참조될 수 있는 범위
let으로 변수 선언자를 바꾼다면?
👉 var와 달리 let var2는 전역으로 선언되지 않았으므로 ReferenceError 출력
식별자를 검색할 때 사용하는 규칙 (식별자 결정)
var x = 'global';
function foo(){
var x = 'local';
console.log(x);
}
foo(); // local
console.log(x); // global
자바스크립트 엔진은 코드를 실행할 때 코드의 문맥을 고려하는데 이 정보에 따라 식별자가 어떤 변수를 의미하는 지 결정된다.
💡 코드가 어디에서 실행되며 주변에 어떤 코드가 있다라는 정보 = 렉시컬 환경
코드의 문맥은 렉시컬 환경으로 이뤄진다.
네임 스페이스
식별자는 어떤 값인지 구별할 수 있어야 하므로 유일해야 한다. 따라서 값은 유일한 식별자에 연결되어야 하는데, 이를 네임 바인딩이라고 한다.
네임 바인딩은 스코프에 의해 결정된다. 스코프 내에서 식별자는 유일해야하지만 다른 스코프에서는 같은 이름의 식별자를 사용할 수 있다.
var ↔️ let, const
var 키워드는 같은 스코프 내에서 중복 선언 가능
let, const 키워드는 중복 선언 불가능
렉시컬 환경 : 식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 자료구조
키와 값을 갖는 객체형태의 스코프를 생성해 식별자를 키로 설정, 식별자에 바인딩된 값을 관리
실행 컨텍스트는 lexical environment와 variable environment 으로 구성
Variable Environment
Lexical Environment와 비슷하지만 최초 실행시의 스냅샷을 유지한다는 점이 다르다.
실행 컨텍스트 생성 시 Variable Environment에서 해당 정보를 먼저 담은 다음, 이를 그대로 복사해 Lexical Environment를 생성합니다. Variable Enviroment는 변하지 않은 채로, 복사한 Lexical Environment 값을 사용하게 된다.
👉 var로 선언된 식별자들의 값을 저장하는데 사용
Lexical Environment
environment record
식별자들을 연관된 렉시컬 환경 범위 안의 값들 맵핑시켜주는 구조
소스코드의 타입에 따라 관리하는 내용에 차이가 존재
함수 안에 선언된 변수는 Environment Record에 저장될 뿐만 아니라, arguments에 유사배열객체 안에도 저장된다.
👉 let과 const으로 선언된 식별자들의 값을 보관하는 곳
outer reference
부모 렉시컬 환경의 참조값을 가짐.
= JS엔진이 최근 렉시컬 환경을 찾지 못하면 외부 환경의 내부에 있는 변수를 볼 수 있도록 해줍니다.
global context에서 outer reference == null
function context에서 outer reference == 부모의 lexical 환경 (글로벌이나 다른 함수 실행 컨텍스트)
렉시컬 스코프
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x); //1
}
foo();
bar();
bar함수의 상위 스코프가 무엇인가에 따라 출력값이 결정된다.
- 함수를 어디서 호출했는지 → 동적 스코프
- 함수를 어디서 정의했는지 → 렉시컬 스코프 / 정적 스코프
자바스크립트는 렉시컬 스코프를 따르고 있다. 함수 정의가 실행되어 생성된 객체는 호출될 때마다 함수의 상위 스코프를 따르기 때문에 실행 컨텍스트에서 상위 스코프를 기억한다.
스코프의 종류
- 전역 스코프 (코드의 가장 바깥 영역) - 어디서든 참조 가능
- 지역 스코프 (함수 몸체 내부)
- 지역 = 함수 몸체 내부
- 자신의 지역 스코프와 하위 지역 스코프에서 유효
이미 선언 됐던 x가 global x가 아닌 inner x를 보여주는 이유는
👉 자바스크립트 엔진이 스코프 체인을 통해 참조할 변수를 검색했기 때문
let 사용 결과
같은 스코프가 아니기 때문에 x 재선언 가능
스코프 체인
함수가 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있다. 중첩되는 스코프는 계층적 구조를 갖게 된다.
이렇게 계층적으로 연결되는 것을 스코프 체인이라고 하는데, 실행 컨텍스트의 렉시컬 환경을 단방향으로 연결한 것이다.
변수 선언이 실행되면 변수 식별자가 키로 등록이 되고, 변수 할당이 일어나면 해당 식별자에 해당되는 값을 변경한다.
스코프 체인에 의한 변수 검색
inner에서 x 검색
- inner의 렉시컬 환경에서 x 검색
- 찾음! → 검색 종료
inner에서 y 검색
- inner의 렉시컬 환경에서 y 검색
- 없음
- outer 스코프에 저장된 스코프의 렉시컬 환경 탐색
- 찾을 때까지 2,3 반복
해당하는 식별자를 찾으면 종료
상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조 가능하지만, 하위 스코프에서 유표한 변수를 상위 스코프에서 사용하는 것은 불가능
스코프 체인에 의한 함수 검색
- 함수 선언문으로 함수 정의 시, 런타임 이전에 함수 객체가 생성
- 자바스크립트 엔진이 함수 이름과 동일한 식별자를 암묵적으로 선언하고 생성된 함수 객체 할당
- 함수도 식별자에 해당하기 때문에 변수와 마찬가지로 스코프가 적용된다.
This binding
this는 자신을 불러온 객체를 가르킵니다. 실행 컨텍스트 활성화 당시, this가 지정되지 않은 경우(자신을 불러온 객체가 없는 경우) this에는 전역 객체가 저장됩니다. 하지만 arrow function을 사용하게 되면, 부모의 객체를 가르키게 됩니다.
function Coffee(){
this.menu = 'latte';
return {
menu : 'milk',
drink : function () {
console.log(this.menu) // milk
}
}
}
여기서는 this가
{
menu : 'milk',
drink : function () {
console.log(this.menu) // milk
}
}
여기서 호출하는 걸로 쓰입니다.
but, arrow function을 사용하게 되면
function Coffee(){
this.menu = 'latte';
return {
menu : 'milk',
drink : () => {console.log(this.menu)} // latte
}
}
여기선 부모 요소인 latte가 this 값이 됩니다.
global context
글로벌 컨텍스트에서 this는 구동환경에 따라 달라집니다.브라우저에서 전역객체는 window 객체를 의미합니다.
반면 Node.js(서버)상에서 전역객체는 global 객체를 의미합니다.
이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 분류됩니다.
스택에 아무것도 없는 상태에서 호출되기 때문에 최상단 노드인 window가 글로벌 컨텍스트를 불러낸 객체가 됩니다.
👀 next.js는 ssr 방식으로 동작하기 때문에 글로벌 객체가 global이 사용됩니다.(window 사용 불가)
function context
this = 함수를 호출한 객체
실행 컨텍스트의 생성과 식별자 검색 과정
전역 객체 생성
전역 코드가 평가되기 이전에 생성
전역 객체에 다음 요소들 포함
- 빌트인 전역 프로퍼티
- 빌트인 전역 함수
- 표준 빌트인 객체
- 동작 환경에 따른 호스트 객체 및 WebAPI 등등..
Object.prototype 상속 → 전역 객체도 프로토타입 체인의 일원
window.toString(); // [object Window]
window.__proto__ === Object.prototype; // false
window.__proto__.__proto__.__proto__.__proto__ === Object.prototype; // true
궁금해서 찍어본 윈도우 던더프로토
전역 코드 평가
var x = 1;
const y = 2;
function foo(a){
var x = 3;
const y = 4;
function bar(b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);
1. 전역 실행 컨텍스트 생성
비어있는 전역 실행 컨텍스트를 생성해 실행 컨텍스트 스택에 푸시
2. 전역 렉시컬 환경 생성
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩
(환경 레코드 , 외부 렉시컬 환경에 대한 참조)
2-1. 전역 환경 레코드 생성
전역 변수를 관리하는 전역 스코프, 전역 객체의 빌트인 전역 프로퍼티, 빌트인 전역 함수, 표준 빌트인 객체 제공
- 객체 환경 레코드 생성
👉 var로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수, 전역 객체의 빌트인 전역 프로퍼티, 빌트인 전역 함수, 표준 빌트인 객체
- BindingObject 객체를 통해 전역 객체의 프로퍼티와 메서드가 될 수 있다.
- 전역 객체를 가리키는 식별자 없이 전역 객체의 프로퍼티를 사용할 수 있는 매커니즘
var x = 1;
const y = 2;
function foo(a){ ... }
- 선언적 환경 레코드 생성
👉 let, const로 선언한 전역 변수
선언 단계와 초기화 단계가 분리되어 진행
두 단계의 간극 = 일시적 사각지대 (TDZ)에 빠지게 된다.
이 때 uninitialized는 변수에 접근할 수 없음을 나타내기 위해 사용된 표현으로 실제로 uninitialized라는 값이 바인딩된 게 아니다.
2-2. This 바인딩
전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this 바인딩
this 바인딩은 전역 환경 레코드와 함수 환경 레코드에만 존재
2-3. 외부 렉시컬 환경에 대한 참조 결정
현재 소스코드를 포함하는 외부 코드의 렉시컬 환경 (상위 스코프)를 통해 단방향 링크드 리스트인 스코프 체인을 구현
전역에서는 Null을 가짐
전역 코드 실행
전역 코드가 순차적으로 실행되기 시작하는 단계
이전까지 선언문만 실행되었고, 변수 할당문이 실행되어 값이 식별자에 할당된다.
foo 함수가 실행이 되고, foo 식별자를 찾기 위해 식별자 검색을 시작한다
현재 실행중인 실행 컨텍스트에서 해당 식별자를 검색한 뒤, 없다면 상위 스코프를 확인한다.
전역까지 확인한 결과, 식별자가 존재하지 않는다면 Reference Error를 반환한다.
foo 함수 코드 평가
foo 함수가 실행되는 순간, 기존 전역 코드 실행이 중단되고 제어권이 foo 함수로 이동한다.
- 함수 실행 컨텍스트 생성
- 함수 렉시컬 환경 생성
- 함수 환경 레코드 생성
- 매개변수, arguments객체, 함수 내부에서 선언한 지역 변수와 중첩 함수 등록 및 관리
- this 바인딩
- [[ThisValue]]에 바인딩 - 함수 호출 방식에 따라 결정된다.
- 외부 렉시컬 환경에 대한 참조 결정
- [[Environment]]에 상위 스코프 바인딩
- 함수가 어디서 정의되었는지에 따라 결정된다.
foo 함수 코드 실행
식별자 결정을 위해 실행 중인 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색 후, 값을 바인딩하는 단계
bar 함수 코드 평가
bar 함수 코드 실행
- console 식별자 검색
- log 메서드 검색
- 표현식 a + b + x + y + z 평가
- console.log 메서드 호출
bar 함수 코드 실행 종료
더이상 실행할 코드가 없으므로 bar 함수 코드의 실행이 종료된다.
실행컨텍스트에서 bar 함수 실행 컨텍스트가 사라지고 foo 함수 코드가 이어서 실행된다.
실행 컨텍스트의 소멸 ≠ 렉시컬 환경의 소멸
누군가에 의해 참조되고 있다면 bar의 렉시컬 환경은 소멸되지 않고 남아있음 → 클로저 실행 원리
foo 함수 코드 실행 종료
더이상 실행할 코드가 없으므로 foo 함수 코드의 실행이 종료된다.
실행컨텍스트에서 bar 함수 실행 컨텍스트가 사라지고 전역 코드가 이어서 실행된다.
전역 코드 실행 종료
더 이상 실행될 전역코드가 없으면 전역 코드의 실행이 종료되고, 전역 실행 컨텍스트도 실행 컨텍스트 스택에서 사라진다.
실행 컨텍스트와 블록 레벨 스코프
함수 레벨 스코프
코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성
블록 레벨 스코프 | 함수 레벨 스코프 |
함수 뿐만 아니라 모든 코드 블록이 지역 스코프 생성 | 오로지 함수의 코드 블록만을 지역 스코프로 인정 |
let, const | var |
⛔ if문 안에서 선언되었더라도 함수 내부가 아니므로 var 2, var3는 전역변수
🚫 for문에서 다음과 같이 의도치 않게 전역변수의 값이 재할당될 수 있음
블록 레벨 스코프
let x = 1;
if (true){
// if문이 실행되면 새로운 렉시컬 환경을 생성해 기존의 렉시컬 환경 교체
let x = 10;
console.log(x); // 10
}
// if문 코드 블록의 실행이 종료되면 if문의 코드 블록이 실행되기 이전의 렉시컬 환경으로 돌아간다.
console.log(x); // 1
'모던 자바스크립트 Deep Dive' 카테고리의 다른 글
04장 변수, 05장 표현식과 문 (0) | 2022.05.16 |
---|
Comment