[코어 자바스크립트] 06_프로토타입

Mooongs·2022년 6월 16일
0

코어자바스크립트

목록 보기
5/8
post-thumbnail

자바스크립트하고 리액트에 뚜드려 맞다가 오랜만에 다시 생각난 코어 자바스크립트.. 멋사 수료하기 전에 2회독하겠다는 다짐을 잊을 뻔했다.. 초심잡자! 👊 5장 클로저 부분은 6장 프로토타입과 7장 클래스 부분 정리가 끝나면 다시 돌아올 예정이다.


프로토타입

프로토타입의 개념

자바스크립트는 프로토타입 기반 언어이기 때문에 Java, C++, Python과 같은 클래스 기반의 객체 지향 언어와 다른 특징을 가지고 있다. 후자는 '상속'이라는 개념을 사용하지만 자바스크립트는 어떤 객체를 원형(prototype)으로 삼고 이것을 복제(참조)함으로써 상속과 비슷한 효과를 얻게 된다.

constructor, prototype, instance

위와 같은 그림이 6장에서 반복적으로 등장하고 그만큼 많이 강조되는데, 프로토타입의 기본개념을 다지는 데 이 도식화가 도움이 되었던 것 같다. 위의 그림을 설명하면 다음과 같다.

  • 어떤 생성자 함수 Constructor를 new 연산자와 함께 호출하면
  • Constructor에서 정의된 내용을 바탕으로 새로운 instance가 생성된다.
  • 이때 instance에는 __proto__라는 프로퍼티가 자동으로 부여되는데
  • 이 프로퍼티는 Constructor의 prototype 프로퍼티를 참조한다.

📌 __proto__를 읽을 때는 '던더 프로토(dunder proto)'라고 읽으면 된다.
= double underscore의 줄임말

⭐ instance의 __proto__ 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조하고 있으므로 prototype 객체에 접근해 내부의 메서드들을 사용할 수 있게 되는 것이다.

Person이라는 생성자 함수로 nayoung이라는 instance를 생성했다. 그리고 맨 마지막 줄의 코드를 통해 생성자 함수의 프로토타입과 인스턴스의 __proto__가 같은 객체를 바라보고 있음을 알 수 있다.
그렇다면 인스턴스로부터 Person의 프로토타입에 지정한 callName 메서드에 접근해보자.

띠용? 이름이 아닌 undefined 가 출력되었다. 이유가 뭘까?? 어떤 함수를 메서드로서 호출하면 this는 나를 호출한 바로 앞의 객체가 된다. __proto__에는 name 프로퍼티가 없기 때문에 undefined가 뜬 것이다. 이를 해결하기 위해서는 (1) 인스턴스의 __proto__ 객체에 name 프로퍼티를 부여하거나 (2) __proto__ 없이 인스턴스에서 바로 메서드를 사용하는 방법이 있다.

왜냐면 __proto__는 ⭐생략 가능한 프로퍼티이기 때문이다. 원래부터 그렇게 정의되어 있기 때문에 그냥 외워야 할 뿐! 이를 도식화하면 다음과 같다.

constructor 프로퍼티

생성자 함수의 prototype 객체 내부에는 본인의 생성자 함수를 참조하는 constructor라는 프로퍼티가 있다. 그렇기 때문에 이것을 참조하는 인스턴스의 __proto__에도 마찬가지로 존재한다. 왜 있는 걸까 싶지만, ⭐인스턴스로부터 그 원형이 무엇인지 알아내기 위한 수단으로 사용된다.

아래의 예시에서 왼쪽 사진은 arr라는 변수의 디렉터리 구조를, 오른쪽 사진은 생성자 함수중 하나인 Array의 디렉터리 구조를 보여주고 있다. 배열 리터럴로 생성되었어도 new 연산자와 함께 생성자 함수를 호출한 것처럼 같은 결과의 인스턴스가 만들어진다. 그리고 각 사진에서 볼 수 있듯 프로토타입 프로퍼티 안에 constructor 프로퍼티가 공통적으로 있는 것을 확인할 수 있다.

위에서 __proto__가 생략 가능한 프로퍼티라는 점을 다시 생각해보면 다음과 같이 정리할 수 있다. 각 줄은 모두 동일한 대상을 가리키고 있다.

