Programming Languages

[JavaScript] 자바스크립트는 어떻게 돌아가는가?

Sara.H 2020. 8. 4. 16:29

거시적인 관점

프로그래머가 코드를 작성하고 실행하면 자바스크립트 언어를 해석할 수 있는 엔진이 이를 해석해서 기계어로 번역한다. 흔히 알고 있는 브라우저들도 엔진이 있고, 여러 다른 엔진들이 존재한다. 엔진 안에서는 Parser 가 실행되어서 코드들을 갖고 Abstract Syntax Tree 를 만들어내고, 이를 기계어로 변환한다. 변환된 후 컴퓨터에서 코드가 실행된다. 엔진의 구체적인 작동 방식은 엔진마다 차이가 있다. 

 

좀 더 자세히 알아보자. 

 

Execution Context, Execution Stack 

자바스크립트 코드는 항상 어떤 특정한 환경 안에서 돌아간다. 이 환경을 Execution Context 라 부른다. 기본적인 환경은 Global Execution Context 이다. 함수들에 대한 포인터, 글로벌로 선언된 변수 등이 이 안에 담긴다. 대표적인 예로 브라우저 안에서 window 객체가 글로벌 객체와 연관된다. 만약 `lastName === window.lastName` 이라고 하면 true 가 반환되는 것에서 이를 확인할 수 있다. 

 

함수가 호출되면 해당 함수가 포함하고 있는 변수와 다른 함수에 대한 포인터들은 Global Execution Context 위에 스택으로 쌓여나간다. 이를 Execution Context 라 한다. 함수의 실행이 끝나고 나면 Context 는 스택에서 사라진다. 그렇다면 Execution Stack 은 어떻게 만들어지고, 실행되는 걸까? 

 

Execution Context 는 하나의 객체와 연관지을 수 있다. 이 안에는 Variable Object (VO), Scope Chain, `this` variable 이 담겨져 있다. 함수가 호출되면 실행 스택에 Context 가 만들어지고, 실행되는데 이 과정을 크게 1. Creation Phase 와 2. Execution Phase 로 나눈다. Creation phase 에서는 VO가 생기고, Scope Chain이 만들어지며 변수 this 의 값이 결정된다. Execution Phase 에서는 현재의 Execution Context 를 만들어 낸 함수의 코드가 line by line 으로 실행된다. 

 

Creation Phase 

1. VO가 생성될때는 argument object 가 만들어진다. 이 객체에는 함수에 전달된 매개변수들이 포함된다. 함수 안의 코드에는 크게 다른 함수에 대한 포인터와 함수 내에서 선언된 로컬 변수들이 있다. 이들을 인식하기 위해 함수 안의 코드들을 스캔하는 작업을 하게 된다. 함수 내부에 선언된 각 함수마다 VO에 Property 가 생기고, 이 Property에는 다른 함수에 대한 포인터가 할당된다. 그리고 각 변수에 대해서도 마찬가지로 Property 가 형성되는데 값은 아직 할당되지 않는다. 값이 할당되는 것은 Execution Phase 임을 주의하자. Creation Phase 에서 로컬 변수들은 undefined 이다. 이렇듯 함수 안의 함수들과 변수들을 스캐닝하고, 값을 할당 (혹은 undefined 로 지정하는) 하는 작업을 Hoisting이라 한다. 

 

Hoisting in Practice 

calculateAge(1995); //선언 전에 적어도 오류가 나지 않는다. 
//코드가 실행되기 전에 (Creation Phase) Variable Object 에 함수에 대한 포인터가 저장된다. 

function calculateAge(year){
  console.log(2020 - year); 
}
//코드가 실행될 때 (Execution Phase) 함수를 사용할 수 있게 된다. 

 

 

retirement(1990); 

var retirement = function(year){ 
  console.log(65 - (2016-year)); 
}

 

* 위의 예제에서 var retirement 는 Function Declaration 이 아니라, Function Expression 이기 때문에 var 선언 위에서 이를 호출하면 오류가 난다. (TypeError : retirement is not a function) 즉, Hoisting 은 Function declaration 에서만 적용된다는 것을 알 수 있다. 

 

console.log(age); //오류
var age = 23; 
console.log(age); //OK 

function foo(){
  var age = 65; 
  console.log(age); //65
}

foo(); 
console.log(age); //23 

* VO 의 생성 단계에서 age 는 undefined 이다. (값이 없는 상태일 뿐, 오류가 나지는 않는다.)

* 23 이 할당된 맨 위의 age 는 Global Execution Context 안에 포함된다. 하지만 foo 안의 age 의 경우 또 다른 Execution Context 안의 변수이기 때문에 두 개는 서로 다른 변수이다. 따라서 둘의 출력 결과도 다르다. 

 

2. Creation of the Scoping Chain 

Scoping 이란 특정 변수를 어디서 우리가 접근할 수 있는지에 대한 것이다. 각 함수들은 새로운 Scope 를 형성하고, 그 안에서 정의된 변수들은 해당 함수 안에서만 접근이 가능하다. 

 

스코프 체인은 함수가 코드에 작성된 순서에 따라서 결정된다. 

 

3. `this` 키워드 

this 키워드는 각 excution context 마다 할당된다. 일반적인 함수 호출에서 this 는 글로벌 객체를 가리킨다. (window Object). method call 에서 this 변수는 method 를 호출하는 객체를 가리킨다. this 키워드는 그것이 정의된 함수가 실제로 호출되기 전까지 값이 할당되지 않는다. 

var johnObj = {
  name : 'John', 
  yearOfBirth : 1990, 
  calculateAge : function(){
    console.log(this); 
  }
} 
john.calculateAge(); //Object{name : ... , yearOfBirth : ...} 가 this의 출력 결과로 나온다. 

var mike = {
  name : 'Mike', 
  yearOfBirth : 1984
}
mike.calculateAge = john.calculateAge; 
mike.calculateAge(); //Mike 객체에서 호출이 되었으므로 this 의 출력 결과로 Mike 객체가 나온다.