생성자 함수와 객체 생성

자바스크립트에선 Object함수와 new 연산자를 함께 호출해 빈 객체를 생성할 수 있습니다.

const ironMan = new Object();

ironMan.name = "tony";
ironMan.intro = function() {
   console.log("hi. I'm "+this.name); 
};

console.log(ironMan); // {name: "tony", intro: f}
ironMan.intro(); // hi. I'm tony

생성자 함수란 new 연산자와 함께 호출하여 객체를 생성하는 함수를 말한다.

자바스크립트는 Object 함수 외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise 등의 built-in 생성자 함수를 제공합니다.

그러나 생성자 함수를 이용하는 방식은 객체 리터럴을 이용하는 방식에 비해 불편해보입니다. 생성자 함수를 이용한 객체 생성에는 어떤 장점이 있는지 살펴보겠습니다.

생성자 함수의 장점

const circle1 = {
  radius: 5,
  getDiameter() {
   return 2*this.radius; 
  }
};

const circle2 = {
  radius: 10,
  getDiameter() {
    return 2*this.radius; 
  }
}

circle1과 circle2는 radius 프로퍼티의 값만 제외하고 구조가 동일합니다. 하지만 객체 리터럴로 객체를 생성하는 경우 프로퍼티 구조가 동일함에도 불구하고 매번 기술해줘야 합니다. 생성하는 객체와 객체안의 프로퍼티가 많을수록 이러한 문제점은 간과할 수 없게 됩니다.

function Circle(radius) {
 this.radius = radius;
 this.getDiameter = function() {
  return 2*this.radius; 
 }
}

const circle1 = new Circle(5);
const circle2 = new Circle(10);

생성자 함수는 클래스와 같이 객체를 생성하기 위한 템플릿의 역할을 할 수 있습니다.

생성자 함수는 객체를 생성하는 함수입니다. 하지만 생성자 함수는 일반 함수와 동일한 방법으로 정의되고, new 연산자와 함께 호출하면 생성자 함수로 동작합니다. 즉, new 연산자와 함께 호출하지 않으면 일반 함수로 동작한다는 얘기입니다.

const circle3 = new Circle(15);

console.log(circle3) // undefined

console.log(radius); // 15 <- 일반함수로 호출되어 함수 내부의 this가 전역객체로 해석되었습니다.

생성자 함수의 인스턴스 생성 과정

new 연산자와 함께 생성자 함수를 호출하면 자바스크립트 엔진은 암묵적인 처리를 통해 인스턴스를 생성하고 반환합니다. 그 과정은 다음과 같습니다.

  1. 인스턴스 생성과 this 바인딩 - 암묵적으로 빈 객체({})를 생성합니다. 그리고 생성자 함수 내부의 this를 이 객체에 바인딩합니다.

  2. 인스턴스 초기화 - 생성자 함수 내부의 코드가 한 줄씩 실행되어 this에 바인딩된 인스턴스를 초기화 합니다. 이 과정은 개발자가 코드를 작성하여 진행합니다.

  3. 인스턴스 반환 - 모든 처리가 완료되면 인스턴스가 바인딩된 this가 암묵적으로 반환됩니다.

일반적으로 개발자는 2번만 관여하게 되며, 1번과 3번은 자바스크립트 엔진에 의해 암묵적으로 일어나게 됩니다.

function Circle(radius){
 // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩
 
 // 2. this에 바인딩된 인스턴스를 초기화
 this.radius = radius;
 this.getDiameter = function() {
  return 2*this.radius; 
 }
  
 // 3. 완성된 인스턴스를 바인딩하고 있는 this가 암묵적으로 반환 됨.
}

만약 생성자 함수에서 this가 아닌 다른 객체를 명시적으로 반환하면 this가 반환되지 못합니다.

function Circle(radius){
 // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩
 
 // 2. this에 바인딩된 인스턴스를 초기화
 this.radius = radius;
 this.getDiameter = function() {
  return 2*this.radius; 
 }
  
 // 3. 완성된 인스턴스를 바인딩하고 있는 this가 암묵적으로 반환 됨.
 // 명시적으로 객체를 반환하여 암묵적 this 반환이 무시된다.
 return {};
}

const circle = new Circle(1);
console.log(circle); // {}

하지만 원시 값을 명시적으로 반환하면 무시됩니다.

function Circle(radius){
 // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩
 
 // 2. this에 바인딩된 인스턴스를 초기화
 this.radius = radius;
 this.getDiameter = function() {
  return 2*this.radius; 
 }
  
 // 3. 완성된 인스턴스를 바인딩하고 있는 this가 암묵적으로 반환 됨.
 // 원시 값을 반환하면 무시됨
 return 100;
}

const circle = new Circle(1);
console.log(circle); // {radius: 1, getDiameter: f}

