자바스크립트에서 객체를 생성하는 방법은 다음과 같이 세 가지가 있습니다.
1. 객체 리터럴 사용
2. 생성자 함수 사용
3. Object.create()사용
이번 포스트에서는 객체 리터럴과 함께 가장 많이 사용되는 객체 생성 방법인 생성자 함수에 대해서 알아보는 시간을 가지겠습니다.
들어가기 앞서, 생성자 함수를 통해 객체를 생성할 때 가지는 이점에 대해 이야기해 보겠습니다.
객체 리터럴로 만드는 객체가 더 간편하고 편한 방법같아 보이지 않나요? 왜 굳이 추가적으로 함수를 만들고 this.과 같은 귀찮은 문법을 사용해서 객체를 만들어야 할까요?
객체 지향 프로그래밍의 상속 개념을 활용할 수 있다는 점도 있지만, 여기서는 조금 더 쉽고 직관적인 이유에 대해서 알아보겠습니다.
예시를 들어 설명해볼텐데요, 자동차라는 객체를 만들어 봅시다. 우리가 만들어볼 자동차 객체는 제조사와 색상을 프로퍼티로 가지고, 주행이라는 메서드를 가집니다.
일단 객체 리터럴로 만들어 볼게요.
const car = {
brand: 'BMW',
color: 'red',
drive(){
console.log('driving!');
}
};
console.log(car);
그리고 비교를 위해 생성자 함수로도 생성해 봅시다.
function Car(brand, color) {
this.brand = brand;
this.color = color;
this.drive = function(){
console.log('driving!');
}
}
const car = new Car('BMW', 'red');
console.log(car);
우선은 객체 리터럴 사용법이 훨씬 간편해 보입니다.
이제 차를 두 방법으로 10개 더 만들어 볼까요? 객체 리터럴로 만드려고 하는 순간 벌써 지치려고 합니다. 이렇듯 생성자 함수로 객체를 생성하는 방식의 장점은 동일한 프로퍼티와 메서드를 가지는 객체를 편하게 여러번 생성할 수 있다는 장점이 있습니다.
그런데 한 가지 의문점이 생깁니다. 함수를 호출할 때 new키워드를 사용했는데, 이게 뭘까요? 그리고 우리는 함수에 리턴문을 따로 작성하지 않았는데 왜 객체를 반환할까요? 우리의 의도를 어떻게 알고 동작하는 걸까요?
new
키워드는 함수를 생성자 함수, 즉 객체를 반환하는 함수로서 호출할 수 있게 해주는 키워드입니다. 우리가 앞선 자동자 생성자 함수의 예시에서도 new
키워드를 사용했기에 생성자 함수로서 호출이 될 수 있었던 겁니다.
사실, 생성자 함수라는 어떤 특정 형식의 함수는 없습니다. 함수를 보고 "생성자 함수다" 라고 사람은 판단할 수 있겠지만, 프로그래밍 언어적인 판단은 불가능하다는 것입니다.
결론은 new
키워드와 함께 호출하느냐 그냥 일반적으로 호출하느냐의 차이입니다.
new
키워드를 사용하면 객체를 반환하는 것이 디폴트이고, 아니라면 일반 함수처럼 실행되게 됩니다.
자바스크립트 엔진은 new
키워드로 함수를 호출하면 다음과 같은 암묵적인 내부 동작을 수행합니다.
인스턴스 생성과
this
바인딩은 함수 몸체의 코드를 실행하기 전 암묵적으로 수행됩니다.
this
에 이 빈 객체를 bind
this
)가 암묵적으로 반환된다.return this;
라고 암묵적으로 추가된다고 보면 쉬움return
문이 명시적으로 작성되어 있다면?return 객체타입;
인 경우: this
대신 이미 있는 리턴문 수행return 원시타입;
인 경우: 무시하고 this
를 리턴앞서 설명한 바에 따르면, 생성자 함수라는 것은 어떤 함수를 new 키워드와 함께 호출할 때 비로소 우리가 의도했던 인스턴스 생성용 함수로 동작하는 것으로 이해됩니다.
그러면 모든 함수는 new 키워드와 함께 호출, 또는 new 키워드 없이 호출, 이렇게 두 가지 호출 방법이 있는 것일까요?
그렇지 않습니다. new 키워드를 사용할 수 없는 함수들이 있습니다.
자바스크립트에서 함수는 객체입니다. 함수 객체라고 부르는 특별한 객체이죠. 함수 객체 또한 일반 객체가 가지는 모든 내부 슬롯과 내부 메서드를 가집니다.
그런데 함수 객체만 가지는 특별함은 [[Call]]
과 [[Constructor]]
라는 내부 메서드로부터 옵니다. 두 내부 메서드는 일반 객체는 가지지 않는 메서드입니다.
[[Call]]
: 호출을 가능하게 해주는 내부 메서드. 일반 객체를 obj(); 처럼 호출하려고 하면 obj is not a function
이라는 에러 메세지가 나오는데, 일반 객체는 [[Call]]
내부 메서드가 없기 때문이다.[[Constructor]]
: new 키워드와 함께 호출 가능하게 해주는 내부 메서드함수를 new
키워드 없이 일반 호출하게 되면 [[Call]]
메서드가 호출되게 됩니다. 그런데 new 키워드와 함께 호출하면 [[Constructor]]
메서드가 호출되게 됩니다.
여기서 중요한 포인트가 나옵니다. 모든 함수가 [[Constructor]] 메서드를 가지진 않습니다.
다음은 [[Constructor]]
메서드를 가지지 않는 함수들입니다.
위의 종류의 함수들은 new 키워드로 호출하려고 하면 is not a constructor
에러 메세지를 보게 될 겁니다. function 키워드를 사용하여 만든 함수 표현식이나 선언문, class키워드로 만든 클래스 모두 new 키워드를 사용하여 인스턴스를 생성할 수 있습니다.
문제는 ES6 표준에서 메서드란, 객체나 생성자 함수, 또는 class 내에서 메서드 축약형으로 선언된 함수만을 의미합니다. ES6에 따르면 다음의 drive 함수는 메서드가 아니라 프로퍼티에 할당한 일반 함수로서 작동하게 됩니다.
function Car(brand, color) {
this.brand = brand;
this.color = color;
this.drive = function(){
console.log('driving!');
}
}
저렇게 만들어서 메서드로 잘만 쓰는데요?
그렇지 않습니다. 메서드와 프로퍼티에 할당한 일반 함수는 내부적으로 동작이 다르기 때문에 구분해서 사용해야 합니다. 가장 큰 차이는 메서드는 [[Constructor]]
내부 메서드가 없다는 점입니다. new 키워드로 호출할 수 없다는 것이죠. 또한 proptotype 객체가 생성되지 않습니다. 그리고 프로퍼티에 할당한 일반 함수와 다르게 [[HomeObject]]
라는 내부 슬롯을 가지게 되어 super()
를 호출할 수 있게 됩니다. 상속 구현이 가능한것이죠.
함수는 생성자 함수와 일반 함수로 나뉜다고 볼 수 있습니다. 일반 함수는 또 목적에 맞게 메서드와 진짜 일반 함수로 나뉠 수 있겠네요.
이러한 분류를 개발자들끼리 잘 알아볼 수 있게 작성하는 것이 중요합니다.
하지만 function 키워드로는 생성자함수, 일반함수, 그리고 메서드(처럼 보이는 프로퍼티에 할당한 일반함수)를 만들 수 있습니다. 이러한 범용성이 오히려 모호함을 만들기도 합니다.
저의 개인적인 의견으로는 생성자 함수는 class 키워드로, 메서드는 (원래 그래야하지만) 메서드 축약형으로, 일반 함수는 화살표 함수로 나타내어 누가 봐도 작성한 함수의 목적와 용도를 명확하게 하는 편이 나아 보입니다.
물론 저의 의견이 옳고 그름을 판단할 수 있는 말은 아니며, 항상 개인의 자유를 존중합니다.