[10분 테코톡] 💼 크리스의 Prototype(12분) 를 보고 정리하면서 추가적인 내용이 있는 글입니다 :)
console.log를 주도로 개발을 하다보면, 객체 안에 집어넣은 기억이 없는 __proto__
라는 속성이 항상 자리잡고 있는 것을 볼 수 있습니다.
자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있습니다. 그리고 이것은 마치 객체 지향의 상속 개념과 같이 부모 객체의 프로퍼티 또는 메소드를 상속받아 사용할 수 있게 합니다. 이러한 부모 객체를 Prototype(프로토타입)객체 또는 줄여서 Prototype(프로토타입)이라 하니다.
사람들이 Prototype을 공부하면서 가장 많이 잘못 알고있는 경우가 있습니다.
바로 Prototype에 클래스 개념을 끌어들여서 이해하는 것입니다. 많은 글들이 Prototype을 설명할때 'JavaScript에서는 객체를 상속하기위하여 프로토타입이라는 방식을 사용합니다.' 라고 설명합니다.
그래서 흔히들 클래스(class)의 상속을 떠올리기 쉬운데요 하지만 엄밀히 말해서 Prototype의 상속은 클래스의 상속과는 엄연히 다른 개념입니다. 왜냐하면 자바스크립트에는 클래스가 없기 때문입니다. 그리고 내용을 복사해서 일어나는 상속 또한 자바스크립트 내에서는 존재하지 않기 때문입니다.
Prototype은 클래스, 객체의 내용 복사 없이도 상속을 구현할 수 있게 해주는 방법입니다. 이걸 위해서 Prototype이 하는 일은 '상속'이 아니라 '연결'이라고 이해하면 될 것 같습니다.
클래스는 그림처럼 객체를 찍어내는 틀 입니다. 자바와 같은 언어에서는 클래스를 가지고 new 연산자와 함께 실행하면 그 틀에 형식에 맞춘 하나의 새로운 객체가 만들어집니다. 하지만 자바스크립트에는 이런 기능이 없습니다.
이미지의 오른쪽 처럼 함수를 활용해서 클래스 기능을 흉내낼 뿐입니다. 즉 실제로 실행되는 코드는 클래스가 아닙니다.
// 2. 생성된 빈 객체가 this에 바인딩 됨
function Person(name) {
// 3. this 객체의 속성을 채우는 동작이 수행됨
this.name = name;
this.sayHello = function() {
console.log(`${this.name}: hello!`);
}
// 4. return 하는 것이 없다면 그렇게 만들어진 this가 return됨
}
const chris = new Person('chris'); // 1. new연산자가 새로운 빈 객체를 메모리상에 생성함
함수 자체를 잘 들여다 보면 이상한 점이 있습니다. Person함수는 return을 해주는게 하나도 없는데 chris에는 객체가 하나 생성되어서 담기게 됩니다.
어떻게 이런게 가능할까요?🤔
함수와 new 연산자가 만나면 자바스크립트의 한 부분에서 숨겨진 일들이 일어나기 때문입니다.
일반적인 클래스에서 어떤 클래스가 부모 클래스로부터 상속을 받게 된다면, 자식 클래스로 만들어지는 인스턴스에는 부모와 자식 모두의 내용이 합쳐진 내용이 그대로 복사됩니다.
하지만 자바스크립트에도 이런 식의 상속이 가능하냐고 누군가 물어본다면 불가능하다고 말할 수 있습니다.
왜냐하면 자바스크립트에서 '상속이 복사를 의미하는 것'이라면 그런 의미에서 상속이 절대 자바스크립트에서 일어날 수 없기 때문입니다. 자바스크립트는 객체 자체나 코드 내용을 복사하는 깊은 복사를 수행하지 않습니다. 복사할 수 있는 건 오로지 원시값과 객체의 참조값 뿐입니다.
그래서 자바스크립트는 이 상속을 흉내내기 위해서 객체간의 연결이라는 개념을 활용합니다. 이 연결은 __proto__
라는 이름의 속성을 바탕으로 수행됩니다. 자바스크립트의 모든 객체들은 __proto__
라는 속성을 가지고 있는데 이는 객체와 객체간을 연결하는 하나의 링크라고 보면 됩니다. 이런 객체와 객체간의 링크 관계는 크게 3가지로 분리될 수 있습니다.
객체는 자신의 원형이라고 할 수 있는 객체가 있다면 그 객체를 가리키는__proto__
링크를 자동으로 속성으로 가집니다.
cosnt newObj = Object.create(oldObj);
newObj.__proto__ === oldObj // true
이 경우에는 __proto__
외에도 하나 더 만들어주는데 그게 바로 함수의 prototype 객체
입니다. 예를들어 Person 함수가 있다면 이 함수가 만들어질 때 Person과 자동으로 연결된 prototype 객체
가 같이 만들어집니다.
Person 함수는 자신의 prototype
속성을 통해서 prototype 객체
를 가르키고, prototype 객체
는 그 객체 안에 constructor
라는 속성을 통해서 Person 함수를 가리키는 '순환참조 관계'를 가지고 있습니다.
new연산자와 함수가 만나면 자바스크립트 한 부분에서 보이지 않는 일이 일어난다고 앞에서 설명했었습니다. 이때 하나 더 일어나는 일이 있습니다. 그게 바로 생성된 객체에 자바스크립트가 생성자 함수의 prototype 객체
를 가르키는 __proto__
링크를 넣는 것입니다.
위의 설명대로 관계도를 그려보면 이렇게 그릴 수 있습니다. Person 함수에 의해 Kim 객체가 만들어졌다면 Kim의 __proto__
링크는 Person 함수의 prototype 객체
를 가르키게 됩니다.
function sayHello() {
console.log("${this.name}: hello!");
}
function Person(name) {
this.name = name;
}
const chris = new Person('chris');
chris.sayHello(); // Uncauth TypeError 🚨
위의 코드에서 chris 객체는 Person 함수에 의해서 만들어졌지만 chris 객체 안에는 sayHello라는 method가 없기 떄문에 당연히 에러가 뜨게 됩니다.
하지만 Person.prototype.sayHello = sayHello;
라는 한줄이 추가되면 어떻게 될까요?
이상하게도 chris 객체에서도 이제는 sayHello를 호출할 수 있게 됩니다. 왜냐하면 이 안에서는 'Prototype Chaining'이 일어나기 때문입니다.
Prototype Chaining을 간단히 말하자면 __proto__
링크를 따라가서 계속해서 탐색을 해나가는 과정 자체를 의미합니다. 예를 한번 들어보겠습니다.
a 개체의 __proto__
링크를 직접적으로 b로 연결합니다.
a객체에는 없는 속성이 있어도 자바스크립트는 __proto__
링크를 통해 b객체를 이동할 수 있고 거기서 속성을 찾아볼 수 있습니다.
a속성에 없는 속성도 b속성에 있다면 a객체에서 자신에게 없는 속성도 사용할 수 있습니다.
이런식으로 __proto__
링크를 따라 거슬러 올라가서 탐색을 수행하는게 바로 Prototype Chaining 입니다.
아까 위에서 지나갔던 chirs.sayHello
코드를 분석해보겠습니다. 그냥 단순하게 보면 chris라는 객체에 직속된 sayHello method를 접근하려는 코드처럼 보이지만 내부에서는 좀 더많은 일이 일어납니다.
chris라는 객체에 sayHello 속성이 있는지 먼저 찾아봅니다. (=> ❌ 찾을 수 없습니다)
chris 객체안에 __proto__
속성을 통해서 chris 객체를 생성했던 Person 함수의 prototype 객체
로 이동해서 다시 한번 sayHello를 찾아갑니다. (=> ⭕️ 찾을 수 있습니다)
[‼️예외‼️] 만약에 sayHello를 계속해서 찾을 수 없다면 오브젝트라는 이름에 생성자 함수의 prototype 객체
에 도달했을떄 겨우 멈추게 됩니다.
이때는 __proto__
링크 안에 null이 있기 때문에 이 이상으로 Prototype Chaining 을 지속할 수 없습니다.
예시 기능으로 querySelector로 요소를 불러오는 함수에서 요소 객체가 요소를 가릴 수 있는 method를 포함하도록 만들고 싶습니다.
const $1 = (selector, target = document) => {
const all = target.querySelectorAll(selector);
const result = all.length > 1 ? [...all] : all[0];
result.hide = function () {
result.classList.add("invisible");
};
return result;
};
const button1 = $1("#button-1");
button1.hide();
만약 이렇게 코드를 작성하게 된다면 $1 함수가 실행 될 때마다 새로운 Hide라는 함수가 계속해서 만들어지게 됩니다. 하지만 이렇게 되면 메모리 측면에서 효율적이라고 보기는 어렵습니다.
function hide() {
this.element.classList.add("invisible");
}
function $2(selector, target = document) {
const all = target.querySelectorAll(selector);
this.element = all.length > 1 ? [...all] : all[0];
}
$2.prototype.hide = hide; // ⭐️
const button1 = new $2("#button-2");
button1.hide();
다음의 코드에서 $2를 실행시키게 되면 객체는 hide함수 하나만을 사용하게 됩니다. 이렇게 한다면 하나의 method를 여러 곳에서 재활용 할 수 있기 때문에 메모리 측면에서 효율적이라고 볼 수 있습니다.
[10분 테코톡] 💼 크리스의 Prototype - https://youtu.be/RYxgNZW3wl0
JavaScript 객체 지향 프로그래밍 - 15. prototype vs proto - https://www.youtube.com/watch?v=wT1Bl5uV27Y
이거보고 prototype 이해 못하면 강의접음 - https://www.youtube.com/watch?v=wUgmzvExL_E