TIL-55 JavaScript 프로토타입

PRB·2021년 11월 18일
0

JavaScript

목록 보기
19/24
post-thumbnail

1. 프로토타입

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

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

prototype이라는 프로퍼티와 __proto__라는 프로퍼티의 관계가 프로토타입 개념의 핵심이다. prototype는 객체이다. 이를 참조하는 __proto__ 역시 당연히 객체이다. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장한다. 그러면 인스턴스에서도 숨겨진 프로퍼티인 __proto__를 통해 이 메서드들에 접근할 수 있게 된다.

Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정했다고 가정해보자

var Person = function (name) {
  	this._name = name;
};
Person.protype.getName = function() {
  	return this._name;
};

이제 Person의 인스턴스는 __proto__프로퍼티를 통해 getName을 호출할 수 있다.

var suzi = new Person('Suzi');
suzi.__proto__.getName();  // undefined

왜냐하면 instance의 __proto__가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라보기 때문이다.

Person.prototype === suzi.__proto__  // true

메서드 호출 결과로 undefined가 나온 점에 주목해 보자 'Suzi'라는 값이 나오지 않는 것보다는 '에러가 발생하지 않았다'라는 점이 우선이다. 값이 에러가 아닌 다른 값이 나왔으니까 getName이 실제로 실행됐음을 알 수 있고 이로부터 getName이 함수라는 것이 입증됐다.

어떤 함수를 '메서드로서'호출할 때는 메서 두 명 바로 앞의 객체가 곧 this가 된다. 그러니까 thomas.__proto__.getName()에서 getName 함수 내부에서의 this는 thomas가 아니라 thomas.__proto__라는 객체가 되는 것이다. 이 객체 내부에서는 name 프로퍼티가 없으므로 '찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 반환한다'라는 자바스크립트 규약에 의행 undefined가 반환된 것이다.

그럼 만약 __proto__ 객체에 name 프로퍼티가 있다면 어떨까?

var suzi = new Person('Suzi')
suzi.__proto__.name = 'SUZI__proto__';
suzi.__proto__.getName()  // SUZI__proto__

예상대로 잘 출력 된다.

var suzi = new Person('Suzi', 28);
suzi.getName();  // Suzi
var iu = new Person('Jieun', 28);
iu.getName();  // Jieun

또한 __proto__는 생략 가능하다.

var arr = [1,2];
console.dir(arr);
console.dir(Array);

첫 번째 사진은 arr 변수를 출력한 결과이고, 두 번째 사진은 생성자 함수인 Array를 출력한 결과이다. 첫 번째 사진부터 보면 Array(2)라고 표시되어 있다.
Array라는 생성자 함수를 원형 삼아 생성됐고, length가 2임을 알 수 있다.
또한 [[prototype]]를 열어보면 우리가 배열에 사용하는 메서드들이 거의 모두 들어있는 것을 확인할 수 있다.

Array의 prototype 프로퍼티 내부에 있지 않은 from, isArray 등의 메서드들은 인스턴스가 직접 호출할 수 없을 것이다. 이들은 Array 생성자 함수에서 직접 접근해야 실행이 가능하다.

constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 인스턴스의 proto 객체 내부에도 마찬가지이다. 이 프로퍼티는 단어 그대로 원래의 생성자 함수(자기 자신)를 참조한다. 자신을 참조하는 프로퍼티를 굳이 뭐 하러 가지고 있을까 싶지만, 이 역시 인스턴스와의 관계에 있어서 필요한 정보이다. 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단이기 때문이다.

var arr = [1,2];
Array.prototype.constructor === Array  // true
arr.__proto__.constructor === Array  // true
arr.constructor === Array  // true

var arr2 = new arr.constructor(3, 4);
console.log(arr2);  // [3, 4]

constructor는 읽기 전용 속성이 부여된 예외적인 경우를 제외하고는 값을 바꿀 수 있다.

2. 프로토타입 체인

1. 메서드 오버라이드

var Person = function (name) {
  	this.name = name;
};
person.prototype.getName = function () {
  	return this.name;
};

var iu = new Person('지금');
iu.getName = function () {
  	return '바로 ' + this.name;
};
console.log(iu.getName());  // 바로 지금

iu.__proto__.getName이 아닌 iu 객체에 있는 getName 메서드가 호출되었다. 여기서 일어난 현상을 메서드 오버 라이드라고 한다. 메서드 위에 메서드를 덮어씌웠다는 표현이다. 원본을 제거하고 다른 대상으로 교체하는 것이 아니라 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는 이미지를 떠올리면 정확하다.

자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행된다.
그렇다면 메서드 오버라이딩이 이뤄져 있는 상황에서 prototype에 있는 메서드에 접근하려면 어떻게 해야 할까?

console.log(iu.__proto__.getName.call(iu)); // 지금

일반적으로 메서드가 오버라이드 된 경우에는 자신으로부터 가장 가까운 메서드에만 접근할 수 있지만, 그다음으로 가까운 __proto__의 메서드도 우회적인 방법을 통해서 이긴 하지만 접근이 불가능한 건 아니다.

2. 프로토타입 체인

console.dir({a : 1});

배열 리터럴의 __proto__안에 또다시 __proto__가 등장한다.
객체의 __proto__와 동일한 내용으로 이뤄져 있다.
왜 그럴까?
바로 prototype 객체가 '객체'이기 때문이다.
기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다. prototype 객체도 예외는 아니다.

profile
사용자 입장에서 사용자가 원하는 것을 개발하는 프론트엔드 개발자입니다.

0개의 댓글