← 글 목록

notes

17장. 생성자 함수에 의한 객체 생성

모던 JS Deep Dive — 생성자 함수·new와 프로토타입 객체 생성 핵심 정리.

참고: 이웅모 《모던 자바스크립트 Deep Dive》 17장 · [MDN new operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) · [MDN Object constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/Object)

같은 모양의 객체를 리터럴로 두세 번 복붙하다가 지쳤다. 반지름만 다른 원, 이름만 다른 유저… 구조는 같은데 매번 키를 다시 썼다. 생성자 함수는 그 “틀”이다. new와 함께 부르면 빈 객체가 생기고, 프로토타입이 붙고, this로 초기화가 돈다. class 문법은 나중에 예뻐진 껍질이고, 엔진 이야기는 여전히 이쪽이다.

flowchart TD
  NEW["new F(...args)"] --> EMPTY[빈 객체 생성]
  EMPTY --> PROTO["[[Prototype]] = F.prototype"]
  PROTO --> BIND["this에 바인딩 후 본문 실행"]
  BIND --> RET{본문이 객체를 return?}
  RET -->|yes| RO[그 객체가 결과]
  RET -->|no / 원시 return| RT[this가 결과]

17.1 Object 생성자 함수

new Object()로 빈 객체를 만든 뒤 프로퍼티를 붙일 수 있다. 동작은 이해해 두되, 실무 기본은 객체 리터럴 `{}`다. Object는 래퍼·유틸(keys, create, assign)의 입구로 더 자주 만난다.

const person = new Object();
person.name = 'Lee';
person.sayHello = function () {
  console.log('Hi! ' + this.name);
};
person.sayHello();

WHY: “생성자”라는 말이 거창해 보이지만, 시작점은 “빈 객체 공장”이다. 커스텀 생성자는 이 감각을 내 도메인 타입으로 확장한 것.

17.2 생성자 함수

17.2.1 리터럴만 쓰면 뭐가 아프나

리터럴은 한 번에 한 객체. 동일 구조의 인스턴스가 N개면 메서드 정의가 N번 복붙된다. 상태(고유 값)와 행동(공유해도 되는 함수)이 구분되지 않은 채 붙어 있다.

const circle1 = {
  radius: 5,
  getDiameter() { return 2 * this.radius; },
};
const circle2 = {
  radius: 10,
  getDiameter() { return 2 * this.radius; }, // 또 씀
};

17.2.2 생성자 = 동일 구조 템플릿

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

const c1 = new Circle(5);
const c2 = new Circle(10);
console.log(c1.getDiameter(), c2.getDiameter()); // 10 20

new로 만들 때 함수 안의 this는 지금 만들어지는 인스턴스다. 파스칼 케이스(Circle) 컨벤션은 “new로 부르라”는 신호다. 다만 컨벤션은 강제력이 없다 → 17.2.7 new.target.

함정: 위처럼 메서드를 생성자 안에 두면 인스턴스마다 함수 객체가 새로 생긴다. 보통은 Circle.prototype.getDiameter = …로 공유한다(19장). 이 장에선 “생성 과정”에 집중하고, 공유는 프로토타입으로 넘긴다.

17.2.3 인스턴스가 만들어지는 과정

생성자 역할은 (1) 인스턴스 생성 — 필수 (2) 초기화 — 옵션에 가깝다. 엔진/new가 대략 이렇게 움직인다.

  • 빈 객체를 만든다. 이게 인스턴스 후보.
  • 그 객체의 [[Prototype]]을 생성자 함수의 prototype 프로퍼티가 가리키는 객체에 연결한다.
  • 생성자 본문을 `this` = 그 객체로 실행한다. this.xxx =로 고유 상태를 심는다.
  • 본문이 객체를 return하면 그 객체가 new 식의 결과가 된다. return이 없거나 원시값을 return하면 1에서 만든 this가 결과다.
function Circle(radius) {
  // 암묵: this = {}, this.[[Prototype]] = Circle.prototype
  this.radius = radius;
  // return 없음 → this가 결과
}

function Weird() {
  this.a = 1;
  return { hijack: true }; // 객체 return → this 버림
}
console.log(new Weird()); // { hijack: true }

function Weird2() {
  this.a = 1;
  return 42; // 원시 return은 무시 → this
}
console.log(new Weird2()); // Weird2 { a: 1 }

WHY: “return 때문에 new가 이상해짐” 버그는 이 4단계를 모르면 평생 미신으로 남는다.

17.2.4 [[Call]]과 [[Construct]]

함수 객체는 일반 객체 슬롯에 더해, 호출용 [[Call]], (가능한 경우) 생성용 [[Construct]]를 갖는다.

  • foo() → [[Call]]
  • new foo() → [[Construct]]

함수 선언/표현식으로 만든 함수는 보통 둘 다 있다. 그래서 “일반 함수”를 new로도 부를 수 있다(의도했든 아니든).

function foo() {}
foo.prop = 10; // 함수도 객체라 프로퍼티 OK
foo.method = function () { console.log(this.prop); };
foo.method(); // 10

