[ Javascript ] 함수 호이스팅, 함수 선언문과 표현식, 함수 리터럴

2024. 7. 28. 20:12프로그래밍/JavaScript

개요

자바스크립트에서 함수의 개념은 무엇이고 자바스크립트 엔진에서 함수가 어떻게 할당되고 선언되고 동작하는지에 대해 알아본다.

 

 

자바스크립트에서 함수란?

자바스크립트에서 함수는 일급 객체로 분류된다. 여기서 중요한 것은 함수도 결국은 객체로 이루어져 있다는 것이다. 

일급 객체는 객체가 값으로 사용될 수 있음을 뜻한다. 함수는 객체의 프로퍼티에 위치할 수도 있고 변수에 할당될 수도 있으며 배열의 요소가 될 수도 있는 것이다. 

 

함수를 호출할 수 있는 원리

우리는 당연하게도 함수를 작성하고 나서 해당 함수의 이름 뒤에 소괄호를 붙여 함수를 호출한다.

그러나 원래는 우리가 함수를 작성할때 function옆에 입력했던 함수 이름은 함수 내부에서만 사용할 수 있는 식별자이다.

따라서 외부에서는 해당 함수를 식별하는 식별자가 없기 때문에 호출할 수가 없다.

하지만 자바스크립트 엔진이 암묵적으로 해당 함수를 전역적으로 호출할 수 있게끔 식별자를 생성해주기 때문에 정상적으로 동작하는 것이다.

조금 더 자세히 말하자면 런타임 이전에 해당 함수 리터럴에 해당하는 함수 식별자(함수 이름)를 암묵적으로 생성한 후 할당하는 것이다. 

 

아래 설명을 보다 보면 조금씩 이해가 될 것이다.

 

함수의 생성 시점?

자바스크립트에서는 일반 변수와 마찬가지로 함수가 초기화되고 할당되는 시점이 다르다.

함수를 생성하는 방법에는 두가지 방법이 있는데 
첫번째로는 함수 선언문 두번째로는 함수 표현식이다.

또한 자바스크립트에서는 java의 람다와 같이 이름이 없는 함수를 가질 수 있는데 이를 익명함수라 한다.

함수를 선언할때는 익명함수로 작성해야하고 함수 표현식을 이용할때는 익명함수, 기명함수 두가지 방식 모두 사용 가능하다.

 

함수 선언문은 일반적으로 알고 있는 함수 선언문과 같다.

// 함수 선언문
function test(){
	...
}

 

함수 표현식은 아래와 같이 함수 리터럴을 변수에 할당하는 표현 방식이다.

// 함수 표현식
var test = function add(){
	...
}

// 함수 표현식에는 기명 함수와 익명 함수 두가지 방법 모두 사용할 수 있다.
var test = function(){
	...
}

var test = ()=>{
	...
}

 

그렇다면 var 키워드를 가진 변수 타입은 런타임 이전에 자바스크립트 엔진이 암묵적으로 undefined로 초기화하는 과정을 거친다고 했으니 함수의 생성 시점에도 분명차이가 있을 것이다. 

 

함수 선언문의 경우

함수 선언문을 이용하여 함수 선언을 했을 경우에는 위에서 언급했듯 암묵적으로 함수에 해당하는 식별자를 생성한다고 했다. 따라서 var키워드로 변수를 선언한 것과 비슷하게 동작한다. 결론적으로는 아래와 같이 함수 호이스팅이 가능하다.

// 함수 호이스팅에 의해 선언 이전에 함수 호출이 가능
console.log(print()); // "Hello World!"가 출력됨

function print(){
	return "Hello World!";
}

 

* var키워드의 호이스팅과는 다른 부분이 있다면 바로 런타임 시점에 할당 여부를 결정하냐 하지 않느냐 이다. var 키워드로 선언한 변수는 런타임 이전에 암묵적으로 undefined로 초기화 되기는 하지만 할당은 런타임 시점에 이루어진다. 그러나 함수 호이스팅의 경우 런타임 이전에 함수 리터럴을 생성했고 선언문의 함수 이름에 해당하는 식별자를 생성했기 때문에 완벽하게 동작한다. 

 

함수 표현식의 경우

그렇다면 함수 표현식으로 선언한 함수의 경우는 어떻게 동작할까? 앞서 살펴본 내용을 바탕으로 생각해본다면 동작을 예측해볼 수도 있다.

바로 함수 표현식으로 선언할 경우 변수 자체에 값으로써 함수 리터럴을 할당하는 것이므로 변수의 호이스팅은 이루어지지만 함수의 호이스팅은 이루어지지 않는다.

그 이유는 리터럴로 표현된 함수 리터럴은 런타임 이전에 함수 객체를 생성하고 식별자를 할당하는 것이 아니기 때문이다.

// 함수 표현식의 경우 함수 호이스팅이 일어나지 않고 변수 호이스팅만 일어남
console.log(print); // undefined가 출력됨
console.log(print()); // TypeError: print is not a function 에러 발생

var print = function myPrint(){
	return "Hello World!";
}

 

위 예제의 출력 결과를 확인해보면 print라는 변수에 대해서만 호이스팅이 발생했고 리터럴로 선언한 myPrint라는 함수에 대해서는 호이스팅이 발생하지 않은 것을 알 수 있다. 

 

또 하나 재밌는 사실은 아까 설명했듯 함수 선언문이 아닌 함수 표현식의 경우에는 선언한 변수명이 함수를 호출하는 식별자가 된다. 따라서 함수 리터럴에 선언한 myPrint()를 호출하게 되면 참조 에러가 발생하게 된다.

// 함수 표현식의 경우 함수 호이스팅이 일어나지 않고 변수 호이스팅만 일어남
console.log(print); // undefined가 출력됨
console.log(print()); // TypeError: print is not a function 에러 발생

var print = function myPrint(){
	return "Hello World!";
}

print(); // 정상동작
myPrint(); // 에러 발생

 

그렇다면 형식적으로 선언된 myPrint 함수 리터럴 내부에서 myPrint함수를 호출하는 것은 어떻게 동작할까?

 

// 변수명으로 할당된 식별자를 호출
var print = function myPrint(){
	print();
	return "Hello World!";
}

// 함수명으로 함수 내부적으로만 할당된 식별자를 호출
var print = function myPrint(){
	myPrint();
	return "Hello World!";
}

 

결과를 보기전에 미리 예측해보기 바란다. 

위와 같이 기명함수로 함수를 생성하게 되면 해당 함수의 이름은 함수 내부에서만 인식할 수 있다고 했다. 또한 함수 선언문으로 생성한 것이 아니라 함수 표현식이라면 외부에서는 해당 함수의 이름을 갖고 함수를 호출할 수 없다.

그러나 내부에서 서로 다른 식별자를 호출했을 때의 결과는 둘 다 정상 동작한다 이다.

더 정확히 말하면 위 코드를 기준으로는 스택 오버 플로우 에러가 발생한다. 마지막 예제를 이해했다면 본 포스팅의 글을 어느정도 이해했다고 봐도 될 것 같다.