생성자 함수 (Constructor Function)
혹시 이런 코드를 작성하다가 멈춘 적 있나요?
function User(name, email) {
this.name = name;
this.email = email;
}
const user1 = User('Alice', 'alice@example.com');
console.log(user1); // undefined - 왜?!
const user2 = new User('Bob', 'bob@example.com');
console.log(user2); // User { name: 'Bob', email: 'bob@example.com' } - 이건 또 왜?
저도 처음 JavaScript를 배울 때 이 코드가 너무 혼란스러웠습니다. “new를 빼먹으면 왜 undefined가 나올까?”, “this는 대체 무엇을 가리키는 걸까?” 이것이 바로 생성자 함수(Constructor Function)의 핵심 개념입니다.
상상해보세요. 케이크 공장이 있습니다. 공장에는 케이크를 만드는 틀(몰드)이 있고, 그 틀에 반죽을 부어 넣으면 똑같은 모양의 케이크가 계속 만들어집니다. 생성자 함수는 바로 이 “틀”과 같습니다. 틀 자체는 케이크가 아니지만, 틀을 사용하면 수백 개의 동일한 구조를 가진 케이크를 만들 수 있습니다.
JavaScript의 생성자 함수도 정확히 이렇게 동작합니다. 생성자 함수는 객체를 만드는 템플릿(설계도)이고, new 키워드는 그 템플릿을 사용해 실제 객체를 찍어내는 기계입니다.
왜 생성자 함수를 이해해야 할까요?
1. 클래스 문법의 기초
ES6부터 class 문법을 사용하지만, 내부적으로는 생성자 함수로 동작합니다.
// 겉모습: 클래스
class User {
constructor(name) {
this.name = name;
}
}
// 실제 동작: 생성자 함수
typeof User; // "function"
클래스를 이해하려면 생성자 함수를 먼저 이해해야 합니다. 왜냐하면 클래스는 생성자 함수의 문법적 설탕(syntactic sugar)이기 때문입니다.
2. 레거시 코드 이해
많은 라이브러리와 레거시 코드베이스는 여전히 생성자 함수를 사용합니다.
// jQuery
const $element = new jQuery('#myElement');
// Date
const now = new Date();
// Promise
const promise = new Promise((resolve, reject) => {
// ...
});
생성자 함수를 이해하지 못하면 이런 코드들이 어떻게 동작하는지 알 수 없습니다.
3. 디버깅과 문제 해결
실무에서 이런 버그를 만났을 때:
function Counter() {
this.count = 0;
this.increment = function() {
this.count++;
};
}
const counter = Counter(); // new 빼먹음!
counter.increment(); // TypeError: Cannot read property 'increment' of undefined
생성자 함수의 동작 원리를 이해해야 해결할 수 있습니다.
4. 메모리 효율적인 객체 생성
// ❌ 매번 메서드를 새로 생성 (메모리 낭비)
function BadUser(name) {
this.name = name;
this.greet = function() {
return `Hello, ${this.name}`;
};
}
// ✅ 프로토타입에 메서드 공유 (메모리 효율)
function GoodUser(name) {
this.name = name;
}
GoodUser.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const user1 = new GoodUser('Alice');
const user2 = new GoodUser('Bob');
console.log(user1.greet === user2.greet); // true (같은 함수 참조!)
생성자 함수를 제대로 이해하면 메모리 효율적인 코드를 작성할 수 있습니다.
기본 개념: 생성자 함수란 무엇인가?
생성자 함수의 정의
생성자 함수는 새로운 객체를 생성하고 초기화하는 함수입니다. 일반 함수와 동일하지만, new 키워드와 함께 호출될 때 특별한 동작을 합니다.
// 생성자 함수 (관례적으로 대문자로 시작)
function Person(name, age) {
// this = 새로 만들어질 객체를 가리킴
this.name = name;
this.age = age;
}
// new 키워드로 인스턴스 생성
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);
console.log(person1.name); // "Alice"
console.log(person2.age); // 30
console.log(person1 instanceof Person); // true
생성자 함수의 핵심 특징:
- 함수 이름이 관례적으로 대문자로 시작 (PascalCase)
new키워드와 함께 호출this를 통해 새 객체의 속성 설정- 명시적으로
return하지 않아도 자동으로 새 객체 반환
new 키워드가 하는 4가지 일
MDN 공식 문서에 따르면, new Person('Alice', 25)를 실행하면 내부적으로:
// 1. 빈 객체 생성
const newInstance = {};
// 2. 프로토타입 연결
Object.setPrototypeOf(newInstance, Person.prototype);
// 또는: newInstance.__proto__ = Person.prototype;
// 3. 생성자 함수 실행 (this를 새 객체에 바인딩)
Person.call(newInstance, 'Alice', 25);
// 4. 반환값 처리
// - 생성자가 객체를 반환하면 그 객체 반환
// - 아니면 newInstance 반환
return newInstance;
이 과정을 시각화하면:
┌─────────────────────────┐
│ new Person('Alice') │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ 1. 빈 객체 생성 │
│ {} │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ 2. 프로토타입 연결 │
│ {}.__proto__ = │
│ Person.prototype │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ 3. 생성자 실행 │
│ this.name = 'Alice' │
│ this.age = 25 │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ 4. 객체 반환 │
│ { name: 'Alice', │
│ age: 25 } │
└─────────────────────────┘
일반 함수 vs 생성자 함수
// 일반 함수
function greet(name) {
return `Hello, ${name}`;
}
const message = greet('Alice');
console.log(message); // "Hello, Alice"
// 생성자 함수
function User(name) {
this.name = name;
}
const user = new User('Alice');
console.log(user); // User { name: 'Alice' }
// ❌ 생성자를 일반 함수처럼 호출
const result = User('Bob');
console.log(result); // undefined
console.log(window.name); // 'Bob' - 전역 오염!
차이점:
- 일반 함수: 값을 계산하거나 작업을 수행하고 결과를 반환
- 생성자 함수: 새로운 객체를 생성하고 초기화
실전 예제: 생성자 함수 작성하기
1. 기본 생성자 함수
function Car(brand, color, year) {
// 속성 초기화
this.brand = brand;
this.color = color;
this.year = year;
this.mileage = 0; // 기본값
}
// 프로토타입에 메서드 추가 (모든 인스턴스가 공유)
Car.prototype.drive = function(distance) {
this.mileage += distance;
return `${this.color} ${this.brand}가 ${distance}km 주행했습니다.`;
};
Car.prototype.getInfo = function() {
return `${this.year}년식 ${this.color} ${this.brand} (주행거리: ${this.mileage}km)`;
};
// 인스턴스 생성
const tesla = new Car('Tesla', 'red', 2023);
const bmw = new Car('BMW', 'blue', 2022);
console.log(tesla.drive(100)); // "red Tesla가 100km 주행했습니다."
console.log(bmw.drive(50)); // "blue BMW가 50km 주행했습니다."
console.log(tesla.getInfo()); // "2023년식 red Tesla (주행거리: 100km)"
console.log(bmw.getInfo()); // "2022년식 blue BMW (주행거리: 50km)"
// 메서드는 공유되지만 데이터는 독립적
console.log(tesla.drive === bmw.drive); // true (같은 함수 참조)
console.log(tesla.mileage !== bmw.mileage); // true (다른 값)
2. 기본값이 있는 생성자 함수
function Product(name, price, category) {
this.name = name;
this.price = price || 0; // 기본값: 0
this.category = category || 'uncategorized'; // 기본값: 'uncategorized'
this.createdAt = new Date(); // 자동으로 현재 시간
}
// 더 현대적인 방법: ES6 기본 매개변수
function ModernProduct(name, price = 0, category = 'uncategorized') {
this.name = name;
this.price = price;
this.category = category;
this.createdAt = new Date();
}
const product1 = new Product('Laptop', 1200, 'Electronics');
const product2 = new Product('Notebook'); // price와 category는 기본값
console.log(product1); // Product { name: 'Laptop', price: 1200, category: 'Electronics', ... }
console.log(product2); // Product { name: 'Notebook', price: 0, category: 'uncategorized', ... }
3. 검증 로직이 있는 생성자 함수
function BankAccount(owner, initialBalance) {
// 검증: owner는 필수
if (!owner) {
throw new Error('소유자 이름은 필수입니다.');
}
// 검증: 초기 잔액은 0 이상
if (initialBalance < 0) {
throw new Error('초기 잔액은 0 이상이어야 합니다.');
}
this.owner = owner;
this.balance = initialBalance || 0;
this.transactions = [];
}
BankAccount.prototype.deposit = function(amount) {
if (amount <= 0) {
throw new Error('입금액은 0보다 커야 합니다.');
}
this.balance += amount;
this.transactions.push({
type: 'deposit',
amount: amount,
date: new Date()
});
return this.balance;
};
BankAccount.prototype.withdraw = function(amount) {
if (amount <= 0) {
throw new Error('출금액은 0보다 커야 합니다.');
}
if (this.balance < amount) {
throw new Error('잔액이 부족합니다.');
}
this.balance -= amount;
this.transactions.push({
type: 'withdraw',
amount: amount,
date: new Date()
});
return this.balance;
};
// 사용
const account = new BankAccount('Alice', 1000);
account.deposit(500); // 1500
account.withdraw(300); // 1200
console.log(account.balance); // 1200
console.log(account.transactions.length); // 2
// ❌ 검증 실패
try {
const invalidAccount = new BankAccount('', -100);
} catch (e) {
console.error(e.message); // "소유자 이름은 필수입니다."
}
4. new 없는 호출 방지하기
function SafeUser(name, email) {
// new 없이 호출되었는지 체크
if (!(this instanceof SafeUser)) {
return new SafeUser(name, email);
}
this.name = name;
this.email = email;
}
// 두 방법 모두 동작
const user1 = new SafeUser('Alice', 'alice@example.com');
const user2 = SafeUser('Bob', 'bob@example.com'); // new 없이 호출해도 동작!
console.log(user1 instanceof SafeUser); // true
console.log(user2 instanceof SafeUser); // true
// 더 현대적인 방법: new.target 사용 (ES6+)
function ModernUser(name, email) {
// new로 호출되지 않았으면 에러
if (!new.target) {
throw new Error('User는 new 키워드와 함께 호출해야 합니다.');
}
this.name = name;
this.email = email;
}
const user3 = new ModernUser('Charlie', 'charlie@example.com'); // ✅ 동작
try {
const user4 = ModernUser('David', 'david@example.com'); // ❌ 에러
} catch (e) {
console.error(e.message); // "User는 new 키워드와 함께 호출해야 합니다."
}
5. 계산된 속성이 있는 생성자 함수
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
// 프로토타입에 getter 추가
Object.defineProperty(Rectangle.prototype, 'area', {
get: function() {
return this.width * this.height;
}
});
Object.defineProperty(Rectangle.prototype, 'perimeter', {
get: function() {
return 2 * (this.width + this.height);
}
});
Rectangle.prototype.resize = function(factor) {
this.width *= factor;
this.height *= factor;
};
const rect = new Rectangle(10, 20);
console.log(rect.area); // 200 (계산됨)
console.log(rect.perimeter); // 60 (계산됨)
rect.resize(2);
console.log(rect.area); // 800 (자동으로 재계산)
console.log(rect.perimeter); // 120 (자동으로 재계산)
6. 팩토리 메서드가 있는 생성자 함수
function User(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
this.createdAt = new Date();
}
// 정적 팩토리 메서드 (생성자 함수 자체에 추가)
User.createAdmin = function(name, email) {
return new User(name, email, 'admin');
};
User.createGuest = function(name) {
return new User(name, `${name}@guest.com`, 'guest');
};
User.createFromData = function(data) {
return new User(data.name, data.email, data.role);
};
// 사용
const admin = User.createAdmin('Alice', 'alice@example.com');
const guest = User.createGuest('Anonymous');
const user = User.createFromData({
name: 'Bob',
email: 'bob@example.com',
role: 'member'
});
console.log(admin.role); // "admin"
console.log(guest.role); // "guest"
console.log(guest.email); // "Anonymous@guest.com"
좋은 예 vs 나쁜 예
❌ 나쁜 예 1: 메서드를 생성자 내부에서 정의
function BadAnimal(name, species) {
this.name = name;
this.species = species;
// ❌ 인스턴스마다 새로운 함수 생성 (메모리 낭비)
this.speak = function() {
console.log(`${this.name}가 소리를 냅니다.`);
};
this.eat = function(food) {
console.log(`${this.name}가 ${food}를 먹습니다.`);
};
}
const dog = new BadAnimal('멍멍이', '개');
const cat = new BadAnimal('야옹이', '고양이');
console.log(dog.speak === cat.speak); // false (다른 함수!)
// 1000개의 인스턴스를 만들면 1000개의 speak 함수가 생성됨
✅ 좋은 예 1: 메서드는 프로토타입에
function GoodAnimal(name, species) {
// ✅ 인스턴스별 데이터만 생성자에
this.name = name;
this.species = species;
}
// ✅ 공유 메서드는 프로토타입에
GoodAnimal.prototype.speak = function() {
console.log(`${this.name}가 소리를 냅니다.`);
};
GoodAnimal.prototype.eat = function(food) {
console.log(`${this.name}가 ${food}를 먹습니다.`);
};
const dog = new GoodAnimal('멍멍이', '개');
const cat = new GoodAnimal('야옹이', '고양이');
console.log(dog.speak === cat.speak); // true (같은 함수 참조!)
// 1000개의 인스턴스를 만들어도 speak 함수는 하나만 존재
❌ 나쁜 예 2: new 빼먹기
function User(name) {
this.name = name;
}
// ❌ new 없이 호출
const user = User('Alice');
console.log(user); // undefined
console.log(window.name); // 'Alice' - 전역 오염!
// this가 window(전역 객체)를 가리킴
✅ 좋은 예 2: new 강제하기
function User(name) {
// ✅ new 없이 호출되면 자동으로 new 추가
if (!(this instanceof User)) {
return new User(name);
}
this.name = name;
}
// 두 방법 모두 안전
const user1 = new User('Alice'); // ✅
const user2 = User('Bob'); // ✅ 자동으로 new 추가됨
console.log(user1 instanceof User); // true
console.log(user2 instanceof User); // true
❌ 나쁜 예 3: 참조 타입을 프로토타입에
function BadStudent(name) {
this.name = name;
}
// ❌ 배열을 프로토타입에 정의 (모든 인스턴스가 공유!)
BadStudent.prototype.grades = [];
const alice = new BadStudent('Alice');
const bob = new BadStudent('Bob');
alice.grades.push(90);
bob.grades.push(85);
console.log(alice.grades); // [90, 85] - 오염됨!
console.log(bob.grades); // [90, 85] - 오염됨!
console.log(alice.grades === bob.grades); // true (같은 배열 참조)
✅ 좋은 예 3: 참조 타입은 생성자에서
function GoodStudent(name) {
this.name = name;
// ✅ 인스턴스마다 새 배열 생성
this.grades = [];
}
const alice = new GoodStudent('Alice');
const bob = new GoodStudent('Bob');
alice.grades.push(90);
bob.grades.push(85);
console.log(alice.grades); // [90]
console.log(bob.grades); // [85]
console.log(alice.grades === bob.grades); // false (다른 배열)
❌ 나쁜 예 4: 명시적으로 잘못된 값 반환
function BadConstructor(value) {
this.value = value;
// ❌ 객체를 명시적으로 반환하면 this가 무시됨
return { value: 'overridden' };
}
const instance = new BadConstructor(42);
console.log(instance.value); // 'overridden' (this.value가 아님!)
console.log(instance instanceof BadConstructor); // false (프로토타입 연결도 끊김)
✅ 좋은 예 4: 반환값을 명시하지 않기
function GoodConstructor(value) {
this.value = value;
// ✅ 명시적으로 return하지 않음 (자동으로 this 반환)
}
const instance = new GoodConstructor(42);
console.log(instance.value); // 42
console.log(instance instanceof GoodConstructor); // true
// 예외: 원시값 반환은 무시됨 (정상 동작)
function AlsoGoodConstructor(value) {
this.value = value;
return 100; // 무시됨
}
const instance2 = new AlsoGoodConstructor(42);
console.log(instance2.value); // 42 (정상)
활용
1. 상속 구현하기
// 부모 생성자
function Animal(name, species) {
this.name = name;
this.species = species;
}
Animal.prototype.speak = function() {
return `${this.name}가 소리를 냅니다.`;
};
Animal.prototype.getInfo = function() {
return `이름: ${this.name}, 종: ${this.species}`;
};
// 자식 생성자
function Dog(name, breed) {
// 부모 생성자 호출 (속성 상속)
Animal.call(this, name, '개');
this.breed = breed;
}
// 프로토타입 체인 설정 (메서드 상속)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // constructor 복원
// Dog만의 메서드 추가
Dog.prototype.bark = function() {
return `${this.name}: 멍멍!`;
};
// Dog의 speak 메서드 오버라이드
Dog.prototype.speak = function() {
return `${this.name}가 멍멍 짖습니다.`;
};
const dog = new Dog('멍멍이', 'Golden Retriever');
console.log(dog.getInfo()); // "이름: 멍멍이, 종: 개"
console.log(dog.speak()); // "멍멍이가 멍멍 짖습니다."
console.log(dog.bark()); // "멍멍이: 멍멍!"
// 프로토타입 체인 확인
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
프로토타입 체인 시각화:
┌─────────────────────────┐
│ dog 인스턴스 │
│ name: '멍멍이' │
│ species: '개' │
│ breed: 'Golden...' │
└──────────┬──────────────┘
│ [[Prototype]]
↓
┌─────────────────────────┐
│ Dog.prototype │
│ bark: function │
│ speak: function │
│ constructor: Dog │
└──────────┬──────────────┘
│ [[Prototype]]
↓
┌─────────────────────────┐
│ Animal.prototype │
│ speak: function │
│ getInfo: function │
│ constructor: Animal │
└──────────┬──────────────┘
│ [[Prototype]]
↓
┌─────────────────────────┐
│ Object.prototype │
│ toString: function │
│ hasOwnProperty: fn │
└──────────┬──────────────┘
│
↓
null
2. private 변수 구현하기 (클로저 활용)
function Counter(initialValue) {
// private 변수 (클로저로 보호됨)
let count = initialValue || 0;
const history = [];
// public 메서드 (클로저를 통해 private 변수 접근)
this.increment = function() {
count++;
history.push({ action: 'increment', value: count, time: new Date() });
return count;
};
this.decrement = function() {
count--;
history.push({ action: 'decrement', value: count, time: new Date() });
return count;
};
this.getValue = function() {
return count;
};
this.getHistory = function() {
return [...history]; // 복사본 반환
};
this.reset = function() {
const oldCount = count;
count = initialValue || 0;
history.push({ action: 'reset', from: oldCount, to: count, time: new Date() });
return count;
};
}
const counter = new Counter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getValue()); // 11
// ❌ private 변수에 직접 접근 불가
console.log(counter.count); // undefined
console.log(counter.history); // undefined
console.log(counter.getHistory());
// [
// { action: 'increment', value: 11, time: ... },
// { action: 'increment', value: 12, time: ... },
// { action: 'decrement', value: 11, time: ... }
// ]
// 주의: 이 방식은 메서드가 프로토타입에 있지 않고 인스턴스마다 생성됨
const counter2 = new Counter(0);
console.log(counter.increment === counter2.increment); // false
3. 믹스인(Mixin) 패턴
// 믹스인: 여러 생성자에 기능 추가
const timestampMixin = {
getCreatedTime() {
return this.createdAt;
},
getAge() {
return Date.now() - this.createdAt;
}
};
const serializableMixin = {
toJSON() {
const obj = {};
for (let key in this) {
if (this.hasOwnProperty(key)) {
obj[key] = this[key];
}
}
return JSON.stringify(obj);
},
fromJSON(json) {
const obj = JSON.parse(json);
for (let key in obj) {
this[key] = obj[key];
}
return this;
}
};
// 믹스인 적용 헬퍼 함수
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
}
// 생성자 정의
function User(name, email) {
this.name = name;
this.email = email;
this.createdAt = Date.now();
}
function Product(name, price) {
this.name = name;
this.price = price;
this.createdAt = Date.now();
}
// 믹스인 적용
mixin(User, timestampMixin, serializableMixin);
mixin(Product, timestampMixin, serializableMixin);
// 사용
const user = new User('Alice', 'alice@example.com');
const product = new Product('Laptop', 1200);
setTimeout(() => {
console.log(user.getAge()); // 1000 (1초 후)
console.log(product.getAge()); // 1000
const userJson = user.toJSON();
console.log(userJson); // {"name":"Alice","email":"alice@example.com","createdAt":...}
const newUser = new User('', '');
newUser.fromJSON(userJson);
console.log(newUser.name); // "Alice"
}, 1000);
4. 싱글톤 패턴
function Database(host, port) {
// 이미 인스턴스가 존재하면 반환
if (Database.instance) {
return Database.instance;
}
this.host = host;
this.port = port;
this.connected = false;
this.queries = [];
// 인스턴스 저장
Database.instance = this;
}
Database.prototype.connect = function() {
if (this.connected) {
console.log('이미 연결되어 있습니다.');
return;
}
console.log(`${this.host}:${this.port}에 연결 중...`);
this.connected = true;
};
Database.prototype.query = function(sql) {
if (!this.connected) {
throw new Error('데이터베이스에 연결되지 않았습니다.');
}
this.queries.push({ sql, time: new Date() });
console.log(`쿼리 실행: ${sql}`);
};
// 첫 번째 인스턴스 생성
const db1 = new Database('localhost', 5432);
db1.connect();
db1.query('SELECT * FROM users');
// 두 번째 시도 - 같은 인스턴스 반환
const db2 = new Database('example.com', 3306);
console.log(db1 === db2); // true (같은 인스턴스!)
console.log(db2.host); // 'localhost' (첫 번째 설정 유지)
console.log(db2.queries.length); // 1 (첫 번째 쿼리 기록됨)
5. 팩토리 패턴과 결합
function Vehicle(type, brand, model) {
this.type = type;
this.brand = brand;
this.model = model;
}
Vehicle.prototype.getInfo = function() {
return `${this.type}: ${this.brand} ${this.model}`;
};
// 팩토리 함수
function VehicleFactory() {}
VehicleFactory.createCar = function(brand, model) {
return new Vehicle('자동차', brand, model);
};
VehicleFactory.createMotorcycle = function(brand, model) {
return new Vehicle('오토바이', brand, model);
};
VehicleFactory.createTruck = function(brand, model) {
return new Vehicle('트럭', brand, model);
};
VehicleFactory.create = function(type, brand, model) {
const types = {
car: '자동차',
motorcycle: '오토바이',
truck: '트럭'
};
const vehicleType = types[type.toLowerCase()];
if (!vehicleType) {
throw new Error('알 수 없는 차량 타입입니다.');
}
return new Vehicle(vehicleType, brand, model);
};
// 사용
const car = VehicleFactory.createCar('Tesla', 'Model 3');
const motorcycle = VehicleFactory.createMotorcycle('Harley-Davidson', 'Street 750');
const truck = VehicleFactory.create('truck', 'Volvo', 'FH16');
console.log(car.getInfo()); // "자동차: Tesla Model 3"
console.log(motorcycle.getInfo()); // "오토바이: Harley-Davidson Street 750"
console.log(truck.getInfo()); // "트럭: Volvo FH16"
함정과 주의사항
1. this 바인딩 문제
function Timer(name) {
this.name = name;
this.seconds = 0;
}
Timer.prototype.start = function() {
// ❌ 문제: setInterval 내부에서 this가 바뀜
setInterval(function() {
this.seconds++; // this가 window를 가리킴!
console.log(`${this.name}: ${this.seconds}초`);
}, 1000);
};
const timer1 = new Timer('타이머1');
// timer1.start(); // NaN초 출력
// ✅ 해결 1: 화살표 함수 사용
Timer.prototype.startArrow = function() {
setInterval(() => {
this.seconds++; // this가 Timer 인스턴스를 가리킴
console.log(`${this.name}: ${this.seconds}초`);
}, 1000);
};
// ✅ 해결 2: bind 사용
Timer.prototype.startBind = function() {
setInterval(function() {
this.seconds++;
console.log(`${this.name}: ${this.seconds}초`);
}.bind(this), 1000);
};
// ✅ 해결 3: that/self 변수 사용
Timer.prototype.startThat = function() {
const that = this;
setInterval(function() {
that.seconds++;
console.log(`${that.name}: ${that.seconds}초`);
}, 1000);
};
2. 생성자 반환값 오버라이드
function User(name) {
this.name = name;
// ❌ 객체를 반환하면 this가 무시됨
return { name: 'overridden' };
}
const user = new User('Alice');
console.log(user.name); // 'overridden' (this.name이 아님!)
console.log(user instanceof User); // false (프로토타입 연결도 끊김)
// MDN 문서: "생성자가 객체(non-primitive)를 반환하면
// 그 객체가 최종 결과가 됩니다."
// ✅ 원시값 반환은 무시됨 (정상 동작)
function SafeUser(name) {
this.name = name;
return 42; // 무시됨
}
const safeUser = new SafeUser('Bob');
console.log(safeUser.name); // 'Bob'
console.log(safeUser instanceof SafeUser); // true
3. prototype 덮어쓰기
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name}가 소리를 냅니다.`;
};
const dog = new Animal('멍멍이');
console.log(dog.speak()); // "멍멍이가 소리를 냅니다."
// ❌ 프로토타입을 통째로 교체하면 이전 인스턴스는 영향받지 않음
Animal.prototype = {
eat: function(food) {
return `${this.name}가 ${food}를 먹습니다.`;
}
};
const cat = new Animal('야옹이');
console.log(cat.eat); // function (새 프로토타입)
console.log(dog.eat); // undefined (이전 프로토타입 참조)
// ✅ constructor 속성도 복원해야 함
Animal.prototype = {
constructor: Animal, // 명시적으로 추가
eat: function(food) {
return `${this.name}가 ${food}를 먹습니다.`;
}
};
4. 화살표 함수는 생성자로 사용 불가
// ❌ 화살표 함수는 prototype이 없음
const ArrowConstructor = (name) => {
this.name = name;
};
console.log(ArrowConstructor.prototype); // undefined
try {
const instance = new ArrowConstructor('Alice');
} catch (e) {
console.error(e.message); // "ArrowConstructor is not a constructor"
}
// ✅ 일반 함수 사용
function RegularConstructor(name) {
this.name = name;
}
console.log(RegularConstructor.prototype); // { constructor: RegularConstructor }
const instance = new RegularConstructor('Alice'); // 동작
5. 프로토타입 체인의 성능
function DeepChain() {}
// ❌ 너무 깊은 프로토타입 체인
let current = DeepChain.prototype;
for (let i = 0; i < 100; i++) {
const next = Object.create(current);
next[`method${i}`] = function() {
return i;
};
current = next;
}
// 프로토타입 체인이 100단계!
const instance = new DeepChain();
Object.setPrototypeOf(instance, current);
// 메서드 조회가 느려짐 (100단계를 탐색해야 함)
console.time('deep');
instance.method99();
console.timeEnd('deep');
// ✅ 프로토타입 체인은 가능한 짧게 유지
function ShallowChain() {
// 모든 메서드를 한 곳에
}
ShallowChain.prototype.method1 = function() {};
ShallowChain.prototype.method2 = function() {};
ShallowChain.prototype.method99 = function() {};
const shallowInstance = new ShallowChain();
console.time('shallow');
shallowInstance.method99();
console.timeEnd('shallow');
실전 활용
1. 이벤트 에미터 구현
function EventEmitter() {
// 이벤트 리스너를 저장할 객체
this.events = {};
}
EventEmitter.prototype.on = function(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
return this; // 체이닝을 위해 this 반환
};
EventEmitter.prototype.once = function(eventName, listener) {
const wrapper = (...args) => {
listener.apply(this, args);
this.off(eventName, wrapper);
};
return this.on(eventName, wrapper);
};
EventEmitter.prototype.off = function(eventName, listener) {
if (!this.events[eventName]) return this;
this.events[eventName] = this.events[eventName].filter(
fn => fn !== listener
);
return this;
};
EventEmitter.prototype.emit = function(eventName, ...args) {
if (!this.events[eventName]) return this;
this.events[eventName].forEach(listener => {
listener.apply(this, args);
});
return this;
};
// 사용
const emitter = new EventEmitter();
emitter.on('data', (data) => {
console.log('데이터 수신:', data);
});
emitter.once('error', (error) => {
console.error('에러 발생:', error);
});
emitter.emit('data', { id: 1, name: 'Alice' }); // "데이터 수신: ..."
emitter.emit('data', { id: 2, name: 'Bob' }); // "데이터 수신: ..."
emitter.emit('error', new Error('테스트 에러')); // "에러 발생: ..."
emitter.emit('error', new Error('무시됨')); // once이므로 실행 안됨
2. 연결 리스트(Linked List) 구현
function Node(value) {
this.value = value;
this.next = null;
}
function LinkedList() {
this.head = null;
this.tail = null;
this.length = 0;
}
LinkedList.prototype.append = function(value) {
const node = new Node(value);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
return this;
};
LinkedList.prototype.prepend = function(value) {
const node = new Node(value);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
node.next = this.head;
this.head = node;
}
this.length++;
return this;
};
LinkedList.prototype.find = function(value) {
let current = this.head;
while (current) {
if (current.value === value) {
return current;
}
current = current.next;
}
return null;
};
LinkedList.prototype.toArray = function() {
const array = [];
let current = this.head;
while (current) {
array.push(current.value);
current = current.next;
}
return array;
};
// 사용
const list = new LinkedList();
list.append(1).append(2).append(3);
list.prepend(0);
console.log(list.toArray()); // [0, 1, 2, 3]
console.log(list.length); // 4
console.log(list.find(2)); // Node { value: 2, next: Node { ... } }
3. Observable 패턴
function Observable(subscriber) {
this.subscriber = subscriber;
}
Observable.prototype.subscribe = function(observer) {
return this.subscriber(observer);
};
Observable.create = function(subscriber) {
return new Observable(subscriber);
};
// 사용
const observable = Observable.create((observer) => {
let count = 0;
const interval = setInterval(() => {
observer.next(count++);
if (count > 5) {
observer.complete();
clearInterval(interval);
}
}, 1000);
// 구독 취소 함수 반환
return () => {
clearInterval(interval);
console.log('구독 취소됨');
};
});
const subscription = observable.subscribe({
next: (value) => console.log('값:', value),
error: (err) => console.error('에러:', err),
complete: () => console.log('완료!')
});
// 3초 후 구독 취소
setTimeout(() => {
subscription();
}, 3000);
4. 상태 기계(State Machine)
function StateMachine(initialState, transitions) {
this.currentState = initialState;
this.transitions = transitions;
this.history = [initialState];
}
StateMachine.prototype.transition = function(event) {
const stateTransitions = this.transitions[this.currentState];
if (!stateTransitions || !stateTransitions[event]) {
throw new Error(
`"${this.currentState}" 상태에서 "${event}" 이벤트는 허용되지 않습니다.`
);
}
const nextState = stateTransitions[event];
this.currentState = nextState;
this.history.push(nextState);
return this;
};
StateMachine.prototype.getState = function() {
return this.currentState;
};
StateMachine.prototype.can = function(event) {
const stateTransitions = this.transitions[this.currentState];
return !!(stateTransitions && stateTransitions[event]);
};
StateMachine.prototype.getHistory = function() {
return [...this.history];
};
// 사용: 문 상태 기계
const doorMachine = new StateMachine('closed', {
closed: {
open: 'opened'
},
opened: {
close: 'closed',
lock: 'locked'
},
locked: {
unlock: 'closed'
}
});
console.log(doorMachine.getState()); // "closed"
doorMachine.transition('open');
console.log(doorMachine.getState()); // "opened"
doorMachine.transition('lock');
console.log(doorMachine.getState()); // "locked"
console.log(doorMachine.can('unlock')); // true
console.log(doorMachine.can('open')); // false
try {
doorMachine.transition('open'); // 에러!
} catch (e) {
console.error(e.message); // "locked" 상태에서 "open" 이벤트는 허용되지 않습니다.
}
console.log(doorMachine.getHistory()); // ["closed", "opened", "locked"]
클래스 문법과의 비교
ES6부터는 class 문법을 사용할 수 있지만, 내부적으로는 생성자 함수로 동작합니다.
// 생성자 함수 방식
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `안녕하세요, ${this.name}입니다.`;
};
Person.createGuest = function() {
return new Person('Guest', 0);
};
// 클래스 방식 (정확히 같은 동작)
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `안녕하세요, ${this.name}입니다.`;
}
static createGuest() {
return new PersonClass('Guest', 0);
}
}
// 동일한 결과
const person1 = new Person('Alice', 25);
const person2 = new PersonClass('Alice', 25);
console.log(typeof Person); // "function"
console.log(typeof PersonClass); // "function"
console.log(person1.greet()); // "안녕하세요, Alice입니다."
console.log(person2.greet()); // "안녕하세요, Alice입니다."
차이점:
| 특징 | 생성자 함수 | 클래스 |
|---|---|---|
| 호이스팅 | 함수 호이스팅 | 호이스팅 안됨 |
| new 없이 호출 | 가능 (주의 필요) | TypeError 발생 |
| 메서드 열거 | 가능 | 불가능 (non-enumerable) |
| strict mode | 선택 | 자동 적용 |
| 가독성 | 낮음 | 높음 |
| 상속 | 복잡함 | extends로 간단 |
// 호이스팅 차이
const instance1 = new HoistedConstructor(); // ✅ 동작
function HoistedConstructor() {}
try {
const instance2 = new NotHoistedClass(); // ❌ ReferenceError
class NotHoistedClass {}
} catch (e) {
console.error(e.message);
}
// new 없이 호출
function FlexibleConstructor() {
this.value = 42;
}
const result1 = FlexibleConstructor(); // ✅ 동작 (전역 오염 주의)
class StrictClass {
constructor() {
this.value = 42;
}
}
try {
const result2 = StrictClass(); // ❌ TypeError
} catch (e) {
console.error(e.message); // "Class constructor StrictClass cannot be invoked without 'new'"
}
참고 자료
MDN 공식 문서
-
[constructor - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) -
[new operator - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) -
[Function() constructor - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) -
[Object.prototype.constructor - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor) -
[Inheritance and the prototype chain - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) -
[this - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) -
[Classes - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)
댓글