생성자 함수란 함수로 객체를 정의하는 방법을 말한다. (= 객체를 찍어내는 함수)
함수의 형태이기 때문에 초기값 지정 등 함수의 특징을 그대로 사용할 수 있다.
일반적인 함수와 역할을 구분해주기 위해 파스칼 케이스로 이름을 선언하는 관례가 있다.
function User() {
this.name = "철수";
this.age = 30;
}
const u = new User();
매개변수를 전달받을 수 있어 유연하게 사용할 수 있다.
function Car(name, color) {
this.name = name;
this.color = color;
this.getInfo = function () {
return `${this.name}, ${this.color}`;
}
}
const car1 = new Car("benz", "white");
생성자 함수로 생성된 객체는 항상 인스턴스를 그대로 물려받는다. 위 예시에서 getInfo라는 함수는 항상 필요한 인스턴스가 아니며, 공간을 많이 차지하기 때문에 이 방식은 비효율적이다.
이 비효율성은 프로토타입을 잘 활용하면 해결할 수 있다.
현재는 함수에 name, color, getInfo라는 프로퍼티가 모두 있는 거지만, 함수에는 name, color 를, 프로토타입에는 getInfo 를 넣어주는 식으로 변경하면 생성자 함수로 객체를 생성했을 때 기본 속성은 name, color 를 가지고, 필요할 때만 getInfo 를 사용할 수 있도록 생성이 가능하다.
모든 함수는 자신과 1 : 1로 매칭되는 공간을 소유하는데, 이 공간을 프로토타입이라고 부른다.
프로토타입은 constructor와 __proto__를 가지고 있다.
constructor는 자신을 만든 생성자 함수, __proto__는 상위 객체를 가리킨다.
** 주의 : 화살표 함수는 프로토타입이 존재하지 않는다.
프로토타입은 아무런 조작을 하지 않아도 무조건 constructor와 __proto__를 가지고 있다.
constructor 는 자신을 만든 생성자 함수(아래 예시에서는 Person() 함수)를 가리키고, __proto__는 상위 객체를 가리킨다.
이렇게 상위 객체를 타고 올라가는 걸 프로토타입 체이닝이라고 한다.

