본문 바로가기
👩‍💻 Programming/JavaScript

[코어 자바스크립트] 6장 프로토타입

by codingBear 2022. 12. 6.
728x90
반응형

들어가며

👉 자바스크립트는 프로토타입(prototype) 기반 언어이다. 클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(프로토타입)으로 삼아 이를 복제(참조)함으로써 상속과 비슷한 효과를 낸다.


01 프로토타입의 개념 이해

6-1-1 constructor, prototype, instance

❕ 위 그림을 이해하면 프로토타입은 끝이다!

👉 위 코드와 그림의 흐름을 따라가자면 아래와 같다.

  • 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
  • Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(Instance)가 생성됨.
  • 이때 Instance에는 __proto__(dunder proto, 던더 프로토)라는 프로퍼티가 자동으로 부여되는데,
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.

👉 prototype과  __proto__라는 프로퍼티의 관계가 프로토타입의 핵심 개념임.

👉 prototype은 객체인데 내부에 인스턴스가 사용할 메서드를 저장한다. 인스턴스의 숨겨진 프로퍼티인 __proto__를 통해 이 메서드들에 접근 가능하다.

 

👉 Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정하면 Person의 instance suzi는 __proto__ 프로퍼티를 통해 getName을 호출 가능하다. 

👉 instance의 __proto가 Constructor의 prototype 프로퍼티를 참조하므로 둘은 같은 객체를 바라본다.

👉 suzi 인스턴스의 메서드 호출 결과가 undefined이라는 점에 주목하자. 에러가 발생하지 않고 undefined이 반환되었다는 것은 해당 변수가 '호출할 수 있는 함수'라는 뜻이다. 문제는 this에 바인딩된 대상이 잘못된 것이다.

👉 suzi.__proto__.getName()에서 this는 'suzi.__proto__'라는 객체임. 이 객체 내부에는 name이라는 프로퍼티가 없으므로 undefined이 반환됨.

 

💛 자바스크립트에서 __proto__는 생략 가능한 프로퍼티이다. __proto__를 생략하면 this는 instance인 suzi를 가리킨다. suzi.__proto__에 있는 메서드인 getName을 실행하면서도 this는 suzi를 바라보게 되는 것이다.

👉 위의 그림을 한 문장으로 말하자면 다음과 같다.

"new 연산자로 Constructor를 호출하면 instance가 만들어지는데, 이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructor의 prototype을 참조한다"

 

💛 프로토타입에 대해 좀더 자세히 알아보자!

👉 자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데, 해당 함수를 new 연산자와 함께 생성자 함수로서 호출할 경우, 그로부터 생성된 instance에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다.

👉 __proto__ 프로퍼티는 생략 가능하므로 인스턴스에서도 생성자 함수의 prototype에 있는 메서드나 프로퍼티에 자신의 것처럼 접근 가능하다.

👉 예제의 9번째 줄에서 instance를 출력했는데 Constructor가 출력된다. 즉 어떤 instance는 instance가 비롯된 Constructor의 이름을 표기함으로써 해당 Constructor의 instance임을 표시한다.

👉 instance의 내용을 살펴보면 Constructor의 prototype과 동일한 내용으로 구성돼 있음을 알 수 있다.

 

💛 배열 리터럴과 Array의 관계

👉 Array를 new 연산자와 함께 호출하든 배열 리터럴을 생성하든 [1, 2]라는 instance는 생성됨. 이 instance의 __proto__는 Array.prototype을 참조하며 생략 가능해서 instance가 push, pop, forEach 등의 메서드를 호출 가능하다.

👉 Array의 prototype 프로퍼티 내부에 있지 않은 from, isArray 같은 메서드들은 instance가 호출 할 수 없고 Constructor에서 직접 접근해야 한다.

 

6-1-2 constructor 프로퍼티

👉 생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있는데 단어 그대로 생성자 함수 자기 자신을 참조한다. 이를 통해 instance의 원형이 무엇인지 알 수 있다.

👉 instance의 __proto__가 생성자 함수의 prototype 프로퍼티를 참조하며 __proto__가 생략 가능하기 때문에 instance에서 직접 constructor 프로퍼티에 접근 가능한 것임.

 

👉 constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수: number, string, boolean)를 제외하고 값 변경 가능.

