LHJ

I'm a FE developer.

9.2.3 프로토타입

10 May 2020 » js_lj

클래스의 인스턴스에서 사용할 수 있는 메서드라고 하면 그건 프로토타입(prototype) 메서드를 말하는 겁니다.
예를 들어 Car의 인스턴스에서 사용할 수 있는 shift 메서드는 프로토타입 메서드입니다.
프로토타입 메서드는 Car.prototype.shift 처럼 표기할 때가 많습니다.
Array의 forEach를 Array.prototype.forEach 라고 쓰는 것과 마찬가지로 말입니다.
이제 프로토타입이 무엇인지, 자바스크립트가 프로토타입체인을 통해 어떻게 동적 디스패치(dynamic dispatch) 를 구현하는지 알아봅시다.

NOTE_
최근에는 프로토타입 메서드를 #으로 표시하는 표기법이 널리 쓰입니다.
예를 들어 Car.prototype.shift를 Car#shift 로 쓰는 겁니다.

모든 함수에는 prototype이라는 특별한 프로퍼티가 있습니다.
일반적인 함수에서는 프로토타입을 사용할 일이 없지만, 객체 생성자로 동작하는 함수에서는 프로토타입이 대단히 중요합니다.

NOTE_
객체 생성자. 즉 클래스는 Car 처럼 항상 첫 글자를 대문자로 표기합니다.
자바스크립트에서 이런 표기법을 요구하는 것은 아니지만, 일반적인 함수 이름이 대문자로 시작하거나 객체 생성자가 소문자로 시작한다면 이를 경고하는 린트 프로그램이 많습니다.

함수의 prototype 프로퍼티가 중요해지는 시점은 new 키워드로 새 인스턴스를 만들었을 때입니다.
new 키워드로 만든 새 객체는 생성자의 prototype 프로퍼티에 접근할 수 있습니다.
객체 인스턴스는 생성자의 prototype 프로퍼티를 __proto__ 프로퍼티에 저장합니다.

CAUTION_
__proto__ 프로퍼티는 자바스크립트의 내부 동작 방식에 영향을 미칩니다.
밑줄 두 개로 둘러싼 프로퍼티는 모두 그렇습니다.
이런 프로퍼티를 수정하는 것은 정말로 위험합니다.
이들을 적절하고 현명하게 사용할 수 있는 경우가 전혀 없는 건 아니지만, 자바스크립트를 충분히 이해하기 전에는 이들 프로퍼티를 살펴보기만 하고 손대지는 말길 권합니다.

동적 디스패치

프로토타입에서 중요한 것은 동적 디스패치라는 매커니즘입니다.
여기서 디스패치는 메서드 호출과 같은 의미입니다.
객체의 프로퍼티나 메서드에 접근하려 할 때 그런 프로퍼티나 메서드가 존재하지 않으면 자바스크립트는 객체의 프로토타입에서 해당 프로퍼티나 메서드를 찾습니다.
클래스의 인스턴스는 모두 같은 프로토타입을 공유하므로 프로토타입에 프로퍼티나 메서드가 있다면 해당 클래스의 인스턴스는 모두 그 프로퍼티나 메서드에 접근할 수 있습니다.

TIP
클래스의 프로토타입에서 데이터 프로퍼티를 수정하는 것은 일반적으로 권장하지 않습니다.
모든 인스턴스가 그 프로퍼티의 값을 공유하기는 하지만, 인스턴스 중 하나에 그런 이름의 프로퍼티가 있다면 해당 인스턴스는 프로토타입에 있는 값이 아니라 인스턴스에 있는 값을 사용합니다.
이는 혼란과 버그를 초래할 수 있습니다.
인스턴스에 초깃값이 필요하다면 생성자에서 만드는 편이 낫습니다.

인스턴스에서 메서드나 프로퍼티를 정의하면 프로토타입에 있는 것을 가리는 효과가 있습니다.
자바스크립트는 먼저 인스턴스를 체크하고 거기에 없으면 프로토타입을 체크하기 때문입니다.
예제를 봅시다.

const Car = (function() {
	
	const carProps = new WeakMap();
	
	class Car {
		constructor (make, model) {
			this.make = make;
			this.model = model;
			this._userGears = ['P', 'N', 'R', 'D'];
			carProps.set(this, { userGear: this._userGears[0] });
		}
		
		get userGear() { return carProps.get(this).userGear; }
		set userGear(value) {
			if(this._userGears.indexOf(value) < 0)
				throw new Error(`Invalid gear: ${value}`);
			carProps.get(this).userGear = value;
		}
		
		shift(gear) { this.userGear = gear; }
	}
	
	return Car;
})();

const car1 = new Car();
const car2 = new Car();
car1.shift === Car.prototype.shift; // true
car1.shift('D');
car1.shift('d'); // error
car1.userGear; // 'D'
car1.shift === car2.shift // true

car1.shift = function (gear) { this.userGear = gear.toUpperCase(); }
car1.shift === Car.prototype.shift; // false
car1.shift === car2.shift; // false
car1.shift('d');
car1.userGear; // 'D'

이 예제는 자바스크립트에서 동적 디스패치를 어떻게 구현하는지 잘 보여줍니다.
car1 객체에는 shift 메서드가 없지만, car1.shift(‘D’) 를 호출하면 car1의 프로토타입에서 그런 이름의 메서드를 검색합니다.
car1에 shift 메서드를 추가하면 car1과 프로토타입에 같은 이름의 메서드가 존재하게 됩니다.
이제 car1.shift(‘d’)를 호출하면 car1의 메서드가 호출되고 프로토타입의 메서드는 호출되지 않습니다.

프로토타입 체인동적 디스패치를 항상 숙지하고 있을 필요는 없지만, 가끔 문제가 발생할 때 세부사항을 알고 있으면 도움이 됩니다.