function Car(name, color) {
this.name = name; // 멤버 속성 (클래스나 객체 내부에 정의된 변수)
this.color = color; // 멤버 속성
}
Car.prototype.getInfo = function () {
return `${this.name}, ${this.color}`;
};
const car1 = new Car("benz", "white"); // 생성자 함수 Car의 인스턴스 생성
console.dir(car1);
// 1. car1이라는 빈 객체를 생성
// 2. 해당 객체에 생성자 함수 Car의 인스턴스를 복사
const car2 = new Car("bmw", "black");
console.dir(car2);
매번 필요하지 않거나, 한 곳에서만 호출해도 되는 속성이라면, 인스턴스 생성마다 물려받는 것은 불필요한 메모리를 할당해서 메모리 누수를 일으킬 수도 있다.
이 경우 위의 예제처럼 생성자 함수의 프로토타입에 해당 속성을 정의한다면 불필요하게 물려받을 필요도 없고, 메모리가 절약되어 효율적으로 사용할 수 있다.
[ 결론 ]
값이 변하지 않고 공통적으로 사용되는 속성은 프로토타입 객체에, 그게 아니면 생성자 함수 내부에 정의하는 것이 좋다.
__proto__ 자세히 알아보기__proto__ 를 사용해서 프로토타입에 접근할 수 있다.
하지만 자바스크립트는 기본적으로 __proto__ 를 생략할 수 있도록 해준다.
그렇다면 두 호출 결과가 같은가? 그렇지 않다.
console.dir(car1.__proto__.getInfo()); // undefined, undefined
console.dir(car1.getInfo()); // __proto__ 생략 가능
위 예제와 같이 __proto__ 를 사용하면 undefined가 출력되고, __proto__ 를 생략했을 때는 인스턴스의 값을 정상적으로 출력한다. 이것은 두 경우의 this 바인딩이 다르기 때문이다.
💡 각 케이스의 this 바인딩
car1.__proto__.getInfo()를 호출할 때는 this가 프로토타입 객체를 가리키므로 undefined가 출력된다.car1.getInfo()를 호출할 때는 this가 car1 인스턴스를 가리키므로 정상적인 값이 출력된다.
따라서 __proto__의 형태로 사용하는 일은 없다고 봐도 무방하다.
프로토타입 체인(체이닝)
프로토타입을 타고 타고 올라가다 보면 결국 Object()가 나온다.
이를 통해 hasOwnProperty 등등은 Object() 의 프로토타입에 정의되어 있음을 알 수 있다.
자바스크립트 엔진이 기본 자료형의 값을 객체처럼 사용하기 위해 암묵적으로 만드는 객체
const PI = 3.13413412341234;
console.log(PI.toFixed(2)); // PI를 Number로 감싸줬기 때문에 toFixed를 사용할 수 있음
인스턴스를 만들고 나서 객체의 속성을 조작하면 외부에서 속성의 값이 조작이 되기 때문에 예기치 않은 문제가 발생할 수 있다.
function Counter() {
this.count = 0;
}
Counter.prototype.increment = function () {
this.count++;
};
const counter = new Counter();
counter.count = 100; // 외부에서 자유롭게 변경 가능 (위험 요소)
지역 함수 + 클로저로 프라이빗하게 관리
function BankAccount(initialBalance) {
let balance = initialBalance;
this.deposit = function (amount) {
balance += amount;
};
this.withdraw = function (amount) {
balance -= amount;
};
}
const woori = new BankAccount(1000);
woori.deposit(2000);
woori.balance = 10000000; // 외부에서 직접 접근 불가
function Cart() {
let items = [];
let totalAmount = 0;
this.addItem = function(item) {
items.push(item);
totalAmount += item.price;
};
this.getCartInfo = function() {
return {
items: [...items],
totalAmount: totalAmount
};
};
this.checkout = function() {
// 결제 처리 로직...
};
this.processPayment = function(amount) {
// 실제 결제 처리 로직
};
}
function createCart() {
return new Cart();
}
const myCart = createCart();
call()을 이용해서 구현하며, 생성자 함수를 사용해서 프로퍼티를 생성하는 것이 const person = new Person("인강"); 빈 객체를 생성해서 그 안에 생성자 함수의 멤버 속성을 넣어주는 거라면, function Developer(name, position) { Person.call(this, name); // Person 상속} Call()을 사용한 상속은 생성자 함수와 상관 없는 객체에 생성자 함수의 프로퍼티를 사용할 수 있도록 넣어주는 개념이라고 할 수 있다.
이 때 call은 생성자 함수의 인스턴스 프로퍼티만 상속한다.
원래 상속이라는 개념은 클래스에만 존재하는데, 프로토타입을 어떻게 해서든 상속 되는 것처럼 보이게 하려고 만든 패턴이다.
[ Call ]
call()은 이미 할당되어있는 다른 객체의 함수/메소드를 호출하는 해당 객체에 재할당할때 사용됩니다. this는 현재 객체(호출하는 객체)를 참조합니다. 메소드를 한번 작성하면 새 객체를 위한 메소드를 재작성할 필요 없이 call()을 이용해 다른 객체에 상속할 수 있습니다. // 출처 : mdn
function Person(name) {
this.name = name;
}
Person.prototype.introduce = function () {
return `I am ${this.name}`;
};
function Developer(name, position) {
Person.call(this, name);
this.position = position;
}
Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;
Developer.prototype.skill = function () {
return this.position;
};
const dev = new Developer("철수", "프론트개발자");
Person.isAdult
이 형태로 작성된 메서드는 Person 생성자 함수 자체에 붙는 정적(static) 메서드가 된다.
정적 메서드는 인스턴스가 아닌 생성자 함수(Person)를 통해서만 호출할 수 있습니다. 즉, john.isAdult()처럼 인스턴스에서 접근할 수 없다.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Person 생성자 함수 자체에 메서드 추가 (정적 메서드)
Person.isAdult = function () {
console.log(this); // Person 생성자 함수 자체가 this로 출력됨
if (this.age >= 18) return true; // this.age가 undefined라서 오류 발생
return false;
};
const john = new Person("John", 20);
console.log(john.isAdult()); // TypeError: john.isAdult is not a function
⇒ 따라서, 인스턴스 메서드를 추가하려면 프로토타입이나 생성자 내부에서 정의해야 한다.
**근데 이제 반쪽짜리인…!**
보통 객체 지향은 클래스 기반의 객체 지향 언어를 가리킨다. 하지만 자바스크립트는 클래스 기반이 아니라 프로토타입 기반이다.
클래스가 뒤늦게 추가되긴 했지만 프로토타입을 감싸서 흉내낸 것일 뿐이라 반쪽짜리임
🤖 GPT
(ES6에서
class문법이 도입되었지만, 이는 프로토타입 기반 상속을 더 쉽게 사용할 수 있도록 한문법적 설탕(Syntactic Sugar)일 뿐, 실제로는 여전히 프로토타입 기반으로 동작합니다.)
래퍼 객체(Wrapper Object)는 원시값(primitive value)이 객체처럼 동작해야 할 때 avaScript 엔진이 자동으로 생성했다가, 해당 작업이 끝나면 바로 제거된다.
오늘 개념은 생성자 함수, 프로토타입, 클로저를 모두 이해해야 완전히 소화할 수 있는 내용이라 이해하는 데 오래 걸렸다.
특히 상속과 팩토리 패턴은 조금 더 공부 해봐야 할 것 같다.
예전이라면 모르는 게 있어도 적당히 모른 채로 넘겼을 수도 있는데, 요즘에는 알 때까지 공부하게 된다. 좋은 동료들 덕분인듯...!