👉 모든 데이터가 d instanceof NewConstructor 명령에 대해 false를 반환함. 이로써 constructor 프로퍼티를 변경하더라도 참조하는 대상이 변경될 뿐, 이미 만들어진 instance의 원형이나 데이터 타입이 바뀌는 것은 아님을 알 수 있다.

👉 따라서 constructor 프로퍼티는 언제든 변경 가능하므로 어떤 instance의 생성자 정보를 알아내기 위해 constructor 프로퍼티에만 의존하는 것은 항상 안전하지는 않다. 하지만 그렇기 때문에 클래스의 상속을 흉내낼 수도 있는 것이다.

 

💛 중간 정리

👉 p1 ~ p5 모두 Person의 instance

 

👉 위의 각 줄은 모두 동일한 대상을 가리킴.

 

👉 위의 각 줄은 모두 prototype에 접근 가능함.


02 프로토타입 체인

6-2-1 메서드 오버라이드

👉 원본 메서드 위에 다른 메서드를 덧씌우는 현상. 위 예제의 경우 iu 객체에 있는 getName의 메서드가 iu.__proto__.getName에 덮어씌워진 후 호출되었다.

👉 자바스크립트 엔진은 메서드를 찾을 때 가장 가까운 대상인 자신의 프로퍼티를 먼저 검색하고, 그 다음 가까운 대상인 __proto__를 검색한다. 즉 __proto__에 있는 메서드는 자신에게 있는 메서드보다 우선 순위에서 밀려 호출되지 않은 것이다.

 

🤔 메서드 오버라이딩이 된 상태에서 prototype에 있는 메서드에 어떻게 접근할까?

👉 iu.__proto__.getName을 출력했더니 undefined가 나온다. this가 prototype 객체(iu.__proto__)를 가리키는데 prototype에는 name이라는 프로퍼티가 없기 때문이다.

 

👉 prototype에 name이라는 프로퍼티를 부여했더니 값이 잘 출력되는 것을 확인할 수 있다.

👉 다만 여기서 this가 instance가 아닌 prototype을 바라보는데 아래와 같이 call 혹은 apply로 이 문제를 해결할 수 있다.

 

 

🟡 즉 메서드가 오버라이드된 경우 자신으로부터 가장 가까운 메서드에만 접근할 수 있지만 __proto__의 메서드에도 우회적인 방법으로나마 접근 가능하다.

 

6-2-2 프로토타입 체인(prototype chain)

🤔 프로토타입 체인?

👉 어떤 데이터의 __proto__ 프로퍼티 내부에 __proto__ 프로퍼티가 연쇄적으로 이어진 것.

 

👉 배열 리터럴의 내부 구조를 살펴보자. __proto__  안에는 또다시 __proto__가 있고 이 안에 있는 __proto__를 열어보면 그림 6-7에서 살펴본 객체의 __proto__와 내용이 동일하다. 

👉 그림 6-9를 보면 알겠지만 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다. 이는 prototype객체가 '객체(object)'이기 때문이다.

 

👉 앞서 살펴보았듯 __proto__는 생략 가능하다. 그래서 배열이 Array.prototype 내부의 메서드를 자신의 것처럼 사용할 수 있는 것이다. 마찬가지로 Object.prototype 내부의 메서드도 자신의 것처럼 실행 가능하다.

 

💛 메서드 오버라이드와 프로토타입 체이닝

👉 프로토타입 체인을 따라가며 검색하는 것을 프로토타입 체이닝(prototype chaining)이라고 함.

👉 프로토타입 체이닝의 동작 방식은 메서드 오버라이드와 동일하다. 자바스크립트 엔진은 메서드를 호출하면 우선 데이터 자신의 프로퍼티를 검색해서 호출할 메서드가 있다면 호출하고 없다면 __proto__를 검색해보고 그래도 없다면 다른 __proto__를 검색해서 메서드를 호출한다.

 

👉 arr 변수는 배열이므로 arr.__proto__는 Array.prototype을 참조하고, Array.prototype은 객체이므로 Array.prototype.__proto__는 Object.prototype을 참조한다.

👉 따라서 toString이라는 메서드는 Array.prototype 및 Object.prototype에도 있다.

👉 arr.toString()을 실행하면 Array.prototype의 toString 메서드를 실행했을 때와 동일한 결괏값을 반환한다. 왜냐하면 arr.__proto__는 Array.prototype와 동일하기 때문이다.

