1. 팩토리 함수 방식
객체를 일관성 있게 생성하기 위해 팩토리 함수를 사용할 수 있다. 이 방식은 내부적으로 객체를 생성하고 속성을 부여한 뒤 반환한다.
function createUser(username, userAge) {
let obj = new Object();
obj.username = username;
obj.userAge = userAge;
obj.introduce = function() {
console.log(`안녕, 나는 ${obj.username}이야.`);
};
return obj;
}
let user1 = createUser('홍길동', 25);
user1.introduce();
let user2 = createUser('김철수', 30);
user2.introduce();
이 방법의 장점은 유사한 구조의 객체를 반복해서 만들 수 있다는 점이다. 그러나 생성된 인스턴스가 어떤 타입인지 식별할 수 없다는 단점이 있다.
2. 생성자 함수 방식
생성자 함수는 new 키워드와 함께 호출되어 객체를 생성한다. 일반 함수와 동일한 문법을 가지지만, 호출 방식에 따라 차이가 생긴다.
function User(fullName, years) {
this.fullName = fullName;
this.years = years;
this.greet = function() {
console.log(`반가워, 난 ${this.fullName}야.`);
};
}
생성자 함수는 명시적으로 객체를 생성하거나 반환하지 않으며, 속성을 this에 직접 할당한다.
let person1 = new User('박민수', 22);
person1.greet(); // 반가워, 난 박민수야.
let person2 = new User('이지영', 27);
person2.greet(); // 반가워, 난 이지영야.
생성 과정은 다음과 같은 단계로 이루어진다:
- 새로운 객체 생성
- 생성자의 스코프를 새 객체에 바인딩 (
this가 새 객체를 가리킴) - 함수 내부 로직 실행
- 새 객체 반환
생성자 함수는 일반 함수처럼 호출될 수도 있지만, 이 경우 this는 전역 객체(window)를 참조하게 된다.
User('최현우', 20);
window.greet(); // 반가워, 난 최현우야.
생성자로 만든 인스턴스는 해당 생성자뿐 아니라 Object의 인스턴스이기도 하다.
console.log(person1 instanceof Object); // true
console.log(person1 instanceof User); // true
단점으로는 메서드가 각 인스턴스마다 별도로 생성되어 공유되지 않는다는 점이다.
console.log(person1.greet === person2.greet); // false
3. 프로토타입 기반 생성
모든 함수는 prototype 속성을 가지며, 이를 통해 인스턴스 간 공통 속성과 메서드를 공유할 수 있다.
function Student() {}
Student.prototype = {
constructor: Student,
studentName: '기본 이름',
studentAge: 20,
getInfo: function() {
console.log(`${this.studentName}, ${this.studentAge}살`);
}
};
let s1 = new Student();
let s2 = new Student();
console.log(s1.getInfo === s2.getInfo); // true
s1.getInfo(); // 기본 이름, 20살
인스턴스들은 프로토타입의 속성과 메서드를 공유하므로 메모리 효율적이다.
isPrototypeOf() 메서드는 프로토타입과 인스턴스 간 관계를 확인할 때 사용된다.
console.log(Student.prototype.isPrototypeOf(s1)); // true
인스턴스에 프로토타입과 동일한 이름의 속성을 추가하면, 해당 인스턴스에서만 새로운 속성이 생성되며 프로토타입 값은 가려지게 된다.
s1.studentName = '새이름';
console.log(s1.studentName); // 새이름
console.log(s2.studentName); // 기본 이름
해당 속성을 삭제하면 다시 프로토타입 값을 참조하게 된다.
delete s1.studentName;
console.log(s1.studentName); // 기본 이름
hasOwnProperty()는 해당 속성이 인스턴스 자체에 존재하는지를 검사하며, in 연산자는 인스턴스나 프로토타입 중 어디에 있든 상관없이 존재 여부를 반환한다.
console.log(s1.hasOwnProperty('studentName')); // false
console.log('studentName' in s1); // true
s1.studentName = '변경됨';
console.log(s1.hasOwnProperty('studentName')); // true
console.log('studentName' in s1); // true
console.log(s2.hasOwnProperty('studentName')); // false
console.log('studentName' in s2); // true
두 메서드를 조합하여 속성이 프로토타입에 존재하는지 판단할 수 있다.
function isInPrototype(obj, prop) {
return !obj.hasOwnProperty(prop) && (prop in obj);
}
let std1 = new Student();
console.log(isInPrototype(std1, 'studentName')); // true
std1.studentName = '로컬값';
console.log(isInPrototype(std1, 'studentName')); // false
프로토타입은 동적으로 수정할 수 있으며, 변경 사항은 즉시 모든 인스턴스에 반영된다.
function Developer() {}
let dev = new Developer();
Developer.prototype.work = function() {
console.log("개발 중...");
};
dev.work(); // 개발 중...
단점은 모든 인스턴스가 프로토타입의 속성을 공유하기 때문에 개인화된 데이터를 처리하기 어렵다는 것이다.
4. 생성자 + 프로토타입 조합 방식
가장 널리 사용되는 방식으로, 생성자 함수로 인스턴스 고유 속성을 정의하고, 프로토타입으로 공통 메서드를 정의한다.
function Employee(empName, empAge) {
this.empName = empName;
this.empAge = empAge;
this.projects = ['프로젝트A', '프로젝트B'];
}
Employee.prototype = {
constructor: Employee,
introduceSelf: function() {
console.log(`안녕, 나는 ${this.empName}야.`);
}
};
let e1 = new Employee('김영희', 28);
e1.introduceSelf(); // 안녕, 나는 김영희야.
let e2 = new Employee('이상훈', 32);
e2.introduceSelf(); // 안녕, 나는 이상훈야.
e1.projects.push('프로젝트C');
console.log(e1.projects); // ["프로젝트A", "프로젝트B", "프로젝트C"]
console.log(e2.projects); // ["프로젝트A", "프로젝트B"]
5. 동적 프로토타입 방식
생성자 내부에서 프로토타입을 조건부로 초기화하여 코드의 일관성을 유지할 수 있다.
function Teacher(tName, tSubject) {
this.tName = tName;
this.tSubject = tSubject;
if (typeof this.describe !== 'function') {
Teacher.prototype.describe = function() {
console.log(`${this.tSubject} 교사 ${this.tName}입니다.`);
};
}
}
let teacher1 = new Teacher('박선생', '수학');
teacher1.describe(); // 수학 교사 박선생입니다.
let teacher2 = new Teacher('최선생', '영어');
teacher2.describe(); // 영어 교사 최선생입니다.
이 방식에서는 프로토타입의 특정 메서드 존재 여부만 확인하면 되며, 객체 리터럴로 프로토타입 전체를 덮어쓰는 것은 피해야 한다.