17.2.5 constructor vs non-constructor

엔진은 정의 방식으로 나눈다.

주의: 객체 리터럴 안의 method() {} 축약은 사양상 “메서드”라 non-constructor다. obj.fn = function () {}로 넣은 건 일반 함수 표현식이라 constructor다. “메서드처럼 보이면 전부 같다”가 아니다.

function foo() {}
const bar = function () {};
const baz = {
  // 축약 메서드 → non-constructor
  qux() {},
};
const arrow = () => {};

new foo(); // OK
new bar(); // OK
// new baz.qux(); // TypeError
// new arrow(); // TypeError

17.2.6 new 연산자

형식적 차이는 “new를 붙였는가”에 가깝다. new와 함께면 [[Construct]] 경로. non-constructor에 new를 붙이면 TypeError.

일반 함수를 new로 호출해도 객체는 생긴다. 반환값이 객체가 아니면 빈(초기화된) 인스턴스가 나온다. “생성자로 안 쓴 함수”에 new를 붙이는 습관은 피한다.

function add(x, y) {
  return x + y;
}
console.log(new add()); // {} 에 가깝게 — 의도 불명 코드

function make() {
  return { ok: true };
}
console.log(new make()); // { ok: true }

17.2.7 new.target — new 없이 호출 방지

파스칼 케이스만으론 실수가 안 멈춘다. ES6 new.target은 생성자 경로로 들어왔는지 알려 준다.

  • new Foo() 안 → new.target === Foo
  • Foo() 일반 호출 → new.target === undefined
function Foo(name) {
  if (!new.target) {
    return new Foo(name); // 또는 throw new Error('new로 호출하세요')
  }
  this.name = name;
}

const a = Foo('kim');     // 가드가 있으면 인스턴스로 보정 가능
const b = new Foo('lee');

IE 등 옛 환경은 미지원. 모던 타겟이면 class가 new 없이 호출 시 바로 TypeError라 같은 문제를 문법으로 줄인다.

class User {
  constructor(name) {
    this.name = name;
  }
}
// User('x'); // TypeError: Class constructor User cannot be invoked without 'new'

예제 — 왜 이렇게 쳐 봤냐

function User(name, role = 'member') {
  if (!new.target) throw new TypeError('User는 new로 호출');
  this.name = name;
  this.role = role;
}
User.prototype.isAdmin = function () {
  return this.role === 'admin';
};

const u = new User('민섭', 'admin');
console.log(u.isAdmin()); // true
console.log(Object.getPrototypeOf(u) === User.prototype); // true

WHY: 상태(name/role)는 인스턴스, 행동(isAdmin)은 프로토타입. 17에서 공장을 만들고 19에서 체인을 탄다.

// 팩토리( new 없음 ) vs 생성자
function createUser(name) {
  return { name, hello() { return this.name; } };
}
function UserCtor(name) {
  this.name = name;
}
UserCtor.prototype.hello = function () { return this.name; };

const f = createUser('a');
const c = new UserCtor('b');

WHY: 팩토리는 자유도가 높고, 생성자+prototype은 instanceof·확장에 유리. 둘 다 도구다.

헷갈리던 장면 / 함정

  • new 없이 생성자 호출 → 슬롭에선 this가 전역으로 샐 수 있음(20·22). strict에선 this undefined → 할당 TypeError.
  • 생성자 안 메서드 정의 → 메모리·비교(===)에서 “같은 함수”가 아님.
  • 객체 return으로 this를 조용히 폐기
  • 화살표/축약 메서드에 new → TypeError
  • Object.create·리터럴·new·class를 한 무더기로 취급 (생성 경로마다 프로토타입 결정이 다름 → 19.6)

표 — 생성 경로 감각

질문 → 자답

Q. class만 배우면 생성자 함수는 버려도 되나? A. 문법상 class로 충분해도, 스택·라이브러리·트랜스파일 결과·new.target/[[Construct]] 이해는 생성자 모델이 남는다.

Q. 팩토리만 쓰면? A. 가능. 다만 instanceof, 상속 체인, “프로토타입에 메서드 공유”를 직접 설계해야 한다.

Q. 생성자에서 화살표를 this에 붙이면? A. 인스턴스 필드처럼 동작(매번 새 함수). 렉시컬 this라 의도에 맞을 수는 있으나 공유는 안 된다.

웹 보강 메모

MDN new 알고리즘 요약과 new.target을 나란히 두고, 우리 코드베이스에서 function+파스칼 케이스 호출이 new 없는 곳이 있는지 검색했다. 가능하면 class로 옮기거나 new.target 가드를 넣었다.

실무로 바로

  • 도메인 타입: class 또는 생성자+prototype
  • 일회성 데이터: 리터럴
  • “new 강제”가 필요하면 class 또는 new.target
  • 메서드는 프로토타입(또는 class 본문 메서드)으로 공유
  • 생성자 내부에서 객체 return은 의도일 때만

여기서 잠깐 💁‍♂️

18장은 함수가 값·객체라는 쪽, 19장은 프로토타입 체인. 17은 그 둘 사이의 공장 스위치다.