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 === FooFoo()일반 호출 →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에선thisundefined → 할당 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은 그 둘 사이의 공장 스위치다.