생성자 함수
생성자 함수란 new키워드와 함께 함수를 호출해서 객체를 생성할 수 있는 함수를 말한다. 이때 생성되는 객체를 인스턴스라고 한다.
생성자 함수
자바스크립트를 공부하다 보면 생성자 함수로 객체를 생성한다는 말을 들어본 적이 있을 것이다. 그렇다면 생성자 함수는 무엇일까?
생성자 함수란 new키워드와 함께 함수를 호출해서 객체를 생성할 수 있는 함수를 말한다. 이때 생성되는 객체를 인스턴스라고 한다.
객체 생성 방식
앞서 객체에 대해 작성한 글에서 객체를 생성하는 방식에 대해 설명한 적이 있다. 가장 간단한 방법은 객체 리터럴을 통해 객체를 생성하는 방식이다. 하지만, 그 외에도 객체를 생성하는 방식이 존재한다. 바로 자바스크립트에서 제공하는 생성자 함수를 이용하는 것이다.
const obj = {} // 객체 리털럴로 객체를 생성한다.
const obj2 = new Object(); // 객체 생성자 함수를 통해 객체를 생성한다.
console.log(obj2) // {}
객체 생성자 함수로 위와 같이 객체를 생성할 수 있지만, 눈에 잘 들어오는 것은 확실히 객체 리터럴 방식이다. 위의 객체 생성자 함수는 말 그대로 빈 객체를 생성하는 생성자 함수이다. 이렇게 생성된 객체에 프로퍼티나 메소드를 추가할 때는 아래와 같은 방법을 사용할 것이다.
obj2.name = 'name';
obj2.hi = () => console.log('hi')
그렇다면 객체를 생성할 때는 객체 리터럴 방식을 사용하면 되는 것을 번거롭게 생성자 함수를 사용할 필요가 있을까하는 의문이 들 수 있다.
생성자 함수를 사용하는 이유
아래의 예시를 보자
const a = {
name: 'a',
sayHi(){
console.log('hi');
},
sayName(){
console.log(this.name);
}
}
const b = {
name: 'b',
sayHi(){
console.log('hi');
},
sayName(){
console.log(this.name);
}
}
const c = {
name: 'c',
sayHi(){
console.log('hi');
},
sayName(){
console.log(this.name);
}
}
위 코드에서는 a,b,c 객체를 생성한다. 하지만, 각 객체는 name프로퍼티만 다르고 메소드는 동일한 형태이다. 이런 경우 객체 리터럴 방식으로 하면 단 하나의 객체만 생성되기 때문에 같은 형태이더라도 또 정의해줘야 한다는 단점이 있다. 하지만 생성자 함수를 이용하면 다음과 같게 표현된다.
function ObjectMaker(name){
this.name = name;
this.sayHi = () => console.log('hi');
this.sayName = () => console.log(this.name);
}
const a = new ObjectMaker('a');
const b = new ObjectMaker('b');
const c = new ObjectMaker('c');
new키워드를 이용해 생성자 함수를 호출하면서 동적으로 변할 프로퍼티인 name의 값을 인자로 넘겨주면 생성자 함수 내부에서 name프로퍼티에 그 값을 넣어주는 형식이다. 그리고 모든 인스턴스는 같은 메소드를 가지게 된다.
이 같은 방식을 보게 되면 생성자 함수는 틀을 제공하는 것이고, 인스턴스는 그 틀에서 나온 물건들이다. 대신 그 물건의 색이나, 물건을 만든 재료 등은 물건마다 다를 수 있다. 그것이 프로퍼티가 될 것이다.
보통 객체를 생성할 때 프로퍼티의 값은 달라지지만, 메소드의 형태는 거의 비슷하다. 위 같은 경우도 name프로퍼티는 다르지만 메소드가 하는 동작은 같다. 만약 메소드도 달라진다면 그것도 인자로 넘겨줘서 생성자 함수 내에서 메소드에 넣어주기만 하면 된다. 어렵지 않다.
시간이 된다면 위의 코드에서 화살표 함수 내에 있는 this가 왜 인스턴스를 가리킬 수 있는지 생각해보면 좋겠다. 이전 this포스트에서 화살표 함수가 this를 결정하는 방법에 대해 말했었다. 잘 모르겠다면 그 포스트를 읽어보는 것도 좋다.
생성자 함수와 this
생성자 함수는 인스턴스를 생성한다. 즉, 객체를 생선하는 것이다. 그렇다면 어떤 방식으로 객체를 생성하는 것일까?
우선 생성자 함수의 역할을 생각해보자. 생성자 함수는 객체를 생성하기 위해 만든 함수이기 때문에 내부에서는 객체의 프로퍼티와 메소드를 만들어 주는 코드가 있을 것이다.
function ObjectMaker(name){
this.name = name;
this.sayHi = () => console.log('hi');
this.sayName = () => console.log(this.name);
}
위의 생성자 함수는 객체를 만들어주고 그 객체의 name프로퍼티에는 인자로 받은 name의 값을 넣어주고, 메소드는 sayHi와 sayName을 만들어 제공하게 된다. 즉, 이 생성자 함수를 호출해서 만든 인스턴스는 아래와 같은 형태일 것이다.
{
name: name(인자로 받은),
sayHi(){
console.log('hi');
}
sayName(){
console.log(this.name)
}
}
생성자 함수는 실제로 호출되면 빈 객체를 생성하고 거기에 프로퍼티와 메소드를 추가한 뒤 위와 같은 객체를 만들어 반환한다. 그렇게 반환한 객체가 변수에 할당되는 것이다.
그 전에 빈 객체를 생성자 함수의 this에 바인딩하게 된다. 이전에 생성자 함수의 this는 미래에 생성될 인스턴스라고 했는데, 그 뜻이 이것이다.
생성자 함수를 호출하게 되면, 빈 객체를 생성한다. 이 객체는 인스턴스가 될 것이고, 그 인스턴스가 될 객체를 this에 바인딩하는 것이다. 그래서 this는 인스턴스를 가리키게 되는 것이다.
생성자 함수와 constructor
생서자 함수가 인스턴스를 생성할 수 있는 이유는 무엇일까? 우리가 확인한 인스턴스를 생성할 때 생성자 함수를 new키워드와 함께 호출한다는 것이다. 생성자 함수도 본래는 함수이기 때문에 호출이 가능한 것이다. 즉, 생성자 함수를 new키워드 없이도 호출을 할 수 있다.
그렇게 되면 생성자 함수는 일반 함수처럼 호출이 된다. 즉, 우리가 원하는 인스턴스 생성이라는 행동이 아니게 된다. 실제로 일반 함수처럼 호출되는 지 확인하는 방법이 있다. 바로 this를 이용하는 것이다. 생성자 함수의 this는 미래에 생성할 인스턴스가 바인딩된다. 그리고 일반 함수의 this는 전역 객체가 바인딩된다고 했었다. 그럼 확인해보자.
위 사진은 foo라는 함수를 만들고 내부에 this.propFoo를 p로 주었다. 그 후 a라는 변수에 foo함수를 생성자 함수로 호출해서 인스턴스를 할당했다. 그랬을 때 프로퍼티를 접근하면
아래와 같이 정상적으로 인스턴스가 생성되어 프로퍼티에 접근할 수 있는 것을 확인할 수 있다. 그렇다면, 일반 함수로 호출해보자.
일반함수로 호출한다는 것은 위와 같이 new키워드 없이 함수를 호출하는 것이다. 위 함수를 결과를 변수에 할당해도 똑같은 결과가 나올 것이기 때문에 그냥 호출만 했다.
그랬을 때 전역 객체에 propFoo라는 프로퍼티가 생성된 것을 알 수 있다. 이렇게 생성자 함수로 생각하고 함수를 작성했지만, new키워드가 없다면 그 함수는 일반 함수로 동작하게 된다.
그렇다면 자바스크립트는 이 함수가 생성자 함수인지 어떻게 구분할까? 바로 [[construct]]를 통해서다. 모든 함수는 내부메소드로 [[call]]을 갖는다. 이는 이 객체가 호출 가능하다는 것을 의미한다. 즉, 이 내부메소드가 있으면 함수처럼 호출을 할 수 있는 것이다. 또, 어떤 함수는 [[construct]]라는 내부 메소드를 함께 갖는다. 이것을 가진 함수는 생성자 함수로 호출이 가능하다는 뜻이다. 이 내부메소드를 가진 함수들을 constructor라고 한다.
자바스크립트는 코드를 실행하다 일반 함수 호출을 만났을 때는 [[call]]내부 메소드를 실행한다. 즉, 일반 함수로 호출을 하는 것이다. 하지만 new키워드와 함께 있는 함수 호출을 만났을 경우는 [[call]]내부 메소드를 실행하는 것이 아니라 [[construct]]내부 메소드를 실행하게 된다. 이 경우는 함수가 일반 함수로 동작하는 것이 아니라 위에서 설명한 과정들을 거치는 생성자 함수로 호출이 되게 된다.
생성자 함수가 될 수 있는 함수들
위에서 모든 함수는 [[call]]을 갖지만, [[construct]]는 일부만 갖는다고 했다. 그렇다면 constructor가 될 수 있는 함수는 어떻게 구분할까?
방법은 함수 정의 방법에 있다.
- 생성자 함수가 될 수 있는 정의 방법 : 함수 선언문, 함수 표현식, class
- 생성자 함수가 될 수 없는 정의 방법 : ES6메소드, 화살표 함수
생성자 함수가 될 수 없는 방법으로 정의한 함수들은 [[construct]]를 가지고 있지 않기 때문에 new키워드와 함께 호출할 수 없다.
일반 함수로 호출하지 못하게 하자
생성자 함수를 정의할 때 인스턴스를 생성하는 것을 목적으로 만들기 때문에 일반 함수로 호출되는 것은 흐름에 맞지 않는 잘못된 코드가 될 것이다. 그렇다면, 이를 막을 수 있는 방법은 무엇일 있을까? 물론 일반 함수처럼 호출하지 않게 코드를 작성하는 것이 가장 좋겠지만, 혹시 실수로라도 new키워드를 빼고 작성할 수 있다. 그럴 때 자동으로 생성자 함수를 다시 호출하면 되지 않을까?
new.target
new.target을 함수 내부에서 사용하게 되면 함수 자기 자신을 가리키게 된다. 일반 함수에서 this는 전역 객체를 가리키고 생성자 함수에서 this는 인스턴스를 가리키기 때문에 함수 자기 자신을 나타내는 방법이 없었지만, new.target이라는 방법이 생겼다. 이를 이용하면 일반 함수로 호출하는 문제를 해결할 수 있다.
new.target은 생성자 함수로 호출되었을 때만 자기 자신을 가리키게 된다. 만약 일반함수로 호출하게 되면 new.target은 undefined를 갖게 된다. 이를 이용하자.
function ObjectMaker(name){
if(!new.target){
return new ObjectMaker(name);
}
...
}
위의 조건식은 일반 함수로 호출되었을 때 들어가게 된다. 그 경우 다시 이 함수를 new키워드와 함께 호출한 결과를 반환하면 일반 함수로 호출했을 때도 인스턴스 생성이 이루어진다.
instance of
위와 같은 방법으로 일반 함수로 호출 시에도 인스턴스를 생성하게 만들 수 있지만, 이는 ES6이후 도입된 문법으로 지원하지 않는 브라우저가 있을 수 있다. 이럴 경우를 대비해서 같은 동작을 하게 만드는 방법을 알아보자.
function ObjectMaker(name){
if(!(this instanceof ObjectMaker){
return new ObectMaker(name);
}
...
}
instanceof 연산자는 앞의 피연산자가 뒤의 피연산자로 생성된 인스턴스인지 boolean값으로 알려준다. 만약 일반 함수로 호출할 경우 this는 전역 객체가 되므로 전역 객체는 해당 함수로 생성된 인스턴스가 아니기 때문에 위 조건문에 걸리게 된다.
Object나 String과 같은 내장 생성자 함수는 위의 경우처럼 일반함수로 호출되더라도 생성자 함수로 동작하도록 만들어졌다.