참고 자료
https://youtu.be/wUgmzvExL_E
https://youtu.be/ddJcDZHBRm0
프로토타입은 원형, 즉, 원래의 형태를 의미한다.
원형을 의미한다는 말만 봤을 때는 단번에 '아 프로토타입이란 이거군.' 하고 파악할 수 없다.
이해한대로 간단하게 설명하면 자바스크립트는 객체 기반의 스크립트 언어이고 프로토타입은 자바스크립트를 이루고 있는 객체들이 가지는 고유한 객체를 의미한다.
예를 들어 우리가 Array 함수를 쓸 때 sort() 함수를 정의하지 않고도 Array.sort() 이렇게 자연스럽게 호출하여 사용할 수 있는 것은 Array가 가지는 고유한 객체인 프로토타입을 '상속' 받아 그 안의 프로퍼티인 sort()에 접근 가능하기 때문이다.
(보통 array 선언 방식이 let arr = [values...] 이지만 실제 컴퓨터가 선언하는 방식은 let arr = new arr(values...) 형식으로 객체를 생성한다.)
프로토타입은 위에서 언급된 '상속'과 밀접한 관련이 있다.
자바스크립트에서 함수는 객체다! 객체이기 때문에 속성(프로퍼티)들을 가질 수 있다.
자바스크립트에서 클래스 문법을 사용할 때 함수로 지정해둔 기본 객체 속성들을 new 키워드를 사용해 새로운 객체를 생성하여 그대로 갖다 쓸 수 있다.
ex)
<script>
function hamburger () {
this.a = 'bread';
this.b = 'patty';
this.c = 'sauce';
this.d = 'lettuce';
this.e = 'cheese';
this.f = 'pikle';
this.g = 'onion';
}
let mac = new hamburger();
</script>
위의 예시로 선언 시 mac을 콘솔창에 출력하면 hamburger function안에 선언된 속성들이 모두 출력되는 것을 볼 수 있다.
hamburger.prototype 을 출력하면 hamburger 함수가 갖고 있는 고유한 객체인 prototype을 볼 수 있다.
이 prototype에 hamburger.prototype.h = 'salt' 를 입력하여 h라는 키에 'salt' value를 추가한다면?
이 hamburger를 상속받은 객체가 저장된 mac 변수에서도 h라는 속성을 볼 수 있다.
예시를 좀 더 들어보자.
모든 array에서 사용 가능한 함수를 만들고 싶다면?
<script>
Array.prototype.함수 = function() { 구현 ~ };
let arr = [values...];
arr.함수();
</script>
위처럼 정의해보자. 이제 모든 Array에서는 위에 정의된 '함수'라는 함수를 사용할 수 있는 것이다.
위처럼 프로토타입에 속성을 추가하면 자식객체는 그 추가된 속성을 갖지 않고 부모객체만 추가된 속성을 갖게된다.
이렇게 자식객체(mac)은 갖고 있지 않고 부모객체(hamburger)만 갖고 있는 속성을 쓸 수 있는 원리는?
프로토체인 덕분이다.
객체에서 어떤 값을 불러올 때 (ex) mac.a) 해당 값이 호출한 객체에 없으면 해당 객체의 부모객체의 프로토타입에서 찾아본다.
부모객체에도 없으면 부모의 부모 객체의 프로토타입에서 찾아본다.
이렇게 객체에서 원하는 값에 접근할 때 부모객체를 상속 받은 프로토타입을 계속하여 탐색하는 것을 프로토체인이라고 한다.
prototype을 사용하는 문법에는 위와같이 객체.prototype 문법과 __proto__를 이용하는 문법이 있다.
<script>
const car = {
wheels : 4,
drive() {
console.log("drive..");
}
}
const bmw = {
color: "red",
navigation: 1,
};
const benz = {
color: "black"
};
const audi = {
color: "blue"
};
bmw.__proto__ = car;
benz.__proto__ = car;
audi.__proto__ = car;
</script>
위의 객체들 중 car에 있는 wheels와 drive() 속성은 각 객체에 공통으로 들어가는 속성이다.
반복적인 작성을 피하기 위해서 프로토타입을 이용할 수 있다.
상위 객체로 car를 만들어주고 이 car객체를 bmw, benz, audi의 프로토타입을 선언한다. (bmw.__proto__ = car; 이부분)
즉, bmw, benz, audi는 car를 상속받는 것이다.
bmw.wheels 를 출력하면 bmw 내부의 속성을 찾다가 wheels라는 속성이 없기 때문에 __proto__(프로토타입)을 확인한다.
즉, __proto__프로토타입(car를 상속받음) 안에서 wheels 속성을 찾아서 값을 가져오게 된다.
여기서 만약 또 다른 객체가 생성되고 그 새로운 객체가 '객체.__proto__=bmw' 이렇게 상속 받는다면?
객체.wheels 이렇게 새로운 객체에서 wheels를 호출 시
1.'객체'라는 객체를 탐색wheels속성이 없으니까
2. bmw를 상속받은 프로토타입을 탐색, 또 없으니까
3. bmw가 상속 car를 상속받아 갖고 있는 프로토타입을 탐색
이렇게 순차적으로 해당 속성을 갖고 있는 프로토타입을 탐색하게 된다. => 프로토체인!
위 문법을
<script>
const Bmw = function (color) {
this.color = color;
};
Bmw.prototype.wheels = 4;
Bmw.prototype.drive = function () {
console.log("drive..");
}
const x5 = new Bmw("red");
const z4 = new Bmw("blue");
</script>
이렇게 작성하면 생성자 (new Bmw())를 통해서 생성하는 객체에 __proto__를 wheels(Bmw.prototype.wheels 이부분), drive(Bmw.prototype.drive 이부분) 로 설정한다는 것을 의미한다.
prototype 문법을 이용하면 개별적으로 __proto__를 설정할 필요 없이 일괄적으로 프로토타입 설정이 가능하다.
(__proto__는 객체별로 프로토타입을 하나하나 생성하는 느낌이다)
만약 __proto__문법을 썼다면 x5.__proto__, z4.__proto__ 이렇게 객체마다 프로토타입 설정을 해야 하는데 bmw 객체에 prototype으로 프로토타입을 설정해두고 필요한 곳에서 bmw 객체만 상속받아 사용하면 되니까.
생성자 함수를 이용하여 새로운 객체를 만들면 그 새로운 객체를 instance 라고 부른다.
instanceof 함수를 이용하면 생성자와 새로운 객체를 비교할 수 있다.
위의 코드로 예시를 들면 z4 instanceof Bmw 입력시 true가 나온다. z4가 Bmw 생성자로 만들어진 객체, 즉, Bmw의 instance이기 때문이다.
z4.constructor === Bmw; 를 입력해도 true가 나온다. z4의 생성자는 Bmw이기 때문이다.
다만 이런 instance의 값들은 임의로 변경이 가능한데 이런 임의 변경을 막는 방법은 클로저를 이용하는 것이다.
사용전 코드 (color 값을 객체 선언 후 임의로 변경 가능)
<script>
const Bmw = function (color) {
this.color = color;
};
const x5 = new Bmw("red");
</script>
클로저 사용 코드 (선언 이후 임의로 값 변경 불가능)
<script>
const Bmw = function (color) {
const c = color;
this.getColor = function () => {
console.log(c)
}
}
const x5 = new Bmw("red");
</script>
Bmw의 인스턴스 생성 시 생성자로 입력된 "red"를 Bmw 객체 안의 c 변수가 기억하고 있는 클로저를 이용.
this.getColor 안에서 c를 호출하면 초기의 값을 기억한 c 값이 호출된다. 생성자를 통한 변경이 아니면 이후에 직접 속성에 접근하여 수정한 들어가는 것은 불가능하다.