JavaScript는 프로토타입(prototype) 기반의 언어라고 한다. 그래서 프로토타입이 뭘까? 프로토타입 체인은 뭘까?
내 얕은 지식을 바탕으로 정리하자면, 클래스와 프로토타입은 둘 다 객체 지향이라는 개념을 구현하기 위한 방법(론)이고, 둘의 가장 커다란 차이는 프로토타입의 인스턴스 자체가 상속과 다형성을 지원하느냐 아니냐인 것 같다.
클래스는 클래스와 인스턴스 사이의 차이가 명백하다. 클래스는 일종의 틀이고, 인스턴스는 그 틀의 조건을 만족하는 실물이다. 이러한 클래스 기반 언어와 다르게, 프로토타입 기반 언어의 객체는 그러한 구분이 적다. 객체 자체가 또 하나의 프로토타입으로 기능할 수 있는 것이다.
JavaScript에서 모든 객체는 자신의 프로토타입 객체를 참조하는 숨겨진 속성 'prototype'을 갖는다. 혹자는 이러한 프로토타입을 "유전 정보"라고 표현하던데 굉장히 잘 맞는 비유라고 생각한다. 이렇듯 각 객체가 오롯한 상속 가능성을 보유함으로써 보다 유연한 상속과 다형성을 지원할 수 있는데, 이러한 연결 관계를 프로토타입 체인이라고 부른다.
추상적인 이야기를 했으니 구체적인 부분도 살펴봐야 할 것이다.
function Person(name, job) {
this.name = name
this.job = job
}
Person.prototype.greet = function() {
console.log(`내 이름은 ${this.name}, ${this.job}이죠.`)
}
const person1 = new Person('코난', '탐정')
person1.greet() // "내 이름은 코난, 탐정이죠."
function Employee(name, job, salary) {
Person.call(this, name, job) // 상속 받을 대상의 생성자를 호출
this.salary = salary
}
// Employee가 Person을 상속
Employee.prototype = Object.create(Person.prototype)
Employee.prototype.constructor = Employee
Employee.prototype.describeSalary = function() {
console.log(`한 달에 ${this.salary}만원 벌어요.`)
}
const employee1 = new Employee('홍길동', '의적', 3000)
employee1.greet() // "내 이름은 홍길동, 의적이죠."
employee1.describeSalary() // "한 달에 3000만원 벌어요."
특기할 만한 부분은 함수 Person이 new 키워드와 함께 호출되면 객체의 생성을 담보한다는 점이다. new 키워드가 동반되면 함수의 this는 객체를 새로 생성하여 해당 객체를 바인딩한다. 또한 함수가 명시적인 반환값이 없을 경우 this에 바인딩된 객체를 반환한다. 당연히 new 키워드가 동반되지 않으면 주어진 맥락에 따라 this를 바인딩하고, 아무것도 반환하지 않는다(undefined). 생성자로써의 기능을 강제하고 싶다면 다음과 같이 작성할 수도 있다.
function Person(name, job) {
if (!(this instanceof Person)) {
return new Person(name, job);
}
this.name = name;
this.job = job;
}
프로토타입 체인이란 Python의 LEGB rule과 비슷하다. LEGB rule이란 현재 scope에 호출 대상이 없으면 scope를 거슬러 올라가며 찾는 것을 말한다. JS의 프로토타입 체인 또한 현재 객체에 호출한 속성이나 메서드가 없다면 "상속의 흐름"을 거슬러 올라가며 대상을 찾는다. 아래의 예시를 살펴보면 이해에 도움이 될 것이다.
function Person(name, job) {
this.name = name
this.job = job
}
// 객체를 먼저 생성한다
const person1 = new Person('코난', '탐정')
Person.prototype.greet = function() {
console.log(`내 이름은 ${this.name}, ${this.job}이죠.`)
}
// 그래도 호출이 잘 된다!
person1.greet() // "내 이름은 코난, 탐정이죠."
기본적으로 .prototype은 생성자 함수가 갖고 있다. Array도 생성자 함수의 일종이다.
const arr1 = [1, 2, 3]
const arr2 = new Array(1, 2, 3)
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3]
둘은 동등한 선언이다. [...]을 통한 선언은 편의를 위한 일종의 syntax sugar라고 할 수 있다.
객체 인스턴스는 생성자가 아니므로 .prototype이 없는데, 그러면 프로토타입 체인의 형성이 불가능한가? 아니다. 인스턴스 자체를 프로토타입으로 삼을 수 있다.
function Person(name, job) {
this.name = name;
this.job = job;
}
Person.prototype.greet = function() {
console.log(`내 이름은 ${this.name}, ${this.job}이죠.`)
}
const person1 = new Person('코난', '탐정')
person1.greet() // "내 이름은 코난, 탐정이죠."
// person1을 프로토타입으로 삼는 새로운 객체 생성
const child = Object.create(person1)
console.log(child.name) // "코난"
console.log(child.job) // "탐정"
child.greet() // "내 이름은 코난, 탐정이죠."
child.name = '홍길동'
child.job = '의적'
child.greet() // "내 이름은 홍길동, 의적이죠."
person1.greet() // "내 이름은 코난, 탐정이죠."
console.log(child.age) // undefined
person1.age = 15
console.log(child.age) // 15
마지막 두 줄을 통해 단순한 깊은 복사가 아니라 프로토타입 체인이 형성되었음을 확인할 수 있다.
ES6에서는 프로토타입에 적절한 껍데기를 씌워서(이 또한 syntax sugar다) 클래스 기반의 코드를 작성할 수도 있다.
class Person {
constructor(name, job) {
this.name = name
this.job = job
}
greet() {
console.log(`내 이름은 ${this.name}, ${this.job}이죠.`)
}
}
class Employee extends Person {
constructor(name, job, salary) {
super(name, job) // 부모 클래스의 생성자 호출
this.salary = salary
}
describeSalary() {
console.log(`한 달에 ${this.salary}만원 벌어요.`)
}
}
const employee1 = new Employee('홍길동', '의적', 3000)
employee1.greet() // "내 이름은 홍길동, 의적이죠."
employee1.describeSalary() // "한 달에 3000만원 벌어요."
그렇다면 프로토타입 기반의 코드에서는 게시글 초반에 썼던 것처럼 매번 프로토타입에 정보를 추가해줘야 하는 걸까? 그렇지는 않다. 클래스 기반 문법과의 중간 지점이 존재한다.
const Person = {
init(name, job) {
this.name = name
this.job = job
return this
},
greet() {
console.log(`내 이름은 ${this.name}, ${this.job}이죠.`)
}
}
const Employee = Object.create(Person)
Employee.init = function(name, job, salary) {
Person.init.call(this, name, job)
this.salary = salary
return this
}
Employee.describeSalary = function() {
console.log(`한 달에 ${this.salary}만원 벌어요.`)
}
const employee1 = Object.create(Employee).init('홍길동', '의적', 3000)
employee1.greet() // "내 이름은 홍길동, 의적이죠."
employee1.describeSalary() // "한 달에 3000만원 벌어요."
사실 프로토타입이 신기하긴 한데 고유한 장점이 뭔진 감이 잘 안 온다. 보다 유연해보이는 건 알겠는데 그 유연성으로 클래스는 해내지 못하는 일을 해내는 경우가 뭐가 있을지 잘 모르겠다. 이 부분은 앞으로 공부를 더 해봐야겠다.
'Study > JavaScript' 카테고리의 다른 글
[Vue.js] props (0) | 2024.05.08 |
---|---|
this (0) | 2024.04.21 |