본 시리즈는 모던 자바스크립트 Deep Dive 책을 참고하여 작성하고 있습니다.
객체는 다양한 방식으로 생성할 수 있다. 앞에서는 객체 리터럴로 객체를 생성하는 방식을 알아보았다.
const obj = {
name: "kim",
age: 32
}
이번에는 생성자 함수를 통해서 객체를 생성하는 방식에 대해 알아본다.
const obj = new Object();
const obj2 = new Circle(5);
생성자 함수(constructor)는 new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수이다. 생성자 함수에 의해 생성된 객체를 인스턴스(instance)라 한다. 자바스크립트에서 제공하는 빌트인 생성자 함수를 사용할 수도 있고, 직접 생성자 함수를 만들어 쓸 수도 있다. 위 예시의 Object
외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise
와 같은 빌트인 생성자 함수가 제공된다. Object
빌트인 생성자 함수는 빈 객체를 생성해서 반환한다.
const circle = {
radius: 5,
getDiameter() {
return 2 * this.radius;
}
};
circle.getDiameter();
function Circle(radius) {
this.radius = radius; // this는 미래 생성될 인스턴스를 가리킴
this.getDiameter = function () {
return 2 * this.radius;
}
}
const circle = new Circle(5);
circle.getDiameter();
생성자 함수의 역할은 1) 인스턴스를 생성하고 2) 생성된 인스턴스를 초기화하는 것이다. 초기화 과정에서 인스턴스의 프로퍼티를 추가하고 초기값을 할당하는 일을 한다. 생성자 함수는 인스턴스를 반드시 생성해야 하고, 초기화는 할 수도 있고 하지 않을 수도 있다.
그런데 위의 <생성자 함수에 의한 객체 생성> 예시 코드를 보면 생성자 함수 내에서는 인스턴스 초기화를 위한 동작만 하고 있다. 그런데도 인스턴스를 반환받을 수 있는 이유는, new 연산자와 함께 생성자 함수를 호출하면 자바스크립트 엔진이 암묵적 처리를 통해 인스턴스를 생성하고 반환하기 때문이다.
인스턴스 생성과 this 바인딩
이 과정은 함수 실행 런타임 이전에 실행된다.
바인딩: 식별자와 값을 연결하는 과정. this가 가리킬 객체를 바인딩했다는 뜻이고, 생성자 함수 내부에서의 this는 인스턴스를 가리키게 된다.
인스턴스 초기화
생성자 함수 내부 코드가 한 줄씩 실행된다.
인스턴스 반환
function Circle(radius) {
this.radius = radius;
return radius;
}
const circle = new Circle(3);
console.log(circle); // { radius: 3 }
생성자 함수의 기본 동작은 바인딩된 this를 반환하는 것이다. 따라서 다른 값을 반환하는 것은 생성자 함수의 동작 방식을 훼손하는 것이므로 예측을 어렵게 만든다. 따라서 생성자 함수 내부에서는 return 문을 사용하지 않아야 한다.
함수 선언문이나 함수 표현식으로 정의한 함수는 일반 함수로 호출할 수도 있고 생성자 함수로서 호출할 수도 있다. 그리고 함수는 객체이므로 일반 객체와 동일하게 동작할 수 있다. 함수는 일반 객체가 가지는 내부 슬롯과 내부 메서드를 가지고, 추가로 함수로서 동작하기 위한 내부 슬롯과 내부 메서드를 가진다.
[[Environment]]
, [[FormatParameters]]
[[Call]]
, [[Construct]]
[[Call]]
: 함수를 일반 함수로서 호출할 경우[[Construct]]
: new 연산자와 함께 생성자 함수로 호출할 경우즉, 함수 객체는 callable이면서, constructor이거나 non-constructor이다. 모든 함수 객체는 호출 가능하지만, 생성자 함수로서 호출할 수 있을 수도 있고 없을 수도 있다. 호출할 수 없는 객체는 함수 객체가 아니므로 모든 함수 객체는 반드시 callable이다.
함수가 constructor인지 판단하는 기준은 함수를 정의한 방식이다.
// 일반 함수로 정의된 경우
function foo() {}
const bar = function () {};
const obj = {
x: function() {}
};
const ins_1 = new foo();
const ins_2 = new bar();
const ins_3 = new obj.x();
// non-constructor
const arrow = () => {};
const obj_2 = {
x() {}
};
const ins_arr = new arrow(); // TypeError
const ins_met = new obj_2.x(); // TypeError
new 연산자와 함께 함수를 호출하면 그 함수는 생성자 함수로 동작한다. 이 경우 함수 객체 내부 메서드 [[Construct]]
가 호출된다. 이 때 함수는 constructor여야 한다. new 연산자 없이 생성자 함수를 호출하면 일반 함수로 호출된다. 즉, 내부 메서드 [[Call]]
이 호출된다.
생성자 함수와 일반 함수 간에 형식적인 차이는 없기 때문에, 일반적으로 생성자 함수의 경우 함수 이름의 첫 문자를 대문자로 기술하여 일반 함수와 구분한다.
new.target
생성자 함수를 일반 함수로 호출하면, 함수 내에서 사용한 this는 전역 객체를 가리킨다. 따라서 new 키워드를 제외할 경우 전역 객체에 의도치 않게 프로퍼티나 메서드를 추가하게 될 수도 있다.
function Circle(radius) {
this.radius = radius;
this.getDiameter = function () {
return this.radius * 2;
}
}
Circle(2);
console.log(window.radius); // 2
console.log(window.getDiameter()); // 4
이러한 위험을 방지하기 위해 new.target
을 지원한다. 이를 메타 프로퍼티라고 부른다. new.target
은 ES6 문법으로, IE에서는 new.target
이 지원되지 않는다. 이는 constructor인 함수 내부에서 암묵적인 지역 변수처럼 사용된다.
function Circle(radius) {
// 일반 함수로 호출된 경우
if (!new.target) {
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return this.radius * 2;
}
}
new.target
을 사용 불가능한 경우에는 스코프 세이프 생성자 패턴을 사용할 수 있다.
function Circle(radius) {
// 생성자 함수로 생성되었다면 -> this가 Circle에 바인딩 됨
// 일반 함수로 생성되었다면 -> this는 전역 객체를 가리킴
if (!(this instanceof Circle)) {
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return this.radius * 2;
}
}
인스턴스는 프로토타입에 의해 생성자 함수와 연결된다. 대부분의 빌트인 생성자 함수도 new 연산자와 함께 호출되었는지 확인한 후 값을 반환한다.