본문 바로가기

Javascript/모던 자바스크립트 DeepDive

[모던 자바스크립트 Deep Dive] 17 생성자 함수에 의한 객체 생성

Object 생성자 함수

  • 생성자 함수 constructor란 new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수
  • 생성자 함수에 의해서 생성된 객체를 인스턴스 instance라 함
  • new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환
  • Object 외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise 등의 빌트인 생성자 함수 제공
// 빈 객체의 생성
const person = new Object();

// 프로퍼티 추가
person.name = "Jin";
person.sayHello = function () {
  console.log("Hi! My name is " + this.name);
};

console.log(person); // {name: 'Jin', sayHello: ƒ}
person.sayHello(); // Hi! My name is Jin
// String 생성자 함수에 의한 String 객체 생성
const strObj = new String("Jin");
console.log(typeof strObj); // object
console.log(strObj); // String {'Jin'}

// Number 생성자 함수에 의한 Number 객체 생성
const numObj = new Number(123);
console.log(typeof numObj); // object
console.log(numObj); // Number {123}

// Boolean 생성자 함수에 의한 Boolean 객체 생성
const boolObj = new Boolean(true);
console.log(typeof boolObj); // object
console.log(boolObj); // Boolean {true}

// Function 생성자 함수에 의한 Function 객체(함수) 생성
const func = new Function("x", "return x * x");
// Uncaught EvalError: Refused to evaluate a string as JavaScript 
// because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive:

// Array 생성자 함수에 의한 Array 객체(배열) 생성
const arr = new Array(1, 2, 3);
console.log(typeof arr); // object
console.log(arr); // [1, 2, 3]

// RegExp 생성자 함수에 의한 RegExp 객체(정규표현식) 생성
const regExp = new RegExp(/ab+c/i);
console.log(typeof regExp); // object
console.log(regExp); // /ab+c/i

// Date 생성자 함수에 의한 Date 객체 생성
const date = new Date();
console.log(typeof date); // object
console.log(date); // Tue Dec 12 2023 21:46:29 GMT+0900 (한국 표준시)

생성자 함수

객체 리터럴에 의한 객체 생성 방식의 문제점

  • 객체 리터럴에 의한 생성 방식은 단 하나의 객체만 생성하므로 동일한 프로퍼티를 갖는 여러 객체를 생성할 때 비효율적
const circle1 = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  },
};

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

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

생성자 함수에 의한 객체 생성 방식의 장점

  • 생성자 함수에 의한 객체 생성 방식은 마치 객체(인스턴스)를 생성하기 위한 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성 가능
  • 자바와 같은 클래스 기반 객체지향 언어의 생성자와는 다르게 그 형식이 정해져 있는 것이 아니라 일반 함수와 동일한 방식으로 생성자 함수를 정의하고 new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작
  • new 연산자 없이 호출하면 일반 함수로 동작
  • 일반적으로 생성자 함수로 사용할 함수는 이름을 대문자로 시작
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스의 생성
const circle1 = new Circle(5);
const circle2 = new Circle(10);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
함수 호출 방식 this가 가리키는 값 (this 바인딩)
일반 함수로 호출 전역 객체
메서드로 호출 메서드를 호출한 객체 (마침표 앞의 객체)
생성자 함수로 호출 생성자 함수가 (미래에) 생성할 인스턴스
// 함수는 다양한 방법으로 호출될 수 있음
function foo() {
  console.log(this);
}

// 일반 함수로 호출
// 전역 객체는 브라우저 환경에서는 window, Node.js 환경에는 global을 가리킴
foo(); // Window

const obj = { foo }; // ES6 프로퍼티 축약 표현
// 메서드로 호출
obj.foo(); // {foo: ƒ} obj

// 생성자 함수로 호출
const inst = new foo(); // foo {} inst
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// new 연산자와 함께 호출하지 않으면 생성자 함수로 동작하지 않음
// 즉, 일반 함수로 호출됨
const circle3 = Circle(15);

// 일반 함수로 호출된 Circle은 반환문이 없으므로 암묵적으로 undefined를 반환
console.log(circle3); // undefined

// 일반 함수로 호출된 Circle의 this는 전역 객체를 가리킴
console.log(radius); // 15

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

  • 생성자 함수 몸체에서 수행해야하는 것
    • 인스턴스를 생성 - 필수
    • 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기 값 할당) - 옵션
  • 자바스크립트는 new 연산자와 함께 생성자 함수를 호출하면 암묵적으로 인스턴스를 생성, 초기화, 반환함