이처럼 생성자 함수 내부에선 암묵적으로 this를 반환하기에 return문은 생략해야 합니다.

내부 메서드 [[Call]]과 [[Construct]]

함수 선언문이나 함수 표현식으로 정의된 일반적인 함수는 new 연산자와 함께 생성자 함수로서 호출할 수 있습니다.

함수는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 물론, 함수로서 동작하기 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]], [[Construct]]같은 내부 메서드를 추가로 가지고 있습니다.

함수가 일반 함수로 호출되면 함수 객체의 내부 메서드 [[Call]]이 호출되고, new 연산자와 함께 호출되면 [[Construct]]가 호출됩니다.

[[Call]]을 갖는 함수 객체를 callable이라 하며, [[Construct]]를 갖는 함수 객체를 constructor, 갖지 않는 객체를 non-constructor라고 합니다.

모든 함수는 호출이 가능하므로 모든 함수는 [[Call]]을 갖고 있습니다. 하지만 모든 함수가 [[Construct]]를 갖는 것은 아닙니다. (즉 모든 함수가 생성자 함수로서 사용되지는 않습니다.)

constructor와 non-constructor

자바스크립트 엔진은 함수 정의 방식에 따라 함수를 constructor와 non-constructor로 구분합니다.

  1. constructor: 함수 선언문, 함수 표현식, 클래스
  2. non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
// 일반 함수 정의: 함수 선언문, 함수 표현식
function hulk() {}
const thor = function() {};
// x의 값으로 할당된 것은 일반 함수로 정의된 함수이다. 이는 메서드로 인정하지 않는다.
const ironMan = {
 jarvis: function() {} 
};

new hulk(); // hulk {}
new thor(); // thor {}
new ironMan.jarvis(); // jarvis {}

// 화살표 함수는 생성자 함수로 인정되지 않습니다.
const ultron = () => {};

new ultron(); // TypeError: arrow is not a constructor

//ES6의 메서드 축약 표현은 메서드 입니다.
const doctorKim = {
 vision() {} 
};

new doctorKim.vision(); // TypeError: doctorKim.vision is not a constructor

자바스크립트는 함수가 어디에 할당되었는지가 아니라 어떻게 정의되었는지에 따라 constructor와 non-constructor로 구분합니다.

위에서 함수를 일반 함수로 호출하면 내무 메서드 [[Call]]이, new 연산자와 함께 호출하면 내부 메서드 [[Construct]]가 호출된다고 했었죠? 이때 non-constructor인 함수 객체는 [[Construct]]내부 메서드를 가지고 있지 않습니다. 따라서 new와 함께 호출하게 되면 오류가 발생합니다.

또한 일반 함수와 생성자 함수에 특별한 형식적 차이는 없습니다. 따라서 생성자 함수는 일반 함수와 구분되도록 파스칼 케이스로 명명하는것이 일반적입니다.

new.target

생성자 함수가 new 연산자 없이 호출되는 것을 방지하기 위해 ES6에서는 new.target을 지원합니다. new.target은 this와 유사하게 constructor인 함수 내부에서 암묵적으로 사용되며 메타 프로퍼티라고 부릅니다.

함수가 new 연산자와 함께 호출되면 함수 내부의 new.target은 함수 자신을 가리키게 됩니다. 반면 일반 함수로서 호출되면 undefined를 갖게 됩니다. 따라서 new.target을 사용하면 생성자로서 호출되었는지, 일반 함수로서 호출되었는지를 구분할 수 있게 됩니다.

function Circle(radius) {
 if(!new.target){
  return new Circle(radius); 
 }
  
  this.radius = radius;
  this.getDiameter = function() {
   return 2 * this.radius; 
  }
}

이런식으로 사용하면 생성자 함수로서 정의한 함수가 일반 함수로 잘못 호출되는 실수를 방지할 수 있게 됩니다. 만약 IE와 같이 new.target을 사용할 수 없는 환경이라면 this를 이용해 스코프 세이프 생성자 패턴을 사용할 수 있습니다.

function Circle(radius){
 if(!(this instanceof Circle)) {
  return new Circle(radius); 
 }
  
  this.radius = radius;
  this.getDiameter = function() {
   return 2 * this.radius; 
  }
}

자바스크립트에서 대부분의 빌트인 생성자 함수는 new 연산자와 함께 호출되었는지를 확인한 후 적절한 값을 반환합니다.

예를 들면 Object와 Function 생성자 함수는 new 연산자 없이 호출해도 new 연산자와 함께 호출한 것과 동일하게 동작합니다.

반면 String, Number, Boolean 생성자 함수는 new 연산자 없이 호출하면 각각 문자열, 숫자, 불리언 값을 반환합니다. 이를 이용해 타입변환을 하기도 합니다.

  • 모던 자바스크립트 Deep Dive, 이웅모, 위키북스
profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글