첫 기술 면접을 보고 멘탈이 탈탈 털려버렸다. 왠만한 질문에 제대로 대답하지도 못하는 상태를 느꼈다. 비슷한 질문이더라도, 내가 아는 대로 답했어야했는데, 그냥 해당 부분에 대해선 잘 모르겠다고 넘긴 것이 계속 마음에 걸린다. 같은 질문을 다시 받았을 때, 같은 실수를 반복하지 않기 위해 짧게라도 정리해둘 필요가 있다.
JavaScript는 Class
대신 기존의 객체를 복사하여 새로운 객체를 생성합니다. 이를 프로토타입 방식이라 말합니다.
이러한 이유로 JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)이라 부릅니다. 모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미입니다.
JavaScript에서는 객체 인스턴스와 프로토타입 간에 연결이 구성되며 이 연결을 따라 프로토타입 체인을 타고 올라가며 속성과 메소드를 탐색합니다. 이 과정을 프로토타입을 상속받는 과정이라고 말합니다.
ES2015부터 class 문법을 지원하기 시작했지만 그냥 syntax sugar일 뿐, 자바스크립트는 여전히 prototype 기반 언어인 셈입니다.
상속은 객체지향 프로그래밍의 핵심 개념으로, 어던 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말합니다.
JavaScript는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거합니다. 중복을 제거하는 방법의 대표적인 예로는 기존의 코드를 재사용하는 것이죠.
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); // False
console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
생성자 함수를 이용해서 동일한 프로퍼티를 갖는 객체를 여러개 만들 수 있습니다. 그러나 위 예제의 함수는 문제가 있습니다.
Circle 생성자가 만드는 객체는 모둔 동일한 프로퍼티와 메서드를 가집니다. 프로퍼티의 경우는 계속 달라지지만, getArea의 로직은 모두 같습니다. 그러니 메모리 효율을 위해 중복을 막기 위해서는 프로토타입을 이용하는 것이 좋습니다. 그 예로 엄격 비교 연산자를 통해 검사했을 때 False가 출력되는 것을 확인할 수 있습니다.
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); // true
console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
이렇게 프로토타입을 이용하게 된다면, getArea 메서드는 단 하나만 생성되어 프로토타입을 상속받게 됩니다. 비교 연산자를 적용했을 경우, true가 출력되는 것을 확인할 수 있습니다.
상속은 코드의 재사용이라는 관점에서 매우 유용합니다. 생성자 함수가 생성할 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해둔다면, 인스턴스는 별도의 구현 없이 상위 객체인 프로토타입을 공유받아 사용할 수 있습니다.
프로토타입 객체는 객체간 상속을 구현하기 위해 사용됩니다. 프로토타입 객체의 부모 열할을 하는 객체로서 다른 객체에 공유 프로퍼티,메서드를 제공합니다.
모든 객체는 [[Prototype]]
이라는 내부 슬롯을 가지게 됩니다. 이 내부 슬롯값은 프로토타입의 참조가 됩니다. 여기에 저장는 프로토타입은 객체 생성 방식에 의해 결정됩니다. 즉, 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 슬롯에 저장되는 것이죠.
이것만 기억합시다.
- 모든 객체는 하나의 프로토타입을 갖습니다.
- 모든 프로토타입 생성자 함수와 연결되어 있습니다.
- 객체와 프로토타입과 생성자 함수는 서로 연결되어 있습니다.
그렇다면 객체의 프로토타입은 어떻게 접근할 수 있을까요?
__proto__
을 이용하면 내부 슬롯 [[Prototype]]
에 접근할 수 있습니다.
참고로,
__proto__
는 객체가 직접 소유하는 프로퍼티가 아닙니다.object.prototype.__proto__
로 접근자 프로퍼티를 사용할 수 있습니다.
이때, __proto__
을 쓰는 이유에 대해서 조금만 더 생각해보면, 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서도 있습니다. 이게 무슨 말인지 조금 헷갈릴 수도 있습니다. 이는 뱀이 자신의 꼬리를 삼키는 것과 비슷합니다.
이 그림은 우로보로스라고 합니다. 주로 영원성과 불멸성을 상징하는 그림으로 뱀이 자신의 꼬리를 삼키고 있는 모습입니다. 갑자기 이 얘기를 왜하냐구요?
만약 프로토타입을 서로를 가리키게 된다면, 비정상적입니다. 이럴때 에러를 발생시킵니다.
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
여기서 알 수 있는 것은 프로토타입은 순환하지 않습니다. 단방향으로 흘러야한다는 것입니다. 이렇게 순환참조하게 된다면, 체인의 종점이 없기 때문에, 프로토타입에서 프로퍼티를 검색할 때, 무한 루프에 빠지게 됩니다.
물론 이런 접근자 프로퍼티를 코드내에 사용하는 것은 권장되는 방법은 아닙니다. 만약, ES6에서 직접 참조를 만들어주고 싶다면, object.getPrototypeOf
이라는 메서드를 사용할 것을 권장하고 있습니다.
프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됩니다. 여기서 생성자 함수는 크게 두가지로 나눌 수 있습니다.
생성자 함수로서 호출할 수 있는 함수, constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됩니다.
즉, new 연산자로 생성자 함수를 호출할 때, 객체의 프로토타입도 함께 생성됩니다.
Object, String, Number, Array, Function, Date, Promise ,RegExp 등과 같은 빌트인 함수도 일반 함수와 마찬가지로 생성되는 시점에 프로토타입이 생깁니다. 생성된 프로토타입은 빌트인 함수의 prototype 프로퍼티에 바인딩됩니다.
이건 이전에 정리한 경험이 있습니다. 프로토타입 chain
이란 개념은 JS에서 한 객체의 값을 찾을때, 프로토타입
을 참조하며, 만약에 그렇게도 못찾을 경우엔 프로토타입의 프로토타입까지 찾아 내려가서 결국 마지막 프로토타입에 도달하는 것을 프로토타입 체인
이라고 설명했습니다.
상속 관점에서 자바스크립트의 유일한 생성자는 객체뿐입니다. 각각의 객체는 프로토타입
이라는 은닉 속성을 가지는데 자신의 프로토타입이 되는 다른 객체를 가리킵니다.
그 객체의 프로토타입 또한 프로토타입을 가지고 있고 이것이 반복되다, 결국 null을 프로토타입으로 가지는 오브젝트에서 끝납니다. null은 더 이상의 프로토타입이 없다고 정의되며, 프로토타입 체인의 종점 역할을 하는 셈입니다.
마치 러시아 인형마냥 계속해서 파고 들어가는 것과 비슷하다는 생각이 들었습니다.
이것이 프로토타입의 생성 과정이다. 이전에 공부를 하면서 프로토타입의 체이닝에 대해서 알고 있었지만 이 것을 생성하는 과정이라고 받으니, 머리가 하얘졌다. 이렇게 개념을 연결할 생각을 전혀 하지 못했던 것이 너무 아쉽기도 했다.
다른 말로 하면 몰랐기 때문에 답을 못했던 것이다.
만약 기회가 한번 더 주어진다면, 내가 아는 개념을 조금이라도 이어서 답하는 능력을 탑재해야겠다. 나만의 언어에 갇히지 말고, 사람들과 대화하며 학습해야겠다.