// 생성자 함수
function Circle(radius) {
  // 인스턴스 초기화
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스 생성
const circle1 = new Circle(5)

1. 인스턴스의 생성과 this 바인딩

  • 암묵적으로 빈 객체 생성
  • 암묵적으로 생성된 빈 객체, 즉 인스턴스는 this에 바인딩
  • 이 처리는 함수 몸체의 코드가 한 줄씩 실행되는 런타임 이전에 실행

2. 인스턴스 초기화

  • 생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩 되어 있는 인스턴스를 초기화
  • 즉, this에 바인딩 되어있는 인스턴스에 프로퍼티나 메서드를 추가하고 생성자 함수가 인수로 전달 받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값 할당

3. 인스턴스 반환

  • 생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 전달
  • 생성자 함수 내부에 다른 객체를 명시적으로 반환할 경우,  this가 반환되지 못하고 return문에 명시한 객체가 반환
  • 생성자 함수 내부에 원시 값을 반환할 경우, 원시 값 반환은 무시되고 암묵적으로 this가 반환
  • 따라서 생성사 함수 내부에서 return문은 반드시 생략해야함
// 생성자 함수
function Circle(radius) {
  // 인스턴스 초기화
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스 생성
const circle1 = new Circle(5)
// 생성자 함수 내부에서 다른 객체를 return할 경우
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
  return {};
}
const circle1 = new Circle(5);
console.log(circle1); // {} this가 반환되지 못하고 return문에 명시한 객체가 반환

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

  • 함수는 객체이면서 호출할 수 있다는 추가적인 특징 보유
  • 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 몰론 함수로 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]], [[Contruct]] 같은 내부 메서드를 추가로 보유
  • 함수가 일반 함수로 호출되면 함수 객체의 내부 메서드 [[Call]] 호출
  • 함수가 new 연산자와 함게 생성자 함수로 호출되면 함수 객체 내부 메서드 [[Construct]] 호출
  • 내부 메서드 [[Call]]을 갖는 함수 객체를 callable
  • 내부 메서드 [[Construct]]를 갖는 함수 객체를 constructor / 안갖는 함수 객체를 non-constructor
  • 모든 함수는 callable이지만 모든 함수 객체가 constructor인 것은 아님

constructor와 non-constructor의 구분

  • constructor: 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
  • non-constructor: 메서드(ES6 메서드 축약 표현만 인정), 화살표 함수
  • 주의사항: ECMAScript 사양에서 메서드로 인정하는 범위가 일반적인 의미의 메서드보다 좁음
// constructor
// 일반 함수 정의: 함수 선언문, 함수 표현식
function foo() {}
const bar = function () {};
// 프로퍼티 x의 값으로 할당된 것은 일반 함수로 정의된 함수. 이는 메서드로 인정 x
const baz = {
  x: function () {},
};

new foo();
new bar();
new baz.x();

// non-constuctor
// 화살표 함수
const arrow = () => {};
const obj = {
  y() {},
};

new arrow(); // TypeError: arrow is not a constructor
new obj.y(); //TypeError: obj.y is not a constructor

new 연산자

  • 일반 함수와 생성자 함수에 특별한 형식적 차이는 없음
  • new 연산자와 함께 함수를 호출함녀 해당 함수는 생성자 함수로 동작
  • 함수 객체의 내부 메서드 [[Construct]]가 호출 ([[Call]]이 아닌)
  • 단, new 연산자와 함께 호출하는 함수는 constructor여야 함
  • 생성자 함수는 일반적으로 첫 문자를 대문자로 기술하는 파스칼 케이스로 명명하여 일반 함수와 구별할 수 있도록 함

new.target

  • ES6부터 지원, IE는 지원 안함
  • new.target은 this와 유사하게 contructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용되며 메타 프로퍼티라고 부름
  • new 연산자와 함께 생성자 함수로 호출되면 함수 내부의 new.target은 함수 자신을 가리킴
  • new 연산자 없이 일반 함수로 호출되면 함수 내부의 new.target은 undefined
  • 따라서 함수 내부에서 new.target을 사용하여 new 연산자와 생성자 함수로서 호출했는지 확인하여 그렇지 않은 경우 new 연산자와 함께 재귀 호출을 통해 생성자 함수로 호출 가능
  • 참고: 대부분의 빌트인 생성자 함수(Object, String, Number, Boolean, Function, Array, Date, RegExp, Promise 등)는 new 연산자와 함께 호출되었는지를 확인 후 적절한 값을 반환
    • Object와 Function 생성자 함수는 new 연산자 없이 호출해도 new 연산자와 함께 호출했을 때와 동일하게 동작
    • String,  Number, Boolean 생성자 함수는 new 연산자와 함께 호출했을 때 String, Number, Boolean 객체를 생성하여 반환하지만 new 연산자 없이 호출하면 문자열, 숫자, 불리언 값을 반환. 이를 통해 데이터 타입을 변환하기도 함
function Circle(radius) {
  // 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined
  if (!new.target) {
    // new 연산자와 함께 생성자 함수를 재귀 호출하여 생성된 인스턴스 반환
    return new Circle(radius);
  }
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
// new 연산자 호출 없이 생성자 함수를 호출해도 new.target을 통해 생성자 함수로 호출됨
const circle = Circle(5);
console.log(circle.getDiameter()); // 10

// ES6를 지원하지 않는 IE의 경우, 스코프 세이프 생성자 패턴 사용 가능
// Scope Safe Pattern
function Circle2(radius) {
  // 이 함수가 new 연산자와 함게 호출되지 않았다면(일반 함수 호출) 이 시점의 this는 전역 객체 window
  // 즉 this와 Circle은 프로토타입에 의해 연결되지 않는다
  if (!(this instanceof Circle)) {
    return new Circle(radius);
  }
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
const circle2 = Circle2(5);
console.log(circle2.getDiameter()); // 10
// new 없이 호출해도 동일하게 동작하는 Object
let obj = new Object();
console.log(obj); // {}

obj = Object();
console.log(obj); // {}

// new 없이 호출할 경우 다른 데이터 타입을 반환하는 Number
const numObj = new Number("123");
console.log(numObj, typeof numObj); // [Number: 123] object

const num = Number("123");
console.log(num, typeof num); // 123 number