이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
생성자 함수(Constructor)란 new 연산자와 함께 객체(인스턴스)를 생성하는 함수를 말한다. 생성자 함수에 의해 생성된 객체를 인스턴스라 한다. 자바스크립트에서의 생성자 함수는 빌트인 생성자 함수 제공을 통해 그 종류가 다양하다. 그 중 몇 가지를 적어보자면 다음과 같다.
코드로 한번 살펴보자.
const obj = new Object();
console.log(typeof obj); // object
console.log(obj); // {}
const objStr = new String("Kim");
console.log(typeof objStr); // object
console.log(objStr); // [String : 'Kim']
const objNum = new Number(10);
console.log(typeof objNum); // object
console.log(objNum); // [Number : 10]
const func = new Function("x", "return x+x");
console.log(typeof func); // function
console.log(func); // [Function : anonymous]
후에 기술하겠지만 형 변환을 할 때 String(000), Number(000)과 같은 작업을 한 번씩 해본 경험이 있을 것이다. 이는 new 연산자를 사용하지 않음으로써 형 변환의 효과를 보는 것이다.
생성자 함수는 객체 리터럴에 의한 객체 생성 방식의 단점(중복 코드)을 보완할 수 있다. 생성자 함수는 this를 통해 프로퍼티를 추가하고 전달 받은 인수로 이를 초기화한다. 그리고 자바스크립트 엔진에 의해 암묵적인 처리를 통해 인스턴스를 생성하고 반환한다. 그 과정을 살펴보자.
function Cons(x) {
// 1.
// 2.
this.x = x;
this.plusOne = function() {
return this.x + 1;
};
// 3.
}
const a = new Cons(1);
console.log(a); // Cons { x: 1, plusOne: [Function (anonymous)] }
1. 인스턴스 생성 및 this 바인딩
암묵적으로 빈 객체가 생성(인스턴스)되고 이곳에 this가 바인딩 된다. 일반 함수와는 다르게 생성자 함수 내부의 this가 인스턴스를 가리키는 이유이다. 이는 런타임 이전에 처리되는 작업이다.
2. 인스턴스 초기화
런타임에 코드가 한 줄씩 실행되어 this에 바인딩 되어 있는 인스턴스를 초기화한다.
3. 인스턴스 반환
생성자 함수 내부의 처리가 모두 완료되면 바인딩된 this가 암묵적으로 반환된다.
만약 다른 객체를 명시적으로 반환하려면 return 문을 통해 반환할 수 있다.(ex: return {};) 이때 this는 무시된다. 하지만 return 값이 원시 값이라면(ex : return 1;) return 문은 무시되고 this가 반환된다.
내부 메서드 [[Call]]을 갖는 함수 객체를 callable이라 하며, 내부 메서드 [[Construct]]를 갖는 함수 객체를 constructor, 갖지 않는 함수 객체를 non-constructor이라 한다. 함수가 일반 함수로서 호출이 되면 [[Call]] 메서드를 통해 호출이 되고 new 연산자와 함께 생성자 함수로서 호출되면 [[Construct]] 메서드를 통해 호출된다.
function f() {};
const a = () => {};
new f(); // f {}
new a(); // TypeError: a is not a constructor
참고
내부 메서드 및 내부 슬롯은 자바스크립트 엔진에서 내부적으로 동작한다. 개발자가 직접 접근할 수 있는 외부로 공개된 프로퍼티는 아니지만 간접적으로 접근이 가능하다. 예를 들면 모든 객체는 [[Prototype]] 내부 슬롯을 갖는데 __proto__를 통해 접근이 가능하다.
프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.
사실 일반 함수와 생성자 함수에 특별한 형식적 차이는 없다. 위에서 설명했듯이 new 연산자와 함께 호출하면 [[Construct]] 내부 메서드에 의해 생성자 함수로 new 연산자 없이 호출하면 [[Call]] 내부 메서드에 의해 일반 함수로 호출된다.
function Cons(x) {
this.x = x;
this.plusOne = function() {
return this.x + 1;
};
}
const a = Cons(1);
console.log(a); // undefined
console.log(x); // 1
console.log(plusOne()); //2
a.plusOne(); // TypeError: Cannot read properties of undefined (reading 'plusOne')
위 코드에서는 일반 함수로 호출하였다. 따라서 일반 함수의 this는 전역 객체 window(node에선 global)를 가리키므로 프로퍼티 x와 메서드 plusOne은 전역 객체의 프로퍼티와 메서드가 된다. 위와 같은 실수를 방지하기 위해 생성자 함수는 일반적으로 파스칼 케이스로 명명한다. 하지만 이보다 더 확실한 방법이 있다.
function Cons(x) {
// 생성자 함수로 호출되지 않았다면 new 연산자를 통해 생성자 함수로 재귀 호출
if (!new.target) {
return new Cons(x);
}
this.x = x;
this.plusOne = function() {
return this.x + 1;
};
}
const a = Cons(1);
console.log(a.plusOne()); // 2
new.target은 ES6에서부터 지원한 기능으로(IE는 지원불가) new 연산자와 함께 생성자 함수로 선언이 되었는지 확인이 가능하다. 일반 함수로 호출되었다면 undefined를 생성자 함수로 호출되었다면 함수 자신을 가리킨다. if 조건문에 프로토타입 연결 유무로 생성자 함수 호출 여부를 파악할 수 있는 스코프 세이프 생성자 패턴(!(this instanceof Cons))으로 대체하여 사용이 가능하다.
위에서 나열한 빌트인 생성자 함수는 new 연산자 사용 여부에 따라 적절한 값을 반환한다.
const strObj = new String(123);
console.log(strObj, typeof strObj); // [String: '123'] object
const str = String(123);
console.log(str, typeof str); // 123 string
따라서 이와 같은 특징을 이용해 데이터 타입 변환(형 변환)을 할 수 있다.