자바스크립트는 무엇인가? 프로그래밍 언어 관점에서 정의하자면 다음과 같다. 함수형 언어, 명령형 언어, 프로토타입 기반 언어, 객체지향 언어. 오늘 주목할 건 이중에서 마지막 두 단어를 결합한 개념이다.
그럼 프로토타입 기반의 객체지향 언어는 무엇일까?
객체지향 프로그래밍은 프로그램을 구성하는 거의 대부분이 객체라는 의미이다. 객체는 상태를 나타내는 데이터(프로퍼티)와 동적 기능을 보여주는 데이터(메서드)의 집합으로, 이를 key와 value로 나타낸다. 객체마다 고유한 특성을 지녔다고 볼 수 있다.
그런데 종종 객체들 간 공통 부분이 존재한다.
예를 들어, 원(circle)의 넓이를 구한다고 하자.
넓이를 구하는 함수 즉 메서드는 반지름 ** 2 * Math.PI
다.
function Circle(radius) {
this.radius = radius;
this.getArea: function () { // Circle.prototype의 메서드
return this.radius ** 2 * Math.PI
};
}
const circle1 = new Circle(5); // 반지름 5인 인스턴스 생성
const circle2 = new Circle(10); // 반지름 10인 인스턴스 생성
생성자 함수* Circle을 new 연산자와 함께 호출해 각각 반지름이 5와 10인 인스턴스를 만들었다.
생성자 함수?
말 그대로 객체를 생성하는 함수를 가리킨다. 함수 이름의 첫머리가 대문자이고, new 연산자와 함께 호출한다. return 없이도 새로운 객체를 반환하는데 그렇게 생성된 객체를 '인스턴스'라고 부른다.
function Person(name) { this.name = name; } const user = new Person('chocol'); // 인스턴스 생성 console.log(user); // Person {name: 'chocol'}
그런데 두 인스턴스는 반지름만 다를 뿐 메서드가 동일하다. 지금 구조로는 똑같은 메서드를 2번 생성한 것과 다름 없다.
인스턴스 객체의 공통 부분을 하나만 작성해 두 객체가 똑같은 메서드(혹은 프로퍼티)를 참조할 수 있게 한다면?
👍 코드를 재사용할 수 있어서 활용도가 높아지고
👍 메모리 낭비를 줄일 수 있다.
👍 똑같은 메서드를 하나로 통일했으니 퍼포먼스도 향상된다.
이 장점을 활용할 수 있는 이유, 자바스크립트의 고유한 프로토타입 상속 덕분이다.
자바스크립트의 프로토타입은 원형(prototype)이 되는 객체를 참조하는 객체를 생성하여 클래스의 상속과 비슷한 효과를 만든다.
생성자(Constructor) 함수로 객체를 생성해 인스턴트를 만들면, 생성자 함수가 지닌 prototype 프로퍼티(메서드 포함)가 인스턴스의 __proto__
로 상속되는 것이다. 이를 도식화하면 아래와 같다.
__proto__
라는 접근자 프로퍼티*가 자동으로 부여된다.
__proto__
접근자 프로퍼티
크롬 브라우저 콘솔에 객체 리터럴 방식으로 객체 하나를 생성하면 '숨겨진' 내부 슬롯 [[prototype]]이 보인다.
크롬을 비롯한 브라우저는 슬롯에 직접 접근하는__proto__
접근자 프로퍼티를 사용했다. 숨겨진 내부 슬롯에 사용자가 직접 조작하는 방식이라서 ES는 권장하지 않았지만, 브라우저 측은 이 방식을 계속 유지했다. 결국 ES6부터 브라우저 호환성 유지 차원에서 사용을 공식화했다.
하지만 여전히 직접 접근은 권장하는 건 아니다. 브라우저가 아닌 환경에서 이 방식이 어떻게 동작할지 알 수 없기 때문이다. 안정성을 위해 객체의 프로퍼티에 접근할 때엔 아래의 메서드를 사용하길 권한다.
- 객체의 프로퍼티 참조 : Object.getPrototypeOf / Object.create()
- 객체의 프로퍼티 수정 : Object.setPrototypeOf
다만 프로토타입 이해하기엔__proto__
가 용이해서 포스팅에선 이렇게 표기하는 중이다.
위의 Circle 예제를 똑같이 그려보자.
Circle의 prototype 프로퍼티로는 getArea()가 있다. Circle의 인스턴스인 circle1, circle2가 이 메서드를 사용할 수 있는 이유는 바로 circle의 __proto__
가 Circle prototype 프로퍼티를 상속 받기 때문이다.
__proto__
의 관계🔺 __proto__
접근자 프로퍼티
🔺 prototype 프로퍼티
쉽게 말해 두 프로퍼티는 동일한 프로토타입을 가리킨다. (이유는 단연 상속 때문) 다만 사용 주체가 객체, 생성자 함수로 다를 뿐이다.
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티
Person.prototype.getName = fucntion() {
return this.name;
};
const me = new Person('chocol');
// 생성자 함수의 프로퍼티와 인스턴스의 프로퍼티 비교
console.log(Person.prototype === me.__proto__); // true
그리고 __proto__
는 결국 숨겨진 내부 슬롯을 구현한 대상이라선지 생략이 가능하다. this를 객체 자신에게 두려면 오히려 생략이 필요하기도 하다.
왜냐, 메서드는 바로 앞에 적은 대상을 this로 간주하여 호출한다.
me.__proto__getName()
는 me.__proto__
가 this이다. 그런데 getName()
는 prototype에 있지 __proto__
에 존재하지 않는다. 단지 prototype의 프로퍼티를 상속 받아 참조하고 있으니 말이다.
아마 이 점을 고려해서 생략을 가능하게 만든 건가 싶은데 자바스크립트 창시자가 그렇게 정해둔 것이라 그러려니 하면 된다.
me.getName() === Person.prototype.getName()
그나저나 프로토타입 상속은 어떤 원리로 이루어지는 걸까?
자바스크립트가 프로퍼티 검색하는 방식에 그 답이 있다.
먼저 해당 객체에서 프로퍼티를 찾아보고, 여기에 없으면 객체가 참조하는 대상 즉 prototype 프로퍼티에서 검색한다. 여기에도 없으면 최상위 프로퍼티까지 확장해가며 찾는다. (스코프 체인과 비슷한 형상)
그래서 오버라이딩 현상이 발생하기도 한다.
예시를 통해 보자.
const Person = function (name) {
this.name = name;
};
// prototype의 getName 메서드
Person.prototype.getName = function() {
return this.name;
}
const user = new Person('chocol');
// __proto__에 동명의 메서드가 있다
chocol.getName = function() {
return 'Hi!' + this.name;
};
console.log(chocol.getName()); // Hi! chocol
앞의 이름 예시를 다시 가져와 이번엔 인스턴트에 직접 prototype 프로퍼티와 동명의 메서드인 getName을 작성했다.
코드 마지막 줄에서 인스턴스의 __proto__
가 생략된 상태로 메서드를 호출했다. 그래서 chocol 인스턴스에 getName 메서드가 있는지 확인해보니 존재한다. 물론 prototype 프로터티에도 같은 이름의 메서드가 있지만, 이미 검색이 끝난 상태라 그 내용을 덮어씌웠다.
prototype의 프로퍼티가 삭제된 게 아니다. 그래서 chocol.prototype.getName()
으로 Person 생성자 함수의 메서드를 불러올 수 있다.
이쯤에서 생성자 함수도 객체라는 사실에 주목해보자. 그럼 생성자 함수의 상위엔 무엇이 있을까? 바로 Object.prototype이 있다.
결국 프로토타입 체이닝 최상단에는 언제나 Object.prototype가 있다. 사실상 여기서 모든 상속이 시작된다고 볼 수 있다.
✅ 자바스크립트는 프로토타입 기반 객체지향 프로그래밍이다.
✅ 이는 생성자 함수를 new 연산자와 함께 호출해 객체를 생성하는 방식이다.
✅ [[prototype]] 내부 슬롯의 독특한 동작으로 프로토타입 상속이 가능하다.
✅ 어떤 생성자 함수든 객체이므로 프로토타입 체이닝의 최상단은 언제나 Object.property이다.
✅ 모든 prototype 프로퍼티는 constructor라는 프로퍼티를 가진다. 이는 객체가 참조하는 대상을 담는다.
📚 책 자바스크립트 딥 다이브
📚 책 코어 자바스크립트
📄 프로토타입 상속
누군가에게 설명할 수 있어야 진짜 내 지식인데 프로토타입은 쓰면서도 헷갈리는 지점이 많았다. 대강 내용을 써넣을 순 있었지만 그러면 절대 내 것이 될 수 없어서 모르겠는 파트는 과감히 넘겼다. 때로는 돌아가는 길이 가장 빠르다.
딥 다이브 책 읽다가 포기하게 된 지점이 프로토타입... 할 수 있을까 싶었는데 다시 읽어보니 전보다는 이해할 수 있는 지점이 생겼다.
프로토타입이 왜 어려울까? 생각해봤는데 답은 명쾌하다. 체감이 안 된다.
JSON의 구조가 왜 key와 value로 구별되는지 궁금했는데 프로토타입 기반이라서 그랬군요. 잘 배우고 갑니다.