👉 6번째 줄에서는 arr에 직접 toString이라는 새로운 메서드를 부여했는데 이를 통해 메서드 오버라이드가 일어나고 9번째 줄에서는 덮어씌운 toString 메서드의 결괏값이 출력된다.

 

🟡 자바스크립트 데이터의 프로토타입 체인 구조

👉 배열뿐만 아니라 자바스크립트 데이터는 모두 위의 그림처럼 동일한 형태의 프로토타입 체인 구조를 가짐.

👉 모든 자료형의 최상단의 prototype은 Object.prototype이다.

 

👉 생성자 함수는 모두 함수이므로 Function 생성자 함수의 prototype과 연결된다. Function 생성자 함수 역시 함수이므로 다시 Function 생성자 함수의 prototype과 연결된다. 이렇게 __proto__의 constructor의 __proto__의 constructor...는 재귀적으로 반복되며 루트를 따라가다보면 끝없이 찾아갈 수 있다.

👉 일반적으로 instance와 직접 연결되는 삼각형에만 주목하면 된다.

 

6-2-3 객체 전용 메서드의 예외사항

👉 앞서 살펴보았듯 어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 최상단에는 Object.prototype이 존재함.

👉 Object.prototype에만 정의된 메서드라도 다른 데이터 타입에서 접근 가능함. 따라서 객체에서만 사용할 메서드는 다른 데이터 타입처럼 prototype 객체 안에 정의할 수 없음.

👉 원래 의도대로라면 객체가 아닌 다른 데이터 타입에 대해서는 오류를 던져야 하나 모든 데이터가 오류 없이 출력됨. 어

느 데이터 타입이건 프로토타입 체이닝을 통해 getEntries 메서드에 접근 가능 하기 때문임.

👉 따라서 객체 전용 메서드는 Object에 스태틱 메서드(static method)로 부여돼 있음.

👉 또한 생성자 함수인 Object와 instance인 객체 리터럴 사이에는 this를 통한 연결이 불가능하므로 대상 instance를 인자를 주입하여 직접 this에 할당하는 식으로 구현돼 있음.

 

🟡 toString, hasOwnProperty, valueOf, isPrototypeOf 등은 다른 데이터 타입에서도 활용 가능함.

 

🟡 Object.create를 이용하면 Object.prototype의 메서드에 접근할 수 없다.

 

6-2-4 다중 프로토타입 체인

👉 자바스크립트의 기본 내장 데이터 타입들은 프로토타입 체인이 1단계(객체)이거나 2단계(나머지)로 끝나기도 하지만 대각선의 __proto__를 연결해나가면 무한대로 체인을 이어나갈 수 있음.

👉 대각선의 __proto__를 연결하려면 __proto__가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 instance를 바라보게 하면 됨.

 

👉 변수 g는 Grade의 인스턴스를 바라보는데 Grade의 instance는 배열의 메서드를 사용할 수 없는 유사배열객체이다. 이 instance에서 배열 메서드를 직접 쓸 수 있게 하려면 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

👉 Grade.prototype이 배열의 인스턴스를 바라보게 되면, 하나의 프로토타입 체인을 형성하고 Grade의 instance인 g에서 직접 배열의 메서드를 사용할 수 있다.

👉 g instance에서는 g 객체 자신의 멤버, Grade.prototype의 멤버, Array.prototype의 멤버, Object.prototype에 있는 멤버까지 접근 가능하다.


03 정리

  • 어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에 정의된 내용을 바탕으로 새로운 instance가 생성됨. 이 instance에는 __proto__라는 Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여됨. __proto__는 생략 가능한 속성이라서 instace는 Constructor.prototype의 메서드를 자신의 메서드인 것처럼 호출 가능함.
  • Constructor.prototype에는 contructor라는 프로퍼티가 있는데 생성자 함수 자신을 가리킴. instance가 자신의 생성자 함수가 무엇인지 알고자 할 때 쓰임.
  • 프로토타입 체인의 __proto__ 방향을 계속 찾아가면 최종적으로는 Object.prototype에 당도함. 이런 식으로 __proto__를 탐색해나가는 과정을 프로토타입 체이닝이라 하며 이를 통해 연결된 각 prototype의 메서드를 자신의 것처럼 사용 호출 가능함.
  • Object.prototype에는 모든 데이터 타입에서 사용 가능한 범용적인 메서드만이 존재하며, 객체 전용 메서드는 Object 생성자 함수에만 스태틱하게 담겨 있음
728x90
반응형

댓글