[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

📌 참고 (MDN 문서)
모든 객체는 __proto__로 자신의 프로토타입([[Prototype]] 내부 슬롯)에 접근할 수 있고 ES6에서 표준으로 채택되었지만, 아직까지 Object.getPrototypeOf() 사용이 더욱 권장되고 있다.



프로토타입 체인

메서드 오버라이드

위에서 봤듯이 __proto__를 생략하면 인스턴스는 생성자 함수의 프로토타입에 정의된 프로퍼티나 메서드를 자신의 것처럼 활용할 수 있다고 했다. 그렇다면 인스턴스가 동일한 이름의 프로퍼티나 메서드를 가지고 있다면 어떻게 될까? 책의 예제롤 보기 전에 스코프 체이닝처럼 본인의 것이 있다면 이것을 참조하고 범위를 넓혀나가며 탐색해나가지 않을까 했는데, 정답이 맞았다! ⭐먼저 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그다음으로 가까운 대상인 __proto__를 탐색하는 것이다. 이렇게 본인의 프로퍼티나 메서드가 기존의 것을 덮어 씌운 것을 메서드 오버라이드라고 정의한다. (교체가 아니라 덮어 씌운 것에 주의!)


위의 예시를 보면 bobo.__proto__에 있는 getName이 아닌 bobo 자신 객체에 있는 getName 메서드를 호출했음을 볼 수 있다. 그렇다면 이렇게 메서드 오버라이딩이 일어나고 있는 상황에서 prototype에 접근하려면 어떻게 해야 할까?

  1. 생성자 함수의 프로토타입에 name 프로퍼티 지정
Person.prototype.name = '보보'; // name 지정해주지 않으면 undefined 출력
console.log(bobo.__proto__.getName()); // 보보
  1. getName이 바라보고 있는 this를 프로토타입이 아닌 인스턴스로 고정해주기
    (call이나 apply 사용)
console.log(bobo.__proto__.getName.call(bobo)); 
// 원래 의도한 대로 영문이름 'bobo' 출력

프로토타입 체인

console.dir([1,2,3])

위의 간단한 코드를 콘솔에 찍어보면 다음과 같은 모습이다.

프로토타입 안에 또 다른 프로토타입이 있다. 왜일까?
프로토타입이 기본적으로 객체이기 때문이다!

그래서 배열 리터럴로 생성한 데이터의 __proto__ 안을 보면 constructor는 Array이지만 그 안의 프로토타입의 constructor는 Object인 것을 확인할 수 있다. ⭐기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결되는 것이다!

이 특징에 대한 용어를 정리 해보면 다음과 같다.

프로토타입 체인: __proto__ 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것
프토토타입 체이닝: 이 체인을 따라가며 검색하는 것

프로토타입 체이닝도 메서드 오버라이드와 마찬가지로, 가장 가까운 프로퍼티로부터 범위를 넓혀나가며 탐색하는 방식을 취한다. 다음은 Array와 Object 프로토타입에 모두 존재하는 toString 메서드를 가지고 변수 arr에 적용했을 때의 결과를 확인해보기 위한 예제이다.

const arr = [1, 2];
Array.prototype.toString.call(arr); // 1,2
Object.prototype.toString.call(arr); // [object Array]
arr.toString(); // 1,2

arr.toString = function () {
	return this.join('_'); 
}
arr.toString(); // 1_2

arr에 toString 메서드를 직접 부여하기 전까지는 Object.prototype이 아닌 Array.prototype의 toString 메서드의 결과값과 같게 나오고(더 가까운 관계이므로), 직접 부여한 이후에는 join 메서드를 활용한 결과값을 출력하게 된다.

객체 전용 메서드의 예외사항

어떤 생성자 함수든 모든 프로토타입은 반드시 객체이기 때문에 Object.prototype이 항상 프로토타입의 최상단에 존재하게 된다. 따라서 객체에서'만' 사용될 메서드는 프로토타입 객체 안에 정의할 수가 없다. 안에 정의하게 되면 다른 데이터 타입도 프로토타입 체이닝을 통해 해당 메서드에 접근할 수 있기 때문이다.

이러한 이유 때문에 객체 전용 메서드들은 Object.prototype이 아닌 Object에 static method로 부여할 수밖에 없었다.

Object.freeze(instance) // OK
instance.freeze() // NOoo

Object.getPrototypeOf(instance) // OK
instance.getPrototypeOf() // NOoo

위의 내용을 바꿔 말하면, Object.prototype에 있는 메서드들은 어떤 데이터에서도 접근 및 활용이 가능하다는 것을 의미한다.

다중 프로토타입 체인

지금까지 프로토타입 체인이 2단계까지 존재할 수 있다는 것은 확인할 수 있었다.

  • 1단계: 객체
  • 2단계: 나머지 데이터 타입 (해당 데이터 타입의 프로토타입 → 객체의 프로토타입)

하지만 사용자가 새롭게 만드는 경우에는 그 이상으로 프로토타입 체인을 이어나갈 수 있다. 대각선의 __proto__를 연결하는 방법은 __proto__가 가리키는 대상, 즉 생성자 함수의 프로토타입이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다.

const Grade = function(){
    const args = Array.prototype.slice.call(arguments);
    for (let i = 0; i < args.length; i++){
        this[i] = args[i];
    }
    this.length = args.length;
}

const g = new Grade(100, 80);

인스턴스 g를 찍어보면 배열의 메서드를 사용할 수 없는 유사배열객체임을 알 수 있다.

하지만 인스턴스에서 바로 배열 메서드를 활용하고 싶다면?
Grade.prototype이 배열의 인스턴스를 바라보게 해야 한다.

Grade.prototype = [];

// 이제 g에서 배열 메서드 직접 사용 가능
console.log(g);  // Grade(2) [100, 80]
g.pop();
console.log(g);  // Grade(1) [100]


이 부분이 가장 어려웠는데, 도식화된 그림을 보고나니 조금은 알 것 같다. 다중 프로토타입 체인을 사용하는 이유, 구현 방식 등은 7장에서 더 공부한 뒤 이 부분으로 다시 돌아와서 봐야겠다는 생각이 들었다.


이번 장은 콘솔에서 쉽게 확인해보면서 공부할 수 있어서 더 직관적이고 재미있었다. 프로토타입 개념 자체만 봤을 때는 자바스크립트의 기본 원리라는 것 외에는 큰 중요성을 느끼지 못했는데, 이 개념을 공부하면서 평소에 아무 생각 없이 사용하던 메서드들의 동작 원리, 정적 메서드의 존재 이유 등을 알게되니 너무 흥미로웠고 시야가 조금이나마 확장된 기분이다 ✨

profile
#FE개발자🐣 #새로운건 #짜릿해

0개의 댓글