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

이예슬·2023년 2월 19일
0
post-thumbnail

💡 자바스크립트는 프로토타입 기반 언어이다.

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

자바스크립트는 왜 프로토타입을 사용해 상속을 구현할까?

먼저 상속이란 객체지향 프로그래밍의 핵심 개념으로 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다. 자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다. 이 때 중복을 제거하는 방법은 기존의 코드를 적극적으로 재사용하는 것이다.

function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radisus ** 2;
  };
}

// 반지름이 1인 인스턴스 생성
const circle1 = new Circle(1);

// 반지름이 2인 인스턴스 생성
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea); //false

function CircleByPrototype(radius) {
  this.radius = radius;
}

위 코드는 생성자 함수안에 메서드를 만들어 상속을 구현한 방법이다. 해당 코드에서 Circle 생성자 함수가 생성하는 모든 객체는 radius 프로퍼티와 getArea 메서드를 가진다.

즉 인스턴스를 생성할 때마다 getArea 메서드를 중복으로 생성하고 모든 인스턴스가 중복 소유하게 된다.

⇒ 메모리를 불필요하게 낭비하고 퍼포먼스에도 악영향을 끼친다.

function CircleByPrototype(radius) {
  this.radius = radius;
}

CircleByPrototype.prototype.getArea = function () {
  return Math.PI * this.radisus ** 2;
};

// 반지름이 1인 인스턴스 생성
const CircleByPrototype1 = new CircleByPrototype(1);

// 반지름이 2인 인스턴스 생성
const CircleByPrototype2 = new CircleByPrototype(2);

console.log(CircleByPrototype1.getArea === CircleByPrototype2.getArea); //true

같은 내용을 프로토타입을 사용해 구현한 코드이다. 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입 즉 상위 객체의 프로토타입의 모든 프로퍼티와 메서드를 상속받는다.

즉 동일한 내용의 메서드를 중복 생성하지 않고 모든 인스턴스가 getArea 메서드를 상속받아 사용할 수 있다.

프로토타입의 개념 이해

constructor, prototype, instance

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

prototype은 객체이며 이를 참조하는 proto 역시 객체이다. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장하고 인스턴스에서도 숨겨진 프로퍼티인 proto를 통해 이 메서드들에 접근할 수 있다.

__proto__를 코드 내에서 집적 사용하는 것은 권장하지 않는다. (그 대신Object.getPrototypeOf, Object.setPrototypeOf를 사용할 수 있다!)

프로토타입 내부 슬롯([[prototype]])에 직접 접근할 수 없는 이유?

⇒ 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서

⇒ 프로토타입 체인은 단방향 링크드 리스트로 구성되어야 한다.

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

instance의 proto가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라본다. 즉 Person의 인스턴스는 proto 프로퍼티를 통해 getName을 호출할 수 있다.

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

위 코드에서 undefined가 출력된 이유는 this에 바인딩된 대상이 잘못 지정됐기 때문이다.

어떤 함수를 메서드로서 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 되므로 위 코드에서 this가 바라보고 있는 것은 suzi가 아니라 suzi.__proto__ 가 된다. 이 객체 내부에는 name 프로퍼티가 없으므로 찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 반환하는 자바스크립트 규약에 의해 undefined가 반환됐다.

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

suzi.__proto__.name 에 할당하면 원하는 값을 얻는 것을 확인할 수 있다.

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

proto는 생략 가능한 프로퍼티이므로 위와 같은 코드가 가능하다.

proto를 생략하지 않으면 this는 suzi.proto를 가리키지만 이를 생략하면 suzi를 가리킨다. suzi.proto에 있는 메서드인 getName을 실행하지만 this는 suzi를 바라보게 할 수 있게 된 것이다.

var Constructor = function(name) {
	this.name = name;
};
Constructor.prototype.method1 = function() {}; 
Constructor.prototype.property1 = 'Counstructor Prototype Property'; 

var instance = new Constructor('instance')
console.dir(Constructor)
console.dir(instance)

위 예제와 같이 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.

옅은 색 짙은 색의 차이는 { enumerable : false } 속성이 부여된 프로퍼티인지 여부에 따른다. 짙은색은 enumerable 즉 열거가능한 프로퍼티임을 의미하고 옅은 색은 innumerable 즉 열거할 수 없는 프로퍼티임을 의미한다.

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

Constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 이 프로퍼티는 단어 그대로 원래의 생성자 함수를 참조한다.

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

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

인스턴스의 proto가 생성자 함수의 prototype 프로퍼티를 참조하며 proto가 생략 가능하므로 인스턴스가 직접 constructor에 접근할 수 있다.

constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean)를 제외하고는 값을 바꿀 수 있는데 이는 단지 참조하는 대상이 변경되는 것일 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것이 아니다.

그러므로 어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 게 항상 안전하지는 않다.

프로토타입 체인

메서드 오버라이드

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.protogetName이 아닌 iu 객체에 있는 getName 메서드가 호출되었다. 메서드 위에 메서드를 덮어씌운 것으로 이를 메서드 오버라이드라고 한다.

가장 가까운 대상인 자신의 프로퍼티를 검색하고 없으면 그 다음으로 가까운 대상인 proto를 검색하는 순서로 진행된다.

console.log(iu.__proto__.getName()); // undefined 
Person.prototype.name = '이지금';
console.log(iu.__proto__.getName()); // 이지금 
console.log(iu.__proto__.getName.call(iu)); // 지금 // this가 prototype을 바라보고 있으므로 call을 사용하였다 

프로토타입 체인

object의 인스턴스이며 constructor는 생성자 함수인 Object를 가리키고 있다.

console.dir([1, 2])

어떤 데이터의 proto 프로퍼티 내부에 다시 proto 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라 하고 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 한다.

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

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다.

객체에서만 사용할 메서드를 Object.prototype 내부에 정의하면 다른 데이터 타입도 해당 메서드를 사용할 수 있으므로 겍체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의해서는 안된다.

Object.create를 이용하면 Object.prototype의 메서드에 접근할 수 없는 경우가 있다. Object.create(null)은 proto가 없는 객체를 생성할 수 있다.

다중 프로토타입 체인

프로토타입 체인을 이용하여 proto를 연결해나가면 무한대로 체인 관계를 이어나갈 수 있다.

proto를 연결하기 위해서는 proto가 가리키는 대상인 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게 해줘야 한다.

var Grade = function() { // 유사배열객체
	var args = Array.prototype.slice.call(arguments);
	for(var i = 0; i < args.length; i++) {
		this[i] = args[i]; 
	} 
};
var g = new Grade(100, 80) 

생성자 함수 Grade는 유사배열객체이므로 배열 메서드를 직접 사용할 수 있다. 만약 Grade의 인스턴스인 g가 배열 메서드를 직접 사용하게끔 하고 싶다면 g.proto 즉 Grade가 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = [];

<코어 자바스크립트> 정재남, 위키북스(2019)

profile
꾸준히 열심히!

0개의 댓글