Prototype에 대해 이해하기

seungchan.dev·2022년 10월 2일
2

Javascript

목록 보기
3/3

정의

자바스크립트에서는 다른 상속 기반의 언어와 달리, prototype(원형) 기반의 언어로, 모든 객체는 특정 객체를 원형(prototype)으로 삼고 이를 복제(참조)하는 방식을 통해 상속과 비슷한 효과를 가지게 한다.

개념 추상화

프로토타입을 추상화 하면 위와 같은 그림이 나오는데 이 그림은 다음의 의미를 가진다.

  • 생성자(Constructor)은 원형(prototype)을 속성으로 가진다.
  • 생성자와 new 키워드를 결합하면 인스턴스(instance) 가 생성된다.
  • 이때 인스턴스는 자동으로 __proto__ 속성을 가진다.
  • __proto__ 속성은 생성자의 prototype 을 참조한다.

인스턴스와 proto 의 관계와 이해

let Person = function (name) {
	this._name = name;
}
    
Person.prototype.getName = function () {
	console.log('call getName method');
    return this._name;
}

위와 같이 생성자 함수 Person 을 정의하고 생성자 함수의 prototypegetName 이라는 메소드를 정의하였다.

const j113 = new Person("seungchan");
    
j113.__proto__.getName(); 
// call getName method
// undefined

j113 이라는 인스턴스를 생성해주었고 이 인스턴스의 prototype 에 정의된 getName 이라는 메소드를 호출하였다. 이때 결과로 undefined 가 나오고 에러가 발생하지는 않는다.

Person.prototype === j113.__proto__ // true

참고로, 인스턴스에서 호출하는 __proto__Personprototype 과 동일하기에 getName 메소드를 호출 할 수 있다.

이를 통해 알 수 있는 것은 prototype 에 정의된 getName 메소드에는 잘 도달하지만 getName 에서 반환하는 this._name 이 문제가 될 것이다. 이는 getName 메소드에서 this 가 무엇인지를 확인하면 원인파악이 되는데 getName 메소드를 호출하는 주체가 j113.__proto__ 이고 이는 곧 Personprototype 인데 여기에는 _name 이라는 것이 존재하지 않는다. 그래서 위의 예시에서 undefined 가 나오게 된 것이다.

// Person 생성자함수 정의는 생략
Person.prototype._name = "seungchan"
    
const j113 = new Person("seungchan");
    
j113.__proto__.getName();
// seungchan

실제로 위에 처럼 Personprototype 자체에 _name 프로퍼티를 정의해 놓으면 getName 이 원하는대로 출력되는 것을 확인할 수 있다.

const j113 = new Person("seungchan");
    
j113.getName();// seungchan

또한, 인스턴스에서 __proto__ 라는 키워드를 생략한뒤 메소드를 호출하면 this 가 인스턴스가 됨으로써 원하는 결과가 나오게 된다. 여기서 인스턴스에서 어떻게 prototype 으로 접근이 가능한지 궁금할 수 있는데 이는 자바스크립트 문법을 설계하면서 정해진 규칙이라고 한다 (이런거 보면 참 근본 없다.)

이러한 특징들을 정리하자면 다음과 같다.

  • 생성자 함수의 prototype에 정의된 메소드나 프로퍼티는 생성된 인스턴스에서 자신의 것처럼 접근이 가능하다.
  • 인스턴스에서 __proto__ 키워드는 생략이 가능하다.

prototype을 기반으로 Array에 대해 이해하기

자바스크립트 내 배열 자료구조를 통해서 생성자함수 Array 와 이것의 인스턴스를 이해해보고자 한다.

const arr = [1,2];

console.dir(arr);

console.dir(Array);

위의 출력 결과는 다음과 같다.

생성자함수 Array의 인스턴스인 arr__proto__ 는 생성자함수 Arrayprototype 과 동일한 것을 참조하는 걸 알 수 있다. 이를 추상화 해보면 다음과 같다.


메소드 오버라이드

인스턴스는 __proto__ 라는 키워드를 생략하더라도 prototype 에 정의된 메소드 및 프로퍼티에 자유롭게 접근이 가능하다. 한편 프로토타입에 정의된 메소드와 동일한 이름의 메소드를 인스턴스에 정의하게 될 경우에는 인스턴스의 함수로 덮어씌워진다.

let Person = function(name){
	this.name = name;
}

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

const seungchan = new Person('승찬');

seungchan.getName = function () {
	return 'i am ' + this.name;
}

console.log(seungchan.getName()); // i am 승찬

한편, 인스턴스의 메소드가 아닌 prototype 에서 정의한 메소드에 직접 접근하기 위해서는 아래와 같이 __proto__ 키워드를 사용해야 한다.

console.log(seungchan.__proto__.getName()); // undefined

하지만 위와 같이 this 가 가리키는 대상이 인스턴스의 __proto__ 가 참조하는 prototype 이 되기 때문에 적절한 name 을 가져오지 못한다. 아래와 같이 이는 call 를 사용하여 this 를 인스턴스로 지정해주면 해결 될 수 있다.

console.log(seungchan.__proto__.getName()).call(seungchan); // "seungchan"

프로토타입 체이닝

배열 인스턴스의 프로토타입인 Array 생성자함수의 프로토타입을 자세히 살펴보면 아래와 같다.

console.dir([1,2]); 

[1,2]__proto__Array 를 가리키고 다시 Array__proto__Object 를 가리키는 것을 확인할 수 있다. 이를 추상화하면 아래와 같다.

이러한 관계 덕분에 배열의 인스턴스인 [1,2] 는 Array의 메소드 뿐만 아니라 Object에서 정의된 메소드가 까지 사용이 가능해진다.

var arr = [1,2];

arr(.__proto__).push(3); // Array에 정의된 push 메소드

arr(.__proto__)(.__proto__).hasOwnProperty(2); // Object에 정의된 메소드

한편 모든 타입은 prototype 을 가지고 prototypeObject 형태를 가지게 되기 때문에 prototype 체인의 최상단에는 Object 가 존재하게 된다.

이렇다보니 모든 데이터타입에서 Object 프로토타입에 정의된 메소드들을 접근할 수 있게 된다. 가령 hasOwnProperty 가 대표적인 예이다. 한편 모든 Object 의 메소드 중 일부는 모든 데이터타입에서 사용될 수 있게 설계 되어 있지는 않다. 이 때문에 그러한 메소드들은 정적인 메소드로 정의되게 되었다.

Object.freeze(...);

Object.entries(...);

Obejcts.keys(...)

이 때문에 위의 메서드들은 Object 키워드와 함께 사용되어야 하는 것이다.


⛳️ 출처 및 참고자료

코어 자바스크립트 - 정재남 지음

profile
For the enjoyful FE development

0